AgentInterface:セッションホストとイベントディスパッチ
AgentInterface は pi-web-ui の中で pi-agent-core の Agent と具体的なメッセージ描画コンポーネントの間に挟まる Lit customElement だ。Agent インスタンスを session プロパティとして持ち、streamFn と getApiKey の組み立て、Agent イベントのサブスクライブ、MessageEditor で打たれたテキストの prompt() 呼び出しへの変換、そして戻ってきたイベントの MessageList と StreamingMessageContainer へのディスパッチを引き受ける。自身は LLM を叩かないし、履歴も持たない。状態を持つイベントルータ (event router) に過ぎない。
責務
- session を保持:
@property sessionがAgentインスタンスを指す。session が差し替わるとイベントを再サブスクライブする。packages/web-ui/src/components/AgentInterface.ts:20-47参照。 - streamFn と getApiKey の組み立て:
connectedCallbackでsession.streamFnがデフォルトのstreamSimpleのままなら、プロキシ対応のcreateStreamFnに差し替える。同時にデフォルトのgetApiKeyを注入し、AppStorageから key を読む。packages/web-ui/src/components/AgentInterface.ts:130-152参照。 - イベントのサブスクライブ:
session.subscribeでコールバックを登録し、message_start/message_update/message_end/agent_endなどのイベントをStreamingMessageContainer.setMessageとrequestUpdateに振り分ける。packages/web-ui/src/components/AgentInterface.ts:153-187参照。 - メッセージ送信:
sendMessage(input, attachments)は model と API key を検証し、key が無ければonApiKeyRequiredを呼ぶ。そのうえでsession.prompt(input)、または添付ありのprompt(message)を呼ぶ。packages/web-ui/src/components/AgentInterface.ts:215-262参照。 - 自動スクロール:
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 参照。
主要ファイル
packages/web-ui/src/components/AgentInterface.ts:20-47—class AgentInterface宣言とすべての@property/@queryフィールド。packages/web-ui/src/components/AgentInterface.ts:77-109—connectedCallback:初回フレーム後に scroll container を取り、ResizeObserverを取り付け、setupSessionSubscriptionを呼ぶ。packages/web-ui/src/components/AgentInterface.ts:130-152— デフォルトstreamFnの差し替え、getApiKeyの注入。packages/web-ui/src/components/AgentInterface.ts:153-187—session.subscribeコールバック、イベントタイプごとに振り分け。packages/web-ui/src/components/AgentInterface.ts:215-262—sendMessage:key 検証、onBeforeSendフック、エディタのクリア、session.prompt呼び出し。packages/web-ui/src/components/AgentInterface.ts:264-297—renderMessages:MessageListとStreamingMessageContainerを組み立て、pendingToolCallsとtoolResultsByIdを渡す。packages/web-ui/src/components/AgentInterface.ts:111-128—disconnectedCallback:observer、listener、unsubscribe の後始末。
setupSessionSubscription が組み立ての中心で、streamFn も getApiKey もここでデフォルト実装を差し替える:
// 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 にダイアログの出し方を任せる:
// 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」プレースホルダをそのまま出す。sendMessageはNo session set on AgentInterfaceを投げる。packages/web-ui/src/components/AgentInterface.ts:216-219参照。 - model 未設定:同じく
sendMessageでNo model setを投げる。host が model を初期化していない合図。 - ストリーミング中の再送:
isStreamingが true の間はsendMessageが即座に return する。キューにも積まないし割り込みもしない。packages/web-ui/src/components/AgentInterface.ts:216参照。 - 重複サブスクライブ:
setupSessionSubscriptionは先に_unsubscribeSession()で古い購読を切ってから新しいのを繋ぐ。session 切り替えでリスナが漏れない。 - メッセージ重複:
message_endでStreamingMessageContainer.setMessage(null, true)を呼んでクリアする。安定リストに既にそのメッセージがあるとき、ストリーミングコンテナが二重に描くのを防ぐ。packages/web-ui/src/components/AgentInterface.ts:161-168参照。
まとめ
AgentInterface はブラウザ側の「セッションホスト (session host)」だ。streamFn と getApiKey を組み立て、イベントをサブスクライブし、描画コンポーネントにディスパッチする。上は ChatPanel がレイアウトに組み込み、下は pi-agent-core の Agent を駆動する。プロキシ判定の詳細は CORS プロキシと createStreamFn、描画層のコンポーネントは メッセージ描画コンポーネント を参照。