Skip to content

Classe Agent et cycle de vie

源码版本v0.73.1

Agent est l'entrée avec état (stateful) de @mariozechner/pi-agent-core. En dessous se trouve la boucle nue runAgentLoop, au-dessus l'orchestration AgentSession. Agent occupe la couche intermédiaire : il détient le transcript et la table d'outils, gère le cycle de vie de activeRun, maintient les deux files d'attente steering/followUp, traduit AgentOptions en AgentLoopConfig, enveloppe l'appel réel à la boucle dans runWithLifecycle, et diffuse les événements aux listeners externes via subscribe. On peut le voir comme une « coquille d'état au-dessus de la boucle » — la boucle elle-même ne stocke aucun état.

Responsabilités

  1. Détention de l'état : AgentState contient messages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage, initialisé à la construction par createMutableAgentState. Voir packages/agent/src/agent.ts:158-188.
  2. Gestion des files : steeringQueue et followUpQueue sont chacune un PendingMessageQueue, dont le mode est "one-at-a-time" ou "all". Voir packages/agent/src/agent.ts:113-144 et packages/agent/src/agent.ts:200-280.
  3. Hooks de cycle de vie : beforeToolCall/afterToolCall sont capturés à la construction, puis transmis à la boucle inférieure via createLoopConfig. Voir packages/agent/src/agent.ts:410-436.
  4. Enveloppe d'exécution : runWithLifecycle crée un AbortController, positionne isStreaming, catche les exceptions via handleRunFailure, et appelle finishRun dans le bloc finally. Voir packages/agent/src/agent.ts:438-486.
  5. Triplet d'entrée : prompt reçoit un nouvel input, continue reprend depuis la fin, steer/followUp mettent en file. Voir packages/agent/src/agent.ts:312-353 et packages/agent/src/agent.ts:252-259.

Motivation de design

Pourquoi ne pas laisser la couche supérieure appeler directement runAgentLoop ? Parce que la boucle est purement fonctionnelle — on lui passe un context, elle tourne avec, et renvoie newMessages à la fin sans rien stocker. Un vrai assistant de code a besoin d'un « transcript persistant entre les tours, de mise en file pendant le streaming, d'une source unique pour l'abort, et d'un errorMessage écrit dans le state en cas d'échec ». Mettre tout ça dans runLoop alourdirait la boucle et la rendrait non réutilisable ; les remonter à la couche supérieure dupliquerait la logique entre les modes print/rpc/TUI. Agent s'extrait donc comme coquille d'état unique : la boucle ne fait que tourner, l'état ne fait que stocker.

Les files sont au nombre de deux plutôt qu'une, parce que steering (intercalaire dans le tour courant, à injecter avant la prochaine réponse assistant) et followUp (enclencher un nouveau tour quand le précédent se termine) ont des sémantiques différentes — steering est tiré par getSteeringMessages dans la boucle while interne, followUp par getFollowUpMessages à la fin de la boucle while externe. Voir Boucle while double. Les modes "all"/"one-at-a-time" de PendingMessageQueue laissent l'appelant décider s'il vide tout d'un coup ou ne prend qu'un message.

Fichiers clés

Le constructeur installe toutes les pièces remplaçables, streamFn pointant par défaut sur streamSimple :

typescript
// packages/agent/src/agent.ts:190-207
constructor(options: AgentOptions = {}) {
  this._state = createMutableAgentState(options.initialState);
  this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
  this.transformContext = options.transformContext;
  this.streamFn = options.streamFn ?? streamSimple;
  // ... getApiKey / onPayload / onResponse / beforeToolCall / afterToolCall ...
  this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
  this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
  this.transport = options.transport ?? "auto";
  this.toolExecution = options.toolExecution ?? "parallel";
}

createLoopConfig ferme le drain des files en fonctions asynchrones, que la boucle tire via ces deux callbacks :

typescript
// packages/agent/src/agent.ts:427-434
getSteeringMessages: async () => {
  if (skipInitialSteeringPoll) {
    skipInitialSteeringPoll = false;
    return [];
  }
  return this.steeringQueue.drain();
},
getFollowUpMessages: async () => this.followUpQueue.drain(),

runWithLifecycle est la coquille commune à tous les points d'entrée d'exécution ; en cas d'erreur, handleRunFailure l'écrit dans le state puis émet agent_end :

typescript
// packages/agent/src/agent.ts:454-461
try {
  await executor(abortController.signal);
} catch (error) {
  await this.handleRunFailure(error, abortController.signal.aborted);
} finally {
  this.finishRun();
}

Flux de données

Cycle de vie de prompt(text) :

Limites et échecs

  • Refus de prompt concurrent : prompt/continue throw directement si activeRun existe déjà, sans mise en file silencieuse. L'appelant doit passer par steer/followUp. Voir packages/agent/src/agent.ts:316-320.
  • Validation du rôle sur continue : si le dernier message est un assistant, on consomme en priorité les files steering/followUp ; quand les deux sont vides, on throw "Cannot continue from message role: assistant". Voir packages/agent/src/agent.ts:336-350.
  • Pas de perte de message en cas d'échec : handleRunFailure emballe l'erreur en un AgentMessage assistant avec stopReason: "aborted"|"error", ajouté au transcript pour que l'UI le rende directement. Voir packages/agent/src/agent.ts:463-478.
  • reset nettoyé : reset vide messages, état de streaming, pendingToolCalls, errorMessage, ainsi que les deux files. Voir packages/agent/src/agent.ts:301-310.
  • Source unique d'abort : abort() ne fait qu'appeler activeRun.abortController.abort() ; la boucle et le stream écoutent le même signal, pas de coordination multi-chemins. Voir packages/agent/src/agent.ts:287-290.

Résumé

Agent enveloppe runAgentLoop en un objet avec état : il détient le transcript, gère deux files, monte les hooks de cycle de vie, unifie le signal d'abort. Au-dessus se trouve la couche d'orchestration AgentSession, en dessous la Boucle while double. La façon dont les hooks de cycle de vie sont consommés par le chemin d'exécution des outils se voit dans Exécution des outils sequential/parallel ; les champs d'état et la forme de AgentMessage/AgentEvent dans Contrat de types.