Skip to content

Exécution des outils sequential/parallel

源码版本v0.73.1

executeToolCalls est le sous-flux de la boucle interne runLoop qui traite les tool calls d'un message assistant. Il découpe le cycle de vie d'un appel d'outil en cinq étapes : prepareToolCall (recherche + validation + hook beforeToolCall) → executePreparedToolCall (exécute vraiment tool.execute) → finalizeExecutedToolCall (surcharge via hook afterToolCall) → emitToolExecutionEndcreateToolResultMessage. Les modes sequential et parallel partagent ces cinq étapes ; seule la scheduler diffère : sequential termine un appel avant de passer au suivant, parallel fait un prepare en série puis lance Promise.all pour exécuter et finaliser en parallèle.

Responsabilités

  1. Dispatch de mode : executeToolCalls regarde config.toolExecution et si un seul outil a executionMode === "sequential", on bascule en sequential. Voir packages/agent/src/agent-loop.ts:350-365.
  2. Phase de préparation : prepareToolCall retrouve l'outil, prepareToolCallArguments assure la compat avec les anciens paramètres, validateToolArguments valide, et le hook beforeToolCall peut bloquer. Voir packages/agent/src/agent-loop.ts:529-579.
  3. Phase d'exécution : executePreparedToolCall appelle tool.execute(id, args, signal, onUpdate) ; les partial results sont transmis à l'UI via l'événement tool_execution_update ; les exceptions sont converties en résultat error. Voir packages/agent/src/agent-loop.ts:581-616.
  4. Phase de finalisation : finalizeExecutedToolCall appelle le hook afterToolCall et surcharge champ par champ content/details/isError/terminate ; si le hook lui-même throw, on convertit aussi en error result. Voir packages/agent/src/agent-loop.ts:618-661.
  5. Construction du message résultat : createToolResultMessage emballe le finalized en ToolResultMessage (role/toolCallId/content/details/isError/timestamp), que l'appelant pousse dans le context. Voir packages/agent/src/agent-loop.ts:680-690.

Motivation de design

Pourquoi prepare en série et execute en parallèle ? Parce que le hook beforeToolCall fait souvent des choses à effet de bord (auth, logs, rate limiting) qui, lancées en parallèle, créent des races (par ex. deux refreshs de token OAuth en même temps). Un prepare en série garantit l'ordre d'exécution des hooks. L'execute en parallèle se justifie parce que les outils eux-mêmes (lecture de fichier, bash, requête HTTP) sont indépendants, les sérialiser gaspillerait du wall time.

Pourquoi tool_execution_end est-il émis « par ordre de complétion » mais les toolResult messages « par ordre source » ? En mode parallel, après le Promise.all, on boucle pour construire les toolResultMessage, mais emitToolExecutionEnd est appelé dès que chaque outil est finalisé — l'UI récupère le résultat au moment où il est prêt, sans attendre toute la batch. Les toolResult messages, eux, doivent être renvoyés au LLM, et un ordre aléatoire perdrait le modèle, donc ils suivent l'ordre des toolCalls dans le message assistant. Cette dissociation « flux d'événements par ordre de complétion, flux de messages par ordre source » rend l'UI réactive et le context LLM stable.

Pourquoi deux outcomes, immediate et prepared ? Quand l'outil n'existe pas, que la validation des paramètres échoue, ou que beforeToolCall bloque, on n'arrive jamais à l'execute : on renvoie directement un error result. En l'enveloppant dans le même FinalizedToolCallOutcome que le résultat d'un execute normal, les emitToolExecutionEnd/createToolResultMessage en aval n'ont pas à distinguer les deux chemins.

Fichiers clés

Le cœur du mode sequential : un outil parcourt les cinq étapes avant le suivant :

typescript
// packages/agent/src/agent-loop.ts:383-416
for (const toolCall of toolCalls) {
  await emit({ type: "tool_execution_start", toolCallId: toolCall.id, toolName: toolCall.name, args: toolCall.arguments });
  const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
  let finalized: FinalizedToolCallOutcome;
  if (preparation.kind === "immediate") {
    finalized = { toolCall, result: preparation.result, isError: preparation.isError };
  } else {
    const executed = await executePreparedToolCall(preparation, signal, emit);
    finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
  }
  await emitToolExecutionEnd(finalized, emit);
  const toolResultMessage = createToolResultMessage(finalized);
  await emitToolResultMessage(toolResultMessage, emit);
  finalizedCalls.push(finalized);
  messages.push(toolResultMessage);
}

En mode parallel, prepare est en série et execute en parallèle — l'ordre des événements est la différence clé :

typescript
// packages/agent/src/agent-loop.ts:434-477
for (const toolCall of toolCalls) {
  await emit({ type: "tool_execution_start", ... });
  const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
  if (preparation.kind === "immediate") {
    const finalized = { toolCall, result: preparation.result, isError: preparation.isError } satisfies FinalizedToolCallOutcome;
    await emitToolExecutionEnd(finalized, emit);   // immediate emits end right away
    finalizedCalls.push(finalized);
    continue;
  }
  finalizedCalls.push(async () => {                 // delayed until Promise.all
    const executed = await executePreparedToolCall(preparation, signal, emit);
    const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
    await emitToolExecutionEnd(finalized, emit);    // emits end as soon as finalized
    return finalized;
  });
}
const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => typeof entry === "function" ? entry() : Promise.resolve(entry)));
// then build toolResultMessage in source order ...

Flux de données

Un message assistant avec 3 tool calls, en mode parallel :

Limites et échecs

Résumé

L'exécution des outils est découpée en cinq étapes : prepare/execute/finalize/emitEnd/createToolResultMessage. Sequential fait toute la batch en série, parallel prépare en série puis exécute en parallèle ; les événements suivent l'ordre de complétion, les toolResult suivent l'ordre source. Les hooks beforeToolCall/afterToolCall sont appelés pendant les phases prepare et finalize ; toute exception est convertie en error result plutôt que d'interrompre la batch. Pour la forme des hooks et les champs de AgentTool, voir Contrat de types ; pour l'endroit où les cinq fonctions sont appelées par la boucle interne de runLoop, voir Boucle while double.