Werkzeugausführung sequential/parallel
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) → emitToolExecutionEnd → createToolResultMessage. 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
- Modus-Verteilung:
executeToolCallsprüftconfig.toolExecutionund ob irgendein WerkzeugexecutionMode === "sequential"deklariert, bei einem Treffer geht die ganze Charge in den sequential-Pfad. Siehepackages/agent/src/agent-loop.ts:350-365. - Prepare-Phase:
prepareToolCallsucht das Werkzeug,prepareToolCallArgumentsmacht alte Parameter kompatibel,validateToolArgumentsvalidiert, derbeforeToolCall-Hook kann blocken. Siehepackages/agent/src/agent-loop.ts:529-579. - Execute-Phase:
executePreparedToolCallrufttool.execute(id, args, signal, onUpdate)auf, partial result fließt über dastool_execution_update-Event an die UI, Exceptions werden zu einem error result. Siehepackages/agent/src/agent-loop.ts:581-616. - Finalize-Phase:
finalizeExecutedToolCallruft denafterToolCall-Hook auf und überschreibt feldweisecontent/details/isError/terminate; wenn der Hook selbst wirft, wird es ebenfalls in ein error result konvertiert. Siehepackages/agent/src/agent-loop.ts:618-661. - Ergebnisnachricht konstruieren:
createToolResultMessagewickelt das finalized Ergebnis in einToolResultMessage(role/toolCallId/content/details/isError/timestamp) ein, das der Aufrufer in den context schiebt. Siehepackages/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
packages/agent/src/agent-loop.ts:350-365—executeToolCallsModus-Verteilung, ein Werkzeug mit sequential deklariert schickt die ganze Charge in den sequential-Pfad.packages/agent/src/agent-loop.ts:372-422—executeToolCallsSequential:for (const toolCall of toolCalls)durchläuft jeden komplett durch die fünf Phasen.packages/agent/src/agent-loop.ts:424-483—executeToolCallsParallel: zuerst Schleife prepare/immediate-Verteilung, die Auszuführenden als() => Promise-Closures eingereiht, dannPromise.allparallel.packages/agent/src/agent-loop.ts:511-513—shouldTerminateToolBatch: nur wenn alle finalizeterminate: truesetzen, wird abgebrochen, ein einziger nicht-terminate führt weiter.packages/agent/src/agent-loop.ts:515-527—prepareToolCallArguments: nutzt das werkzeugeigeneprepareArgumentsfür Parameter-Kompatibilität, gibt dasselbe Objekt zurück, ohne zu kopieren.packages/agent/src/agent-loop.ts:529-579—prepareToolCall: Werkzeugtabelle, Validierung,beforeToolCall-Hook, gibtPreparedToolCalloderImmediateToolCallOutcomezurück.packages/agent/src/agent-loop.ts:581-616—executePreparedToolCall: rufttool.executeauf, partial result übertool_execution_update, Exception wird zu error.packages/agent/src/agent-loop.ts:618-661—finalizeExecutedToolCall: ruftafterToolCallauf, feldweise Überschreibung, Hook-Wurf wird zu error.packages/agent/src/agent-loop.ts:680-690—createToolResultMessage: wickelt finalized inToolResultMessageein.
Die Kernschleife des sequential-Modus: ein Werkzeug läuft durch alle fünf Phasen, bevor das nächste drankommt:
// 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:
// 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
- Werkzeug nicht gefunden:
prepareToolCallfindettoolCall.namenicht in der Werkzeugtabelle, gibtimmediateerror result zurück, wirft nicht und bricht die Charge nicht ab, siehepackages/agent/src/agent-loop.ts:536-543. - Parameter-Validierung fehlgeschlagen:
validateToolArgumentswirft, wird gefangen und in einimmediateerror result konvertiert, dastool_execution_enddieses Werkzeugs wird trotzdem gesendet, siehepackages/agent/src/agent-loop.ts:572-578. - beforeToolCall block: Wenn
beforeToolCall{ block: true, reason }zurückgibt, wird das Werkzeug zu einem error result, der reason wird in den result content geschrieben, siehepackages/agent/src/agent-loop.ts:558-565. - execute wirft:
executePreparedToolCallfängt die Exception, gibt ein result mitisError: truezurück, partial result events werden zuerst mitPromise.allabgewartet, siehepackages/agent/src/agent-loop.ts:608-615. - afterToolCall wirft:
finalizeExecutedToolCallfängt die Hook-Exception, überschreibt mit einem error result, beeinflusst das finalize anderer Werkzeuge nicht, siehepackages/agent/src/agent-loop.ts:650-654. - Charge abbrechen:
shouldTerminateToolBatchverlangt, dass alleterminate: truesetzen, dann gibt es true zurück;runLoopsetzt entsprechendhasMoreToolCalls=falseund verlässt die innere Schleife, siehepackages/agent/src/agent-loop.ts:511-513undpackages/agent/src/agent-loop.ts:206-214.
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.