Contrat de types
packages/agent/src/types.ts est l'ensemble des contrats de types exposés par pi-agent-core. Il définit StreamFn (entrée de boucle), ToolExecutionMode (scheduling des outils), AgentLoopConfig (assemblage de la boucle), AgentState (état runtime), AgentTool (protocole des outils), AgentContext (snapshot de context), AgentEvent (flux d'événements), et la union de messages extensible AgentMessage. Cette couche ne contient volontairement que des interface/type sans code runtime — AgentSession/Agent/runLoop dépendent ainsi de l'abstrait, pas du concret.
Responsabilités
- Contrat de la fonction de stream :
StreamFnreprend directement la signature destreamSimpleet exige que l'implémentation ne throw pas en cas d'échec de requête ou de modèle : l'échec doit être encodé dans le flux via un événementerroretstopReason: "error". Voirpackages/agent/src/types.ts:15-26. - Mode d'exécution des outils :
ToolExecutionMode = "sequential" | "parallel", présent à la fois dansAgentLoopConfig.toolExecutionet dansAgentTool.executionMode(override par outil). Voirpackages/agent/src/types.ts:28-36. - Configuration de la boucle :
AgentLoopConfig extends SimpleStreamOptions, contientmodel/convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages/toolExecution/beforeToolCall/afterToolCall. Voirpackages/agent/src/types.ts:115-248. - État runtime :
AgentStatedéclaretoolsetmessagessous forme setter/getter, ce qui permet à une implémentation de copier le tableau de sommet ;isStreaming/streamingMessage/pendingToolCalls/errorMessagesont tous readonly. Voirpackages/agent/src/types.ts:288-313. - Protocole des outils :
AgentTool<TParameters, TDetails>étend leToolde pi-ai, en ajoutantlabel/prepareArguments/execute/executionMode;executeprendtoolCallId/params/signal/onUpdate. Voirpackages/agent/src/types.ts:332-355. - Union d'événements :
AgentEventest une discriminated union de 9 événements, organisée autour des quatre couches de cycle de vie agent/turn/message/tool execution. Voirpackages/agent/src/types.ts:374-389.
Motivation de design
Pourquoi AgentMessage est-il Message | CustomAgentMessages[keyof CustomAgentMessages] ? Parce qu'un assistant de code doit pouvoir mélanger dans le transcript des « messages spécifiques à l'UI » (artifact, notification, résumé de pensée) qui ne doivent pas être envoyés au LLM. CustomAgentMessages est une interface vide par défaut ; la couche applicative y ajoute ses rôles custom via declaration merging, et convertToLlm se charge de les filtrer ou de les convertir en user/assistant/toolResult compréhensibles par le LLM. Ce design permet au type du transcript de s'étendre à la compilation plutôt que de dégénérer en any.
Pourquoi le contrat des hooks insiste-t-il tant sur « ne pas throw » ? Parce que runLoop est un flux d'événements mono-thread : si un callback throw, la boucle est interrompue sans produire la séquence normale d'événements agent_end, et l'UI reste bloquée sur isStreaming=true. C'est pourquoi la doc de convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages précise toutes « must not throw or reject, renvoyer une fallback ». Les throws des hooks sont catchés aux points d'appel puis convertis en error result, mais les callbacks de la boucle elle-même ne peuvent pas être rattrapés — c'est la contrainte dure du contrat de types.
Pourquoi BeforeToolCallResult n'a-t-il que block/reason comme champs optionnels, et pas un résultat complet ? Parce que la phase before ne décide que « est-ce qu'on laisse tourner », pas « quel est le résultat ». Un outil bloqué se voit attribuer un error result par la boucle, avec le reason écrit dans le content. Ainsi le hook before n'a pas à connaître la forme exacte du result de l'outil, tandis que le hook after (AfterToolCallResult) est autorisé à surcharger champ par champ content/details/isError/terminate, parce qu'à ce moment-là le result existe déjà.
Pourquoi l'execute de AgentTool utilise-t-il les generics TParameters/TDetails ? TParameters est contraint à TSchema (typebox), et Static<TParameters> donne le type des paramètres déduit à la compilation ; TDetails est la forme de details définie par l'outil lui-même. Ainsi, dans l'implémentation de l'outil, params est fortement typé, alors que la boucle stocke tout sous AgentTool<any> — typage sûr tout en fonctionnant dans une liste hétérogène.
Fichiers clés
packages/agent/src/types.ts:15-26—StreamFn: la signature reprend celle destreamSimple; le contrat exige que l'échec soit encodé dans le flux, pas throw.packages/agent/src/types.ts:28-36— commentaire deToolExecutionMode, qui explique la différence d'ordre d'événements entre sequential et parallel.packages/agent/src/types.ts:47-73—BeforeToolCallResult/AfterToolCallResult: block+reason vs surcharge par champ.packages/agent/src/types.ts:76-113—BeforeToolCallContext/AfterToolCallContext/ShouldStopAfterTurnContext, forme des entrées des hooks.packages/agent/src/types.ts:115-248—AgentLoopConfig, tous les champs avec un contrat JSDoc pour chacun.packages/agent/src/types.ts:257-280—CustomAgentMessages+ unionAgentMessage, point d'extension par declaration merging.packages/agent/src/types.ts:288-313—AgentState: setter/getter pour les champs copiables, état runtime entièrement readonly.packages/agent/src/types.ts:316-355—AgentToolResult/AgentToolUpdateCallback/AgentTool, protocole des outils.packages/agent/src/types.ts:358-365—AgentContext: snapshot d'entrée de boucle, ne contient que systemPrompt/messages/tools.packages/agent/src/types.ts:367-389—AgentEvent: discriminated union de 9 événements.
Champs centraux de AgentLoopConfig : convertToLlm est obligatoire, tous les autres optionnels :
// packages/agent/src/types.ts:115-144
export interface AgentLoopConfig extends SimpleStreamOptions {
model: Model<any>;
/** Converts AgentMessage[] to LLM-compatible Message[] before each LLM call. */
convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
// ...
}AgentMessage s'étend par declaration merging ; par défaut, c'est la union Message de pi-ai :
// packages/agent/src/types.ts:271-280
export interface CustomAgentMessages {
// Empty by default - apps extend via declaration merging
}
export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];AgentTool étend le Tool de pi-ai et ajoute le protocole d'exécution :
// packages/agent/src/types.ts:332-346
export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> extends Tool<TParameters> {
label: string;
prepareArguments?: (args: unknown) => Static<TParameters>;
execute: (
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>,
) => Promise<AgentToolResult<TDetails>>;
// ...
}Flux de données
Le cheminement des types entre les couches :
Limites et échecs
- Contrat d'échec de StreamFn : un échec de requête ou de modèle doit être encodé dans le flux, pas throw, sinon le
for awaitderunLoops'interrompt sansagent_end. Voirpackages/agent/src/types.ts:16-23. - Contrat d'échec de convertToLlm : ne doit pas throw, doit renvoyer un
Message[]de fallback ; les commentaires précisent que throw interrompt la boucle sans séquence d'événements. Voirpackages/agent/src/types.ts:119-127. - Copie par setter dans AgentState :
tools/messagessont setter/getter, l'implémentation peut copier le tableau de sommet (c'est ce que faitcreateMutableAgentStatepar défaut), pour éviter qu'un externe ne mute directement. Voirpackages/agent/src/types.ts:295-300. - afterToolCall sans deep merge : la surcharge de
content/detailsest « remplacement total », pas de merge profond ;isError/terminatesont remplacés séparément. La doc le dit explicitement : « No deep merge is performed ». Voirpackages/agent/src/types.ts:52-73. - AgentContext est un snapshot :
Agentappellemessages.slice()/tools.slice()danscreateContextSnapshotavant de le passer à la boucle ; les mutations faites par la boucle (push partial, push toolResult) n'affectent pasAgent._state. Voirpackages/agent/src/agent.ts:402-408.
Résumé
types.ts est la couche de contrat de pi-agent-core : StreamFn fige la forme de la fonction de stream, AgentLoopConfig assemble la boucle, AgentState expose l'état runtime en lecture seule, AgentTool définit le protocole des outils, AgentMessage s'étend par declaration merging, AgentEvent est l'union de 9 événements. Le contrat insiste lourdement sur « ne pas throw » pour protéger l'intégrité de la séquence d'événements de runLoop. Pour la façon dont ces types sont consommés, voir Classe Agent et cycle de vie et Boucle while double ; pour l'appel concret des hooks dans l'exécution des outils, voir Exécution des outils sequential/parallel.