AgentInterface: host de sesión y dispatch de eventos
AgentInterface es la capa intermedia de pi-web-ui, entre el Agent de pi-agent-core y los componentes de render de mensajes concretos, como un customElement de Lit. Mantiene una instancia de Agent como propiedad session, se ocupa de ensamblar streamFn y getApiKey, suscribirse a los eventos del Agent, convertir el texto que el usuario teclea en MessageEditor en llamadas prompt(), y repartir los eventos entrantes a MessageList y StreamingMessageContainer. No toca el LLM ni guarda historial; es sólo un router de eventos con estado.
Responsabilidades
- Mantener session: la propiedad
@property sessionapunta a una instancia deAgent; al cambiar, se vuelve a suscribir a los eventos. Verpackages/web-ui/src/components/AgentInterface.ts:20-47. - Ensamlar streamFn y getApiKey: en
connectedCallback, sisession.streamFnsigue siendo elstreamSimplepor defecto, lo reemplaza porcreateStreamFncon soporte de proxy; al mismo tiempo inyecta ungetApiKeypor defecto que lee la key desdeAppStorage. Verpackages/web-ui/src/components/AgentInterface.ts:130-152. - Suscripción a eventos:
session.subscriberegistra un callback que mapeamessage_start/message_update/message_end/agent_endetc. aStreamingMessageContainer.setMessageyrequestUpdate. Verpackages/web-ui/src/components/AgentInterface.ts:153-187. - Enviar mensaje:
sendMessage(input, attachments)valida modelo y API key; si falta key, llama al callbackonApiKeyRequired, y luego invocasession.prompt(input)oprompt(message)con attachments. Verpackages/web-ui/src/components/AgentInterface.ts:215-262. - Auto-scroll: un
ResizeObserverobserva los cambios de altura del contenido, combinado con un eventoscrollpara decidir si el usuario ha hecho scroll arriba y si hay que quedarse pegado abajo. Verpackages/web-ui/src/components/AgentInterface.ts:89-105.
Motivación de diseño
¿Por qué no dejar que MessageEditor llame directamente a Agent.prompt? Porque en el lado del navegador faltan varias tareas: la URL del proxy hay que decidirla dinámicamente según provider/key; la API key se lee de forma async desde IndexedDB; si falta key hay que mostrar un diálogo; durante el streaming hay que gestionar la desduplicación entre el contenedor de streaming y la lista estable. Meter todo eso en cada host app lo duplicaría; ponerlo en los componentes de mensaje acoplaría la capa de render con la de storage. AgentInterface se abstrae como única entrada: ChatPanel sólo se ocupa del layout, MessageEditor sólo de la entrada, MessageList sólo del render; los tres se conectan al Agent a través de él.
Otra motivación es que session es reemplazable: al cambiar de sesión el usuario no recrea todo el componente, sólo cambia la propiedad session; willUpdate detecta el cambio y llama setupSessionSubscription() para re-suscribir. Ver packages/web-ui/src/components/AgentInterface.ts:68-75.
Archivos clave
packages/web-ui/src/components/AgentInterface.ts:20-47— Declaración declass AgentInterfacey todos los campos@property/@query.packages/web-ui/src/components/AgentInterface.ts:77-109—connectedCallback: tras el primer frame obtiene el scroll container, cuelgaResizeObserver, y llamasetupSessionSubscription.packages/web-ui/src/components/AgentInterface.ts:130-152— Reemplaza elstreamFnpor defecto e inyectagetApiKey.packages/web-ui/src/components/AgentInterface.ts:153-187— Callback desession.subscribe, distribuye por tipo de evento.packages/web-ui/src/components/AgentInterface.ts:215-262—sendMessage: validación de key, hookonBeforeSend, limpia editor, invocasession.prompt.packages/web-ui/src/components/AgentInterface.ts:264-297—renderMessages: ensamblaMessageListyStreamingMessageContainer, pasandopendingToolCallsytoolResultsById.packages/web-ui/src/components/AgentInterface.ts:111-128—disconnectedCallback: limpia observer, listener y unsubscribe.
setupSessionSubscription es el punto central de ensamblaje; aquí se reemplazan las implementaciones por defecto de streamFn y 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 encadena validación de key con el callback onApiKeyRequired, dejando al host decidir cómo mostrar el diálogo cuando falta key:
// 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;
}
}Flujo de datos
El usuario pulsa Enter en MessageEditor, pasa por validación de key y ensamblaje de proxy, y finalmente dispara el bucle del Agent y los eventos que llegan al render:
Límites y fallos
- Session sin setear:
rendermuestra un placeholder "No session set";sendMessagelanzaNo session set on AgentInterface. Verpackages/web-ui/src/components/AgentInterface.ts:216-219. - Model sin setear: igual,
sendMessagelanzaNo model set, avisando al host que no inicializó el modelo. - Mensaje mientras hace streaming: si
isStreaminges true,sendMessageretorna sin encolar ni interrumpir. Verpackages/web-ui/src/components/AgentInterface.ts:216. - Suscripción duplicada:
setupSessionSubscriptionprimero hace_unsubscribeSession()de la suscripción vieja antes de colgar la nueva; al cambiar de session no se filtran listeners. - Deduplicación de mensajes: en
message_end,StreamingMessageContainer.setMessage(null, true)limpia, evitando que la lista estable que ya tiene el mensaje lo vuelva a renderizar el contenedor de streaming. Verpackages/web-ui/src/components/AgentInterface.ts:161-168.
Resumen
AgentInterface es el "host de sesión" del lado del navegador: ensambla streamFn y getApiKey, se suscribe a eventos y los reparte a los componentes de render. Arriba lo monta ChatPanel en su layout; abajo dirige al Agent de pi-agent-core. Los detalles de decisión de proxy en proxy CORS y createStreamFn; los componentes de render en componentes de render de mensajes.