Skip to content

AgentInterface:會話宿主與事件分發

源码版本v0.73.1

AgentInterfacepi-web-ui 裡夾在 pi-agent-coreAgent 和具體訊息渲染元件之間的一層 Lit customElement。它持有一個 Agent 實例作為 session 屬性,負責裝配 streamFngetApiKey,訂閱 Agent 事件,把使用者在 MessageEditor 裡敲的文字轉成 prompt() 呼叫,再把回流的事件分發給 MessageListStreamingMessageContainer。本身不碰 LLM,也不存歷史,只是一個有狀態的事件路由器。

職責

  1. 持有 session:@property session 指向一個 Agent 實例,session 變更時重新訂閱事件,見 packages/web-ui/src/components/AgentInterface.ts:20-47
  2. 裝配 streamFn 與 getApiKey:connectedCallback 時若 session.streamFn 還是預設的 streamSimple,就換成帶代理支援的 createStreamFn,同時注入預設 getApiKeyAppStorage 讀 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 只管渲染,三者透過它對接 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 迴圈並回流到渲染:

邊界與失敗

小結

AgentInterface 是瀏覽器側的「會話宿主」:裝配 streamFngetApiKey、訂閱事件、分發到渲染元件。往上由 ChatPanel 裝入布局,往下驅動 pi-agent-coreAgent。代理決策細節看 CORS 代理與 createStreamFn,渲染層元件看 訊息渲染元件