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 按回车,经过 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,渲染层组件看 消息渲染组件。