Skip to content

AgentInterface: Session-Host und Event-Verteilung

源码版本v0.73.1

AgentInterface ist ein Lit-customElement in pi-web-ui, das zwischen dem Agent aus pi-agent-core und den konkreten Nachrichten-Render-Komponenten vermittelt. Es hält eine Agent-Instanz als session-Property, kümmert sich um die Verdrahtung von streamFn und getApiKey, abonniert die Agent-Ereignisse, wandelt Text aus dem MessageEditor in prompt()-Aufrufe um und verteilt die zurückkommenden Ereignisse an MessageList und StreamingMessageContainer. Die Klasse selbst spricht nicht mit dem LLM und speichert keine Historie — sie ist nur ein zustandsbehafteter Event-Router.

Zuständigkeiten

  1. Session halten: @property session zeigt auf eine Agent-Instanz; bei Session-Wechsel werden die Events neu abonniert. Siehe packages/web-ui/src/components/AgentInterface.ts:20-47.
  2. streamFn und getApiKey verdrahten: In connectedCallback wird session.streamFn, falls noch der Default streamSimple, durch createStreamFn mit Proxy-Unterstützung ersetzt; gleichzeitig wird ein Default-getApiKey injiziert, das den Schlüssel aus AppStorage liest. Siehe packages/web-ui/src/components/AgentInterface.ts:130-152.
  3. Events abonnieren: session.subscribe registriert Callbacks, die message_start/message_update/message_end/agent_end auf StreamingMessageContainer.setMessage und requestUpdate mappen. Siehe packages/web-ui/src/components/AgentInterface.ts:153-187.
  4. Nachrichten senden: sendMessage(input, attachments) prüft model und API key; fehlt der key, wird onApiKeyRequired aufgerufen, danach session.prompt(input) bzw. prompt(message) mit Anhängen. Siehe packages/web-ui/src/components/AgentInterface.ts:215-262.
  5. Auto-Scroll: Ein ResizeObserver beobachtet die Höhe des Inhalts und kombiniert mit dem scroll-Ereignis, ob der Nutzer manuell nach oben gescrollt hat; darüber wird entschieden, ob automatisch unten angeheftet wird. Siehe packages/web-ui/src/components/AgentInterface.ts:89-105.

Designmotivation

Warum nicht direkt MessageEditor Agent.prompt aufrufen lassen? Weil auf der Browser-Seite noch ein paar Dinge fehlen: Die Proxy-URL muss dynamisch nach provider/key gewählt werden, der API key muss asynchron aus IndexedDB geholt werden, bei fehlendem key muss ein Dialog aufpoppen, und während des Streamings müssen Streaming-Container und stabile Liste entdoppelt werden. Wenn man diese Logik in jede Host-App steckt, wiederholt sie sich; packt man sie in die Nachrichten-Komponente, koppelt die Rendering-Schicht an die Storage-Schicht. AgentInterface wird als einzige Einstiegspunkt herausgezogen: ChatPanel macht nur Layout, MessageEditor nur Eingabe, MessageList nur Rendering — alle drei sprechen über die Klasse mit Agent.

Ein weiteres Motiv: session ist austauschbar. Beim Wechsel der Konversation wird nicht die ganze Komponente neu aufgebaut, sondern nur das session-Property getauscht; willUpdate erkennt die Änderung und ruft setupSessionSubscription() für ein neues Abonnement auf. Siehe packages/web-ui/src/components/AgentInterface.ts:68-75.

Wichtige Dateien

setupSessionSubscription ist die zentrale Verdrahtungsstelle; hier werden streamFn und getApiKey auf die Standard-Implementierung ersetzt:

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 verkettet key-Prüfung und onApiKeyRequired-Callback; fehlt der key, entscheidet der Host, wie der Dialog aufgeht:

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;
    }
}

Datenfluss

Der Nutzer drückt im MessageEditor Enter; nach key-Prüfung und Proxy-Verdrahtung wird der Agent-Loop ausgelöst und läuft zurück ins Rendering:

Randbedingungen und Fehler

  • Session nicht gesetzt: render zeigt direkt einen „No session set"-Platzhalter, sendMessage wirft No session set on AgentInterface. Siehe packages/web-ui/src/components/AgentInterface.ts:216-219.
  • model nicht gesetzt: Ebenfalls in sendMessage wirft es No model set, als Hinweis, dass der Host das Modell nicht initialisiert hat.
  • Senden während des Streamings: Wenn isStreaming true ist, kehrt sendMessage direkt zurück — keine Queue, kein Hineinfunken. Siehe packages/web-ui/src/components/AgentInterface.ts:216.
  • Doppeltes Abonnieren: setupSessionSubscription macht zuerst _unsubscribeSession() für das alte Abonnement und hängt dann das neue an; ein Session-Wecksel leakt keine Listener.
  • Nachrichten-Entdopplung: Bei message_end wird StreamingMessageContainer.setMessage(null, true) geleert, damit der Streaming-Container die Nachricht nicht doppelt rendert, wenn die stabile Liste sie schon enthält. Siehe packages/web-ui/src/components/AgentInterface.ts:161-168.

Zusammenfassung

AgentInterface ist der „Session-Host" auf der Browser-Seite: verdrahtet streamFn und getApiKey, abonniert Events und verteilt sie an die Render-Komponenten. Nach oben wird es von ChatPanel in das Layout eingebaut, nach unten treibt es den Agent aus pi-agent-core an. Die Proxy-Entscheidungen im Detail stehen in CORS-Proxy und createStreamFn, die Render-Komponenten in Nachrichten-Render-Komponenten.