简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

モジュール間通信

Note

本章の内容は ErisPulse 2.8.0+ が必要です。

ErisPulse のモジュール間には3つの通信モデルがあり、"点対点 → 定向 → ブロードキャスト"の順序で配置されています:

層 API 語義 典型的な場面
RPC await sdk.module.call("Chat", "get_history", ...) 点対点のリクエスト-レスポンス、契約 / 審計 / タイムアウト付き 他のモジュールの機能を呼び出す(履歴の取得、翻訳、返金など)
定向イベント await lifecycle.emit("message_received", {...}, to="Chat") 指定されたモジュールが登録したライフサイクルフックにのみ配信 上流の状態変化を下流に通知する("新しいメッセージを受け取りました")
ブロードキャスト await lifecycle.emit("config.updated", {...}) フレームワーク全体で見えるライフサイクルイベント 設定のホット更新、モジュールの起動・停止

{!--< tips >!--} 選択の口訣:返り値が必要な場合は call を使い、特定のモジュールのフックに通知する場合は emit(..., to=...) を使い、全員に通知する場合は emit(...) を使う。 {!--< /tips >!--}

RPC:module.call

result = await sdk.module.call("Chat", "get_history", session_id, n=20)

裸属性访问 sdk.module.Chat.get_history(...)(保持不变)与 module.call() 的差异:

module.call() 裸属性アクセス
目標が登録されていない / 有効化されていない ModuleNotAvailableError をスロー AttributeError をスロー
ラグジュアリーなモジュール 自動的に起動(イベント駆動モジュールはアクティベーションロックを経由) 非同期初期化モジュールが RuntimeError をスロー
current_owner 対象モジュールに帰属(内部の wait_reply / 送信 / ログは正しく所有者に属する) 呼び出し元のまま
タイムアウト デフォルト 30 秒(timeout= で上書き、None で無制限) なし
scope 審査 呼び出し元の出力ゲート actions.<呼び出し元>.call なし
契約検証 meta.services のホワイトリスト なし

例外体系

ModuleError                      # モジュールシステムの例外基底クラス
└── ModuleCallError              # モジュール間呼び出しの基底クラス(module / method 属性を含む)
    ├── ModuleNotAvailableError  # 目標が登録されていない / 有効化されていない / 起動に失敗
    ├── ServiceNotProvidedError  # メソッドが services ホワイトリストにない / 私有メソッド / 存在しない
    └── ModuleCallTimeoutError   # コルーチンメソッドのタイムアウト

すべて ErisPulseError 体系に属し、from ErisPulse.Core import ModuleCallError でキャッチ可能です。

サービス契約:meta.services

サービス提供者は get_meta() で公開する白リストを宣言します(commands フィールドと対称):

from ErisPulse.Core.Bases import BaseModule, ModuleMeta

class ChatModule(BaseModule):
    @staticmethod
    def get_meta() -> ModuleMeta:
        return ModuleMeta(
            name="チャット",
            services=[
                "get_history",                                       # 簡単な形
                {"name": "translate", "description": "テキストを指定された言語に翻訳する"},  # 説明付き
            ],
        )

    async def get_history(self, session_id, n=20): ...
    async def translate(self, text, target_lang): ...
    def _internal_helper(self): ...   # アンダースコア付きメソッドは外部からの呼び出しを常に禁止

開発者にとっては無視されるのがデフォルト:

サービスの説明:各サービスに人間やAIが読める説明をつける——不要なら何も書かなくてもよい。 説明は自動的にメソッドの docstring の最初の行を取る(フレームワークは docstring 形式を要求している):

async def translate(self, text, target_lang):
    """テキストを指定された言語に翻訳する"""    # ← この行が自動的にサービスの説明になる
    ...

docstring に上書きしたい、または多言語に対応したいなどの細かい制御が必要な場合は、dict 形式で description を宣言する(i18n 辞書に対応):

services=[
    {"name": "translate", "description": "テキストを指定された言語に翻訳する"},
    {"name": "summarize", "description": {"i18n": "Chat.meta.svc.summarize", "default": "会話の要約"}},
]

サービスディレクトリ: services()

