Skip to content

AgentInterface : hôte de session et dispatch d'événements

源码版本v0.73.1

AgentInterface est une couche Lit customElement dans pi-web-ui, insérée entre l'Agent de pi-agent-core et les composants de rendu de messages concrets. Elle détient une instance d'Agent via la propriété session, se charge d'installer streamFn et getApiKey, s'abonne aux événements de l'Agent, transforme le texte saisi dans MessageEditor en appel prompt(), puis redispatche les événements qui reviennent vers MessageList et StreamingMessageContainer. Elle ne touche pas au LLM et ne stocke pas d'historique : c'est un routeur d'événements avec état.

Responsabilités

  1. Détient la session : @property session pointe vers une instance d'Agent ; à chaque changement de session, on se réabonne aux événements, voir packages/web-ui/src/components/AgentInterface.ts:20-47.
  2. Installe streamFn et getApiKey : dans connectedCallback, si session.streamFn vaut encore le défaut streamSimple, on le remplace par createStreamFn avec support proxy, et on injecte un getApiKey par défaut qui lit la clé depuis AppStorage, voir packages/web-ui/src/components/AgentInterface.ts:130-152.
  3. Abonnement aux événements : session.subscribe enregistre des callbacks et mappe message_start/message_update/message_end/agent_end vers StreamingMessageContainer.setMessage et requestUpdate, voir packages/web-ui/src/components/AgentInterface.ts:153-187.
  4. Envoi de message : sendMessage(input, attachments) valide le modèle et la clé API ; si la clé manque, on appelle onApiKeyRequired, puis on invoque session.prompt(input) ou prompt(message) avec pièces jointes, voir packages/web-ui/src/components/AgentInterface.ts:215-262.
  5. Auto-scroll : un ResizeObserver observe la hauteur du contenu, couplé à l'événement scroll pour détecter si l'utilisateur remonte volontairement, et décider si on colle en bas, voir packages/web-ui/src/components/AgentInterface.ts:89-105.

Motivations de design

Pourquoi ne pas laisser MessageEditor appeler directement Agent.prompt ? Parce que côté navigateur il manque encore quelques bricoles : l'URL du proxy doit être décidée dynamiquement selon provider/clé, la clé API doit être lue de façon asynchrone depuis IndexedDB, une boîte de dialogue doit s'ouvrir en cas de clé manquante, et pendant le streaming il faut gérer la déduplication entre le conteneur de streaming et la liste stable. Si on fourrait cette logique dans chaque application hôte, elle serait dupliquée ; si on la mettait dans les composants de message, le rendu serait couplé à la couche de stockage. AgentInterface est extraite comme point d'entrée unique : ChatPanel ne gère que la mise en page, MessageEditor que la saisie, MessageList que le rendu — les trois branches se branchent à l'Agent via elle.

L'autre motivation est que session est remplaçable : quand l'utilisateur change de session, on ne reconstruit pas tout le composant, on ne change que la propriété session ; willUpdate détecte le changement et relance setupSessionSubscription() pour se réabonner, voir packages/web-ui/src/components/AgentInterface.ts:68-75.

Fichiers clés

setupSessionSubscription est le point d'assemblage central : streamFn et getApiKey y sont remplacés par leurs implémentations par défaut :

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 chaîne la validation de clé et le callback onApiKeyRequired : si la clé manque, c'est l'hôte qui décide comment réagir :

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

Flux de données

L'utilisateur appuie sur Entrée dans MessageEditor ; après validation de clé et assemblage du proxy, on déclenche la boucle Agent et on reçoit le flux pour le rendu :

Limites et cas d'échec

  • Session non définie : render affiche un placeholder « No session set » ; sendMessage lève No session set on AgentInterface, voir packages/web-ui/src/components/AgentInterface.ts:216-219.
  • Modèle non défini : sendMessage lève No model set pour signaler que l'hôte n'a pas initialisé le modèle.
  • Envoi pendant un streaming : quand isStreaming est true, sendMessage retourne immédiatement, sans file d'attente ni interruption, voir packages/web-ui/src/components/AgentInterface.ts:216.
  • Abonnement dupliqué : setupSessionSubscription appelle d'abord _unsubscribeSession() sur l'ancien abonnement avant de brancher le nouveau ; un changement de session ne fuit pas de listeners.
  • Déduplication des messages : sur message_end, StreamingMessageContainer.setMessage(null, true) vide le conteneur, pour éviter que la liste stable (qui contient déjà ce message) ne soit re-rendue par le conteneur de streaming, voir packages/web-ui/src/components/AgentInterface.ts:161-168.

Pour résumer

AgentInterface est l'« hôte de session » côté navigateur : elle installe streamFn et getApiKey, s'abonne aux événements, et les dispatche vers les composants de rendu. En amont, ChatPanel l'insère dans la mise en page ; en aval, elle pilote l'Agent de pi-agent-core. Pour les détails du décision proxy, voir Proxy CORS et createStreamFn ; pour les composants de rendu, voir Composants de rendu des messages.