Ejecución de herramientas sequential/parallel
executeToolCalls es el subflujo del bucle interno de runLoop que procesa las tool calls del mensaje del asistente. Divide el ciclo de vida de cada invocación en cinco tramos: prepareToolCall (consulta tabla + validación + hook beforeToolCall) → executePreparedToolCall (ejecuta tool.execute) → finalizeExecutedToolCall (hook afterToolCall para sobrescribir) → emitToolExecutionEnd → createToolResultMessage. Los modos sequential y parallel comparten los cinco tramos; sólo difieren en el orden de scheduling: sequential termina una herramienta antes de empezar la siguiente; parallel prepara en serie y luego Promise.all para ejecución y finalize en concurrente.
Responsabilidades
- Dispatch por modo:
executeToolCallscompruebaconfig.toolExecutiony si alguna herramienta tieneexecutionMode === "sequential"; si cualquiera es true, va por sequential. Verpackages/agent/src/agent-loop.ts:350-365. - Fase de preparación:
prepareToolCallbusca la herramienta,prepareToolCallArgumentshace compatibilidad con argumentos antiguos,validateToolArgumentsvalida, el hookbeforeToolCallpuede bloquear. Verpackages/agent/src/agent-loop.ts:529-579. - Fase de ejecución:
executePreparedToolCallinvocatool.execute(id, args, signal, onUpdate); los resultados parciales fluyen como eventostool_execution_updatea la UI; las excepciones se convierten en resultados de error. Verpackages/agent/src/agent-loop.ts:581-616. - Fase de finalización:
finalizeExecutedToolCallllama al hookafterToolCall, que sobrescribe por campocontent/details/isError/terminate; si el hook lanza, también se convierte en resultado de error. Verpackages/agent/src/agent-loop.ts:618-661. - Construcción del mensaje de resultado:
createToolResultMessageenvuelve el resultado finalized en unToolResultMessage(role/toolCallId/content/details/isError/timestamp) que el llamador empuja al context. Verpackages/agent/src/agent-loop.ts:680-690.
Motivación de diseño
¿Por qué prepare en serie y execute en paralelo? Porque el hook beforeToolCall suele hacer auth, logs o rate limiting, tareas con efectos secundarios; dispararlos concurrentemente puede causar carreras (por ejemplo, refrescar el OAuth token a la vez). El prepare en serie garantiza que los hooks corren en orden. Execute en paralelo porque las herramientas mismas (leer archivo, bash, HTTP) son independientes entre sí; en serie se pierde wall time.
¿Por qué tool_execution_end se emite "en orden de finalización" y el mensaje toolResult se emite "en orden de origen"? En modo parallel, tras Promise.all se itera para construir toolResultMessage, pero emitToolExecutionEnd se invoca en cuanto cada herramienta termina su finalize: la UI puede recibir el resultado de la herramienta en el instante en que termina, sin esperar al lote. El mensaje toolResult alimenta al LLM; un orden errático lo confundiría, así que se emite en el orden de los toolCalls del mensaje assistant. Esta separación "flujo de eventos por orden de finalización, flujo de mensajes por orden de origen" deja la UI responsiva y el contexto del LLM estable.
¿Por qué hay dos outcomes, immediate y prepared? Cuando la herramienta no se encuentra, falla la validación, o beforeToolCall bloquea, no se llega a execute; se devuelve directamente un resultado de error. Envolviéndolo en la misma forma FinalizedToolCallOutcome que el resultado de un execute normal, los tramos posteriores emitToolExecutionEnd/createToolResultMessage no tienen que distinguir las dos rutas.
Archivos clave
packages/agent/src/agent-loop.ts:350-365— Dispatch deexecuteToolCalls; si una herramienta declara sequential, el lote entero va por sequential.packages/agent/src/agent-loop.ts:372-422—executeToolCallsSequential:for (const toolCall of toolCalls)recorre los cinco tramos por cada una.packages/agent/src/agent-loop.ts:424-483—executeToolCallsParallel: primero un bucle de prepare/immediate, metiendo lo ejecutable en closures() => Promise, luegoPromise.allconcurrente.packages/agent/src/agent-loop.ts:511-513—shouldTerminateToolBatch: el lote termina sólo si todos los finalize marcanterminate: true; si alguno no termina, continúa.packages/agent/src/agent-loop.ts:515-527—prepareToolCallArguments: usa elprepareArgumentsde la herramienta para compatibilidad; si devuelve el mismo objeto, no copia.packages/agent/src/agent-loop.ts:529-579—prepareToolCall: consulta tabla, valida, hookbeforeToolCall; devuelvePreparedToolCalloImmediateToolCallOutcome.packages/agent/src/agent-loop.ts:581-616—executePreparedToolCall: llama atool.execute, partial result portool_execution_update, excepciones a error.packages/agent/src/agent-loop.ts:618-661—finalizeExecutedToolCall: llama aafterToolCall, sobrescribe a nivel de campo, excepciones del hook a error.packages/agent/src/agent-loop.ts:680-690—createToolResultMessage: envuelve el finalized enToolResultMessage.
Núcleo del modo sequential: una herramienta recorre los cinco tramos antes de la siguiente:
// 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 modo parallel, prepare es en serie y execute concurrente; el orden de eventos es la diferencia clave:
// 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 emite end de inmediato
finalizedCalls.push(finalized);
continue;
}
finalizedCalls.push(async () => { // se pospone a Promise.all
const executed = await executePreparedToolCall(preparation, signal, emit);
const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
await emitToolExecutionEnd(finalized, emit); // al finalize se emite end
return finalized;
});
}
const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => typeof entry === "function" ? entry() : Promise.resolve(entry)));
// luego construye toolResultMessage en orden de origen ...Flujo de datos
Secuencia para un mensaje assistant con 3 toolCalls en modo parallel:
Límites y fallos
- Herramienta no encontrada:
prepareToolCallno encuentratoolCall.nameen la tabla y devuelve un resultado de errorimmediate; no lanza ni interrumpe el lote. Verpackages/agent/src/agent-loop.ts:536-543. - Validación de argumentos fallida:
validateToolArgumentslanza y se captura, convirtiéndolo en resultado de errorimmediate; eltool_execution_endde la herramienta se emite igualmente. Verpackages/agent/src/agent-loop.ts:572-578. - beforeToolCall bloquea: si
beforeToolCalldevuelve{ block: true, reason }, la herramienta se convierte en resultado de error; el reason va al content del resultado. Verpackages/agent/src/agent-loop.ts:558-565. - execute lanza:
executePreparedToolCallcaptura la excepción y devuelve un resultado conisError: true; los eventos de partial result esperan aPromise.allantes de regresar. Verpackages/agent/src/agent-loop.ts:608-615. - afterToolCall lanza:
finalizeExecutedToolCallcaptura la excepción del hook y la sobrescribe con un resultado de error, sin afectar al finalize de las demás herramientas. Verpackages/agent/src/agent-loop.ts:650-654. - Terminación del lote:
shouldTerminateToolBatchexige que todo el lote marqueterminate: true;runLooppor tanto ponehasMoreToolCalls=falsey sale del interno. Verpackages/agent/src/agent-loop.ts:511-513ypackages/agent/src/agent-loop.ts:206-214.
Resumen
La ejecución de herramientas se corta en cinco tramos: prepare/execute/finalize/emitEnd/createToolResultMessage. Sequential va en serie para todo el lote; parallel hace prepare en serie y execute concurrente; los eventos van por orden de finalización y los toolResult por orden de origen. Los hooks beforeToolCall/afterToolCall se invocan en las fases prepare y finalize; cualquier excepción se convierte en resultado de error en vez de interrumpir el lote. La forma de los hooks y los campos de AgentTool en contrato de tipos; dónde el interno de runLoop invoca los cinco tramos en bucle while doble.