Classe Agent et cycle de vie
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
- Détention de l'état :
AgentStatecontientmessages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage, initialisé à la construction parcreateMutableAgentState. Voirpackages/agent/src/agent.ts:158-188. - Gestion des files :
steeringQueueetfollowUpQueuesont chacune unPendingMessageQueue, dont le mode est"one-at-a-time"ou"all". Voirpackages/agent/src/agent.ts:113-144etpackages/agent/src/agent.ts:200-280. - Hooks de cycle de vie :
beforeToolCall/afterToolCallsont capturés à la construction, puis transmis à la boucle inférieure viacreateLoopConfig. Voirpackages/agent/src/agent.ts:410-436. - Enveloppe d'exécution :
runWithLifecyclecrée unAbortController, positionneisStreaming, catche les exceptions viahandleRunFailure, et appellefinishRundans le blocfinally. Voirpackages/agent/src/agent.ts:438-486. - Triplet d'entrée :
promptreçoit un nouvel input,continuereprend depuis la fin,steer/followUpmettent en file. Voirpackages/agent/src/agent.ts:312-353etpackages/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
packages/agent/src/agent.ts:113-144—PendingMessageQueue:enqueue/drain/clear;drainprend tout en mode"all", sinon seulement le premier.packages/agent/src/agent.ts:158-207— champs et constructeur declass Agent;streamFnpointe par défaut surstreamSimple,toolExecutionpar défaut"parallel".packages/agent/src/agent.ts:312-323— surcharges deprompt: accepteAgentMessage,AgentMessage[], oustring + images; passe en interne parnormalizePromptInput→runPromptMessages.packages/agent/src/agent.ts:355-372—normalizePromptInput: convertit string + images en unAgentMessageutilisateur avectimestamp.packages/agent/src/agent.ts:374-400—runPromptMessages/runContinuationappellent respectivementrunAgentLoop/runAgentLoopContinue, tous deux enveloppés dansrunWithLifecycle.packages/agent/src/agent.ts:410-436—createLoopConfig: assemble les champs d'instance enAgentLoopConfig;getSteeringMessages/getFollowUpMessagesferment sur ledraindes files.packages/agent/src/agent.ts:438-486—runWithLifecycle+handleRunFailure+finishRun.
Le constructeur installe toutes les pièces remplaçables, streamFn pointant par défaut sur streamSimple :
// 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 :
// 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 :
// 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/continuethrow directement siactiveRunexiste déjà, sans mise en file silencieuse. L'appelant doit passer parsteer/followUp. Voirpackages/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". Voirpackages/agent/src/agent.ts:336-350. - Pas de perte de message en cas d'échec :
handleRunFailureemballe l'erreur en unAgentMessageassistant avecstopReason: "aborted"|"error", ajouté au transcript pour que l'UI le rende directement. Voirpackages/agent/src/agent.ts:463-478. - reset nettoyé :
resetvide messages, état de streaming, pendingToolCalls, errorMessage, ainsi que les deux files. Voirpackages/agent/src/agent.ts:301-310. - Source unique d'abort :
abort()ne fait qu'appeleractiveRun.abortController.abort(); la boucle et le stream écoutent le même signal, pas de coordination multi-chemins. Voirpackages/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.