sdk.module.services()
# {'Chat': [{'name': 'get_history', 'signature': '(session_id, n=20)',
#            'description': 'テキストを指定された言語に翻訳する'}]}

sdk.module.services("Chat")   # 指定されたモジュールのみを照会

{!--< tips >!--} MCP 化の道筋:サービスディレクトリ(名前 + 署名 + 説明)は、MCP ツールの構造に自然に適合する—— 各サービスは天然に {"name", "description", "parameters"} の形をとる。 将来、フレームワークは services() を直接 MCP サーバーエンドポイントとして公開し、AI がモジュールの機能を発見して呼び出すことが可能になる。 また、scope.actions.call の監査は、AI 呼び出しのセキュリティゲートとして自然に機能する。 {!--< /tips >!--}

出向監査:誰が誰を呼び出すか

module.call() のたびに、呼び出し元モジュールの身分としてスコープの出向ゲートを通過します:

[ErisPulse.scope.actions.CallerModule.call]
deny = ["Chat.get_history"]        # CallerModule が Chat の get_history を呼び出すことを禁止
# allow = ["Chat.get_*"]           # またはホワイトリスト:get で始まる Chat のサービスのみを許可

設定方法は スコープ(scope) の出向の観点を参照してください。

定向イベント: lifecycle.emit の to パラメータ

ライフサイクルイベントは、to パラメータで送信先の所有者(owner)を指定することで、特定のオーナーにイベントを送信できます。この場合、イベントはそのオーナーとして登録されたフック(モジュールが on_load 内で登録するフックは自動的に自身のモジュールに属します)にのみ配信され、他のモジュールやワイルドカード * のハンドラはイベントを感知しません。

from ErisPulse.Core.lifecycle import lifecycle

# 送信側:イベントは Chat モジュールが登録したフックにのみ送信されます
await lifecycle.emit("message_received", {"text": "hi", "from": "u1"}, to="Chat")

# 受信側(Chat モジュール内):同名のフックを登録し、owner は登録時に自動的に記録されます
@lifecycle.on("message_received")
async def on_message_received(data): ...

@lifecycle.on("message")          # 点式の親プレフィックスも同様に有効(owner でフィルタリング)
async def on_any(data): ...

動作の詳細:

Note


定向イベントは軽量な通知であり、送信先の存在確認や遅延起動は行いません。送信先の存在確認、契約の監査、または戻り値が必要な場合は、RPC: module.call を使用してください。

慢的ロードと呼び出し

module.call() は、遅延ロードモジュールに対して透明な起動を提供します:

つまり、呼び出し元は対象モジュールが既にロードされているかどうかを気にする必要がなく、またその起動のために特定のイベントを待つ必要もありません。

対象イベント(lifecycle.emit(..., to=...))は遅延起動を行いません。対象がロードされていない場合、フックは存在せず、イベントは静かに破棄されます。確実に送信する必要がある場合は、module.call() を使用してください。

クールスタートリプレイ

新規インストール / 再起動のモジュールが一部のチャットを逃した場合、get_load_strategy(replay=...) により、フレームワークはモジュールが準備完了した後に、セッション受信箱内の最近のメッセージをそのモジュール自身にリプレイします。

from ErisPulse.loaders import ModuleLoadStrategy

class MyAIModule(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(
            lazy_load=False,
            priority=100,
            replay="5m",        # 最近の 5 分間をリプレイ ("1h" / "300" 秒の書き方も可能です)
        )

    async def on_load(self, event):
        @message.on_message()
        async def handle(e):
            if e.get("replayed"):
                # 合成イベント: コンテキストのみ補完、送信などの副作用は発生しない
                ...

意味の詳細:

イベントの冪等性と重複除去

プラットフォームの WebSocket 再接続後に、同じイベント(同じ event["id"])が頻繁に再送されることがあります。イベントの配信エントリポイントでは、ID に基づいて LRU 重複除去(容量 4096)が行われ、同じ ID のイベントは一度だけ配信されます。

[ErisPulse.framework]
event_dedupe = true   # デフォルトで有効。テスト環境では固定 ID で合成イベントを作成する場合は無効にできます。

アダプタが登録(新しい接続のライフサイクルの起点)される際に、自動的に重複除去キャッシュがリセットされます。

関連ドキュメント