Exécution des outils sequential/parallel
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) → emitToolExecutionEnd → createToolResultMessage. 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
- Dispatch de mode :
executeToolCallsregardeconfig.toolExecutionet si un seul outil aexecutionMode === "sequential", on bascule en sequential. Voirpackages/agent/src/agent-loop.ts:350-365. - Phase de préparation :
prepareToolCallretrouve l'outil,prepareToolCallArgumentsassure la compat avec les anciens paramètres,validateToolArgumentsvalide, et le hookbeforeToolCallpeut bloquer. Voirpackages/agent/src/agent-loop.ts:529-579. - Phase d'exécution :
executePreparedToolCallappelletool.execute(id, args, signal, onUpdate); les partial results sont transmis à l'UI via l'événementtool_execution_update; les exceptions sont converties en résultat error. Voirpackages/agent/src/agent-loop.ts:581-616. - Phase de finalisation :
finalizeExecutedToolCallappelle le hookafterToolCallet surcharge champ par champcontent/details/isError/terminate; si le hook lui-même throw, on convertit aussi en error result. Voirpackages/agent/src/agent-loop.ts:618-661. - Construction du message résultat :
createToolResultMessageemballe le finalized enToolResultMessage(role/toolCallId/content/details/isError/timestamp), que l'appelant pousse dans le context. Voirpackages/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
packages/agent/src/agent-loop.ts:350-365— dispatch de mode dansexecuteToolCalls: si un seul outil déclare sequential, toute la batch passe en sequential.packages/agent/src/agent-loop.ts:372-422—executeToolCallsSequential:for (const toolCall of toolCalls)parcourt les cinq étapes pour chaque appel.packages/agent/src/agent-loop.ts:424-483—executeToolCallsParallel: boucle d'abord prepare / déroute immediate, pousse les à-exécuter en closures() => Promise, puisPromise.allpour la parallélisation.packages/agent/src/agent-loop.ts:511-513—shouldTerminateToolBatch: la batch ne s'arrête que si tous les finalized ontterminate: true; si un seul ne l'a pas, on continue.packages/agent/src/agent-loop.ts:515-527—prepareToolCallArguments: utilise leprepareArgumentsde l'outil pour la compat de params, renvoie l'objet tel quel si non requis.packages/agent/src/agent-loop.ts:529-579—prepareToolCall: recherche dans la table d'outils, validation, hookbeforeToolCall, renvoiePreparedToolCallouImmediateToolCallOutcome.packages/agent/src/agent-loop.ts:581-616—executePreparedToolCall: appelletool.execute, partial result viatool_execution_update, exceptions converties en error.packages/agent/src/agent-loop.ts:618-661—finalizeExecutedToolCall: appelleafterToolCall, surcharge au niveau des champs, les throws du hook aussi convertis en error.packages/agent/src/agent-loop.ts:680-690—createToolResultMessage: emballe le finalized enToolResultMessage.
Le cœur du mode sequential : un outil parcourt les cinq étapes avant le suivant :
// 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é :
// 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
- Outil introuvable : si
prepareToolCallne trouve pastoolCall.namedans la table d'outils, il renvoie unimmediateerror result, sans throw ni interruption de la batch. Voirpackages/agent/src/agent-loop.ts:536-543. - Échec de validation des paramètres : un throw de
validateToolArgumentsest catché et converti enimmediateerror result ; letool_execution_endde cet outil est quand même émis. Voirpackages/agent/src/agent-loop.ts:572-578. - beforeToolCall bloque : si
beforeToolCallrenvoie{ block: true, reason }, l'outil devient un error result, le reason est écrit dans le content du résultat. Voirpackages/agent/src/agent-loop.ts:558-565. - execute throw :
executePreparedToolCallcatch l'exception et renvoie un result avecisError: true; les événements de partial result sont d'abord attendus viaPromise.allavant de retourner. Voirpackages/agent/src/agent-loop.ts:608-615. - afterToolCall throw :
finalizeExecutedToolCallcatch l'exception du hook et la convertit en error result, sans impacter le finalize des autres outils. Voirpackages/agent/src/agent-loop.ts:650-654. - Arrêt de toute la batch :
shouldTerminateToolBatchne renvoie true que si toute la batch aterminate: true;runLoopen déduithasMoreToolCalls=falseet sort de la boucle interne. Voirpackages/agent/src/agent-loop.ts:511-513etpackages/agent/src/agent-loop.ts:206-214.
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.