Skip to content

Contrat de types

源码版本v0.73.1

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

  1. Contrat de la fonction de stream : StreamFn reprend directement la signature de streamSimple et 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énement error et stopReason: "error". Voir packages/agent/src/types.ts:15-26.
  2. Mode d'exécution des outils : ToolExecutionMode = "sequential" | "parallel", présent à la fois dans AgentLoopConfig.toolExecution et dans AgentTool.executionMode (override par outil). Voir packages/agent/src/types.ts:28-36.
  3. Configuration de la boucle : AgentLoopConfig extends SimpleStreamOptions, contient model/convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages/toolExecution/beforeToolCall/afterToolCall. Voir packages/agent/src/types.ts:115-248.
  4. État runtime : AgentState déclare tools et messages sous forme setter/getter, ce qui permet à une implémentation de copier le tableau de sommet ; isStreaming/streamingMessage/pendingToolCalls/errorMessage sont tous readonly. Voir packages/agent/src/types.ts:288-313.
  5. Protocole des outils : AgentTool<TParameters, TDetails> étend le Tool de pi-ai, en ajoutant label/prepareArguments/execute/executionMode ; execute prend toolCallId/params/signal/onUpdate. Voir packages/agent/src/types.ts:332-355.
  6. Union d'événements : AgentEvent est une discriminated union de 9 événements, organisée autour des quatre couches de cycle de vie agent/turn/message/tool execution. Voir packages/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

Champs centraux de AgentLoopConfig : convertToLlm est obligatoire, tous les autres optionnels :

typescript
// 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 :

typescript
// 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 :

typescript
// 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 await de runLoop s'interrompt sans agent_end. Voir packages/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. Voir packages/agent/src/types.ts:119-127.
  • Copie par setter dans AgentState : tools/messages sont setter/getter, l'implémentation peut copier le tableau de sommet (c'est ce que fait createMutableAgentState par défaut), pour éviter qu'un externe ne mute directement. Voir packages/agent/src/types.ts:295-300.
  • afterToolCall sans deep merge : la surcharge de content/details est « remplacement total », pas de merge profond ; isError/terminate sont remplacés séparément. La doc le dit explicitement : « No deep merge is performed ». Voir packages/agent/src/types.ts:52-73.
  • AgentContext est un snapshot : Agent appelle messages.slice()/tools.slice() dans createContextSnapshot avant de le passer à la boucle ; les mutations faites par la boucle (push partial, push toolResult) n'affectent pas Agent._state. Voir packages/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.