Skip to content

Ejecución de herramientas sequential/parallel

源码版本v0.73.1

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) → emitToolExecutionEndcreateToolResultMessage. 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

  1. Dispatch por modo: executeToolCalls comprueba config.toolExecution y si alguna herramienta tiene executionMode === "sequential"; si cualquiera es true, va por sequential. Ver packages/agent/src/agent-loop.ts:350-365.
  2. Fase de preparación: prepareToolCall busca la herramienta, prepareToolCallArguments hace compatibilidad con argumentos antiguos, validateToolArguments valida, el hook beforeToolCall puede bloquear. Ver packages/agent/src/agent-loop.ts:529-579.
  3. Fase de ejecución: executePreparedToolCall invoca tool.execute(id, args, signal, onUpdate); los resultados parciales fluyen como eventos tool_execution_update a la UI; las excepciones se convierten en resultados de error. Ver packages/agent/src/agent-loop.ts:581-616.
  4. Fase de finalización: finalizeExecutedToolCall llama al hook afterToolCall, que sobrescribe por campo content/details/isError/terminate; si el hook lanza, también se convierte en resultado de error. Ver packages/agent/src/agent-loop.ts:618-661.
  5. Construcción del mensaje de resultado: createToolResultMessage envuelve el resultado finalized en un ToolResultMessage (role/toolCallId/content/details/isError/timestamp) que el llamador empuja al context. Ver packages/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

Núcleo del modo sequential: una herramienta recorre los cinco tramos antes de la siguiente:

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 modo parallel, prepare es en serie y execute concurrente; el orden de eventos es la diferencia clave:

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 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

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.