Skip to content

Werkzeugausführung sequential/parallel

源码版本v0.73.1

executeToolCalls ist der Teilprozess innerhalb der inneren Schleife von runLoop, der die tool calls in der assistant-Nachricht bearbeitet. Er zerlegt den Lebenszyklus eines Werkzeugaufrufs in fünf Phasen: prepareToolCall (Tabellen-Lookup + Validierung + beforeToolCall-Hook) → executePreparedToolCall (tool.execute wirklich ausführen) → finalizeExecutedToolCall (afterToolCall-Hook überschreibt Felder) → emitToolExecutionEndcreateToolResultMessage. Die beiden Modi sequential und parallel teilen sich diese fünf Phasen, der Unterschied liegt nur in der Reihenfolge: sequential läuft einen komplett durch, bevor der nächste drankommt; parallel bereitet zuerst seriell vor, dann über Promise.all parallel execute+finalize.

Verantwortung

  1. Modus-Verteilung: executeToolCalls prüft config.toolExecution und ob irgendein Werkzeug executionMode === "sequential" deklariert, bei einem Treffer geht die ganze Charge in den sequential-Pfad. Siehe packages/agent/src/agent-loop.ts:350-365.
  2. Prepare-Phase: prepareToolCall sucht das Werkzeug, prepareToolCallArguments macht alte Parameter kompatibel, validateToolArguments validiert, der beforeToolCall-Hook kann blocken. Siehe packages/agent/src/agent-loop.ts:529-579.
  3. Execute-Phase: executePreparedToolCall ruft tool.execute(id, args, signal, onUpdate) auf, partial result fließt über das tool_execution_update-Event an die UI, Exceptions werden zu einem error result. Siehe packages/agent/src/agent-loop.ts:581-616.
  4. Finalize-Phase: finalizeExecutedToolCall ruft den afterToolCall-Hook auf und überschreibt feldweise content/details/isError/terminate; wenn der Hook selbst wirft, wird es ebenfalls in ein error result konvertiert. Siehe packages/agent/src/agent-loop.ts:618-661.
  5. Ergebnisnachricht konstruieren: createToolResultMessage wickelt das finalized Ergebnis in ein ToolResultMessage (role/toolCallId/content/details/isError/timestamp) ein, das der Aufrufer in den context schiebt. Siehe packages/agent/src/agent-loop.ts:680-690.

Entwurfsmotivation

Warum prepare seriell und execute parallel? Weil der beforeToolCall-Hook normalerweise Dinge mit Seiteneffekten macht wie Auth, Logging, Rate-Limiting; paralleles Triggern erzeugt schnell Race Conditions (z. B. gleichzeitiges OAuth-Token-Auffrischen). Serielles prepare stellt sicher, dass die Hooks in Reihenfolge laufen. Execute ist parallel, weil die Werkzeuge selbst (Datei lesen, bash ausführen, HTTP-Request) voneinander unabhängig sind, seriell verschwendet wall time.

Warum wird tool_execution_end nach "Reihenfolge des Abschlusses" gesendet, die toolResult-Nachricht aber nach "Quell-Reihenfolge"? Im parallel-Modus wird nach Promise.all in einer Schleife toolResultMessage konstruiert, aber emitToolExecutionEnd wird direkt nach finalize jedes Werkzeugs aufgerufen - die UI bekommt das Ergebnis in dem Moment, in dem ein Werkzeug fertig ist, ohne auf die ganze Charge zu warten. Die toolResult-Nachricht muss an den LLM zurückgegeben werden, eine falsche Reihenfolge verwirrt das Modell, deshalb wird sie in der toolCall-Reihenfolge der assistant-Nachricht gesendet. Diese Trennung von "Event-Stream nach Abschluss-Reihenfolge, Nachrichten-Stream nach Quell-Reihenfolge" macht die UI schnell und den LLM-Kontext stabil.

Warum gibt es immediate und prepared als zwei Outcomes? Wenn ein Werkzeug nicht gefunden wird, die Parameter-Validierung scheitert oder beforeToolCall blockt, kommt execute gar nicht erst zum Zug - es wird direkt ein error result zurückgegeben. Dieses und das Ergebnis eines echten execute werden in derselben FinalizedToolCallOutcome-Form gekapselt, damit emitToolExecutionEnd/createToolResultMessage downstream die beiden Pfade nicht unterscheiden müssen.

Wichtige Dateien

Die Kernschleife des sequential-Modus: ein Werkzeug läuft durch alle fünf Phasen, bevor das nächste drankommt:

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);
}

Im parallel-Modus ist prepare seriell, execute parallel, die Event-Reihenfolge ist der entscheidende Unterschied:

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 sendet end sofort
    finalizedCalls.push(finalized);
    continue;
  }
  finalizedCalls.push(async () => {                 // verzögert bis Promise.all
    const executed = await executePreparedToolCall(preparation, signal, emit);
    const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
    await emitToolExecutionEnd(finalized, emit);    // end direkt nach finalize
    return finalized;
  });
}
const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => typeof entry === "function" ? entry() : Promise.resolve(entry)));
// dann toolResultMessage in Quell-Reihenfolge konstruieren ...

Datenfluss

Eine assistant-Nachricht mit 3 tool calls, Zeitreihe im parallel-Modus:

Grenzen und Fehler

Zusammenfassung

Die Werkzeugausführung ist in fünf Phasen geschnitten: prepare/execute/finalize/emitEnd/createToolResultMessage. sequential macht die ganze Charge seriell, parallel macht prepare seriell und execute parallel, Events nach Abschluss-Reihenfolge, toolResult nach Quell-Reihenfolge. Die beforeToolCall/afterToolCall-Hooks werden in prepare und finalize aufgerufen, jede Exception wird in ein error result konvertiert statt die Charge abzubrechen. Die Form der Hooks und die AgentTool-Felder siehe Typvertrag; wo die fünf Phasen-Funktionen von der inneren Schleife von runLoop aufgerufen werden, siehe Doppelte while-Hauptschleife.