AgentInterface: Session Host and Event Dispatcher
AgentInterface is a Lit customElement in pi-web-ui that sits between the Agent from pi-agent-core and the concrete message rendering components. It holds an Agent instance as the session property, wires up streamFn and getApiKey, subscribes to Agent events, turns the text typed in MessageEditor into prompt() calls, and dispatches the events that come back to MessageList and StreamingMessageContainer. It does not touch the LLM itself, nor does it store history — it is just a stateful event router.
Responsibilities
- Hold session: the
@property sessionpoints at anAgentinstance; when session changes the event subscription is refreshed, seepackages/web-ui/src/components/AgentInterface.ts:20-47. - Wire streamFn and getApiKey: in
connectedCallback, ifsession.streamFnis still the defaultstreamSimple, swap it forcreateStreamFnwith proxy support, and inject a defaultgetApiKeythat reads the key fromAppStorage, seepackages/web-ui/src/components/AgentInterface.ts:130-152. - Subscribe to events:
session.subscriberegisters callbacks that mapmessage_start/message_update/message_end/agent_endand similar events toStreamingMessageContainer.setMessageandrequestUpdate, seepackages/web-ui/src/components/AgentInterface.ts:153-187. - Send messages:
sendMessage(input, attachments)validates model and API key; when a key is missing it calls back toonApiKeyRequired, then invokessession.prompt(input)orprompt(message)with attachments, seepackages/web-ui/src/components/AgentInterface.ts:215-262. - Auto-scroll: a
ResizeObserverwatches content height, and together with thescrollevent it detects whether the user has manually scrolled up, deciding whether to stick to the bottom, seepackages/web-ui/src/components/AgentInterface.ts:89-105.
Design motivation
Why not let MessageEditor call Agent.prompt directly? Because the browser side still has a handful of chores: the proxy URL has to be decided dynamically based on provider/key, the API key has to be fetched asynchronously from IndexedDB, a dialog has to pop when the key is missing, and during streaming the streaming container has to stay in sync with the stable list and avoid duplicate renders. Putting this logic into every host app would duplicate it, and pushing it into the message components would couple the render layer to the storage layer. AgentInterface is extracted as the single entry point: ChatPanel only handles layout, MessageEditor only handles input, MessageList only handles rendering, and all three reach Agent through it.
The other motivation is that session is swappable: when the user switches sessions the whole component is not rebuilt, only the session property changes, and willUpdate detects the change and calls setupSessionSubscription() to resubscribe, see packages/web-ui/src/components/AgentInterface.ts:68-75.
Key files
packages/web-ui/src/components/AgentInterface.ts:20-47—class AgentInterfacedeclaration and all@property/@queryfields.packages/web-ui/src/components/AgentInterface.ts:77-109—connectedCallback: wait for first frame, grab the scroll container, attachResizeObserver, callsetupSessionSubscription.packages/web-ui/src/components/AgentInterface.ts:130-152— replace the defaultstreamFn, injectgetApiKey.packages/web-ui/src/components/AgentInterface.ts:153-187—session.subscribecallbacks, dispatching by event type.packages/web-ui/src/components/AgentInterface.ts:215-262—sendMessage: key validation,onBeforeSendhook, clearing the editor, callingsession.prompt.packages/web-ui/src/components/AgentInterface.ts:264-297—renderMessages: assemblingMessageListandStreamingMessageContainer, passing inpendingToolCallsandtoolResultsById.packages/web-ui/src/components/AgentInterface.ts:111-128—disconnectedCallback: tear down observer, listeners, unsubscribe.
setupSessionSubscription is the core wiring point — streamFn and getApiKey both get their default implementations replaced here:
// 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 chains key validation and the onApiKeyRequired callback — when a key is missing the host decides how to prompt for it:
// 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;
}
}Data flow
When the user hits enter in MessageEditor, after key validation and proxy wiring, the Agent loop is triggered and events flow back to rendering:
Edges and failures
- Session unset:
rendershows a "No session set" placeholder;sendMessagethrowsNo session set on AgentInterface, seepackages/web-ui/src/components/AgentInterface.ts:216-219. - Model unset:
sendMessagealso throwsNo model set, signaling that the host did not initialize the model. - Sending while streaming: when
isStreamingis true,sendMessagereturns immediately — no queuing, no interrupting, seepackages/web-ui/src/components/AgentInterface.ts:216. - Duplicate subscriptions:
setupSessionSubscriptioncalls_unsubscribeSession()on the old subscription before attaching a new one, so session switches do not leak listeners. - Message dedup: on
message_end,StreamingMessageContainer.setMessage(null, true)clears the streaming container, so the stable list (which already has this message) does not get a duplicate render, seepackages/web-ui/src/components/AgentInterface.ts:161-168.
Summary
AgentInterface is the browser-side "session host": it wires up streamFn and getApiKey, subscribes to events, and dispatches them to the rendering components. Upward it is mounted into layout by ChatPanel; downward it drives the Agent from pi-agent-core. For proxy decision details see CORS proxy and createStreamFn; for the rendering layer components see Message rendering components.