Skip to content

AgentInterface:セッションホストとイベントディスパッチ

源码版本v0.73.1

AgentInterfacepi-web-ui の中で pi-agent-coreAgent と具体的なメッセージ描画コンポーネントの間に挟まる Lit customElement だ。Agent インスタンスを session プロパティとして持ち、streamFngetApiKey の組み立て、Agent イベントのサブスクライブ、MessageEditor で打たれたテキストの prompt() 呼び出しへの変換、そして戻ってきたイベントの MessageListStreamingMessageContainer へのディスパッチを引き受ける。自身は LLM を叩かないし、履歴も持たない。状態を持つイベントルータ (event router) に過ぎない。

責務

  1. session を保持:@property sessionAgent インスタンスを指す。session が差し替わるとイベントを再サブスクライブする。packages/web-ui/src/components/AgentInterface.ts:20-47 参照。
  2. streamFn と getApiKey の組み立て:connectedCallbacksession.streamFn がデフォルトの streamSimple のままなら、プロキシ対応の createStreamFn に差し替える。同時にデフォルトの getApiKey を注入し、AppStorage から key を読む。packages/web-ui/src/components/AgentInterface.ts:130-152 参照。
  3. イベントのサブスクライブ:session.subscribe でコールバックを登録し、message_start/message_update/message_end/agent_end などのイベントを StreamingMessageContainer.setMessagerequestUpdate に振り分ける。packages/web-ui/src/components/AgentInterface.ts:153-187 参照。
  4. メッセージ送信:sendMessage(input, attachments) は model と API key を検証し、key が無ければ onApiKeyRequired を呼ぶ。そのうえで session.prompt(input)、または添付ありの prompt(message) を呼ぶ。packages/web-ui/src/components/AgentInterface.ts:215-262 参照。
  5. 自動スクロール:ResizeObserver で内容の高さ変化を観察しつつ、scroll イベントでユーザーが上へ戻ったかを判定し、自動で底に貼るかを決める。packages/web-ui/src/components/AgentInterface.ts:89-105 参照。

設計動機

なぜ MessageEditor に直接 Agent.prompt を呼ばせないのか。ブラウザ側には雑用がいくつも残っているからだ。プロキシ URL を provider/key ごとに動的に決める、API key を IndexedDB から非同期で取る、key が無ければダイアログを出す、ストリーミング中は streaming コンテナと安定リストの重複を排除する。これらを各 host アプリに詰め込むと重複するし、メッセージコンポーネントに入れると描画層がストレージ層に結合してしまう。AgentInterface を唯一の入口として切り出すことで、ChatPanel はレイアウトだけ、MessageEditor は入力だけ、MessageList は描画だけを担い、3 者はこれを経由して Agent に接続する。

もう一つの動機は session の差し替え可能性だ。ユーザーが会話を切り替えてもコンポーネント全体を作り直さず、session プロパティだけ交換する。willUpdate が変更を検出すると setupSessionSubscription() で再サブスクライブする。packages/web-ui/src/components/AgentInterface.ts:68-75 参照。

主要ファイル

setupSessionSubscription が組み立ての中心で、streamFn も getApiKey もここでデフォルト実装を差し替える:

typescript
// packages/web-ui/src/components/AgentInterface.ts:138-151
if (this.session.streamFn === streamSimple) {
    this.session.streamFn = createStreamFn(async () => {
        const enabled = await getAppStorage().settings.get<boolean>("proxy.enabled");
        return enabled ? (await getAppStorage().settings.get<string>("proxy.url")) || undefined : undefined;
    });
}

if (!this.session.getApiKey) {
    this.session.getApiKey = async (provider: string) => {
        const key = await getAppStorage().providerKeys.get(provider);
        return key ?? undefined;
    };
}

sendMessage は key 検証と onApiKeyRequired コールバックを繋げ、key が無ければ host にダイアログの出し方を任せる:

typescript
// packages/web-ui/src/components/AgentInterface.ts:222-238
const provider = session.state.model.provider;
const apiKey = await getAppStorage().providerKeys.get(provider);

if (!apiKey) {
    if (!this.onApiKeyRequired) {
        console.error("No API key configured and no onApiKeyRequired handler set");
        return;
    }
    const success = await this.onApiKeyRequired(provider);
    if (!success) {
        return;
    }
}

データフロー

ユーザーが MessageEditor で Enter を押すと、key 検証、プロキシ組み立てを経て、最終的に Agent ループを起動し、戻ってきたイベントが描画に流れる:

境界と失敗

  • session 未設定:render は「No session set」プレースホルダをそのまま出す。sendMessageNo session set on AgentInterface を投げる。packages/web-ui/src/components/AgentInterface.ts:216-219 参照。
  • model 未設定:同じく sendMessageNo model set を投げる。host が model を初期化していない合図。
  • ストリーミング中の再送:isStreaming が true の間は sendMessage が即座に return する。キューにも積まないし割り込みもしない。packages/web-ui/src/components/AgentInterface.ts:216 参照。
  • 重複サブスクライブ:setupSessionSubscription は先に _unsubscribeSession() で古い購読を切ってから新しいのを繋ぐ。session 切り替えでリスナが漏れない。
  • メッセージ重複:message_endStreamingMessageContainer.setMessage(null, true) を呼んでクリアする。安定リストに既にそのメッセージがあるとき、ストリーミングコンテナが二重に描くのを防ぐ。packages/web-ui/src/components/AgentInterface.ts:161-168 参照。

まとめ

AgentInterface はブラウザ側の「セッションホスト (session host)」だ。streamFngetApiKey を組み立て、イベントをサブスクライブし、描画コンポーネントにディスパッチする。上は ChatPanel がレイアウトに組み込み、下は pi-agent-coreAgent を駆動する。プロキシ判定の詳細は CORS プロキシと createStreamFn、描画層のコンポーネントは メッセージ描画コンポーネント を参照。