AgentInterface:會話宿主與事件分發
AgentInterface 是 pi-web-ui 裡夾在 pi-agent-core 的 Agent 和具體訊息渲染元件之間的一層 Lit customElement。它持有一個 Agent 實例作為 session 屬性,負責裝配 streamFn 與 getApiKey,訂閱 Agent 事件,把使用者在 MessageEditor 裡敲的文字轉成 prompt() 呼叫,再把回流的事件分發給 MessageList 和 StreamingMessageContainer。本身不碰 LLM,也不存歷史,只是一個有狀態的事件路由器。
職責
- 持有 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 只管渲染,三者透過它對接 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 沒初始化模型。 - 串流中再發訊息:
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 是瀏覽器側的「會話宿主」:裝配 streamFn 與 getApiKey、訂閱事件、分發到渲染元件。往上由 ChatPanel 裝入布局,往下驅動 pi-agent-core 的 Agent。代理決策細節看 CORS 代理與 createStreamFn,渲染層元件看 訊息渲染元件。