ツール実行 sequential/parallel
executeToolCalls は runLoop の内側で assistant メッセージ内の tool calls を処理するサブフローだ。ツール呼び出しのライフサイクルを 5 段階に切る:prepareToolCall (表引き+検証+beforeToolCall フック) → executePreparedToolCall (実際に tool.execute を走らせる) → finalizeExecutedToolCall (afterToolCall フックで上書き) → emitToolExecutionEnd → createToolResultMessage。sequential と parallel の 2 モードはこの 5 段階を共有し、違いはスケジュール順だけ:sequential は 1 本ずつ終わってから次、parallel は直列 prepare の後に Promise.all で並列 execute+finalize。
責務
- モードディスパッチ:
executeToolCallsはconfig.toolExecutionと「いずれかのツールのexecutionMode === "sequential"」をチェックし、どちらかが真なら sequential に進む。packages/agent/src/agent-loop.ts:350-365参照。 - 準備段階:
prepareToolCallがツールを探し、prepareToolCallArgumentsで旧引数に互換性を持たせ、validateToolArgumentsで検証し、beforeToolCallフックで block 可能。packages/agent/src/agent-loop.ts:529-579参照。 - 実行段階:
executePreparedToolCallはtool.execute(id, args, signal, onUpdate)を呼ぶ。partial result はtool_execution_updateイベントで UI に流れ、例外は error result に変換される。packages/agent/src/agent-loop.ts:581-616参照。 - 仕上げ段階:
finalizeExecutedToolCallはafterToolCallフックを呼び、content/details/isError/terminateをフィールド単位で上書きする。フック自身が throw しても error result に変換される。packages/agent/src/agent-loop.ts:618-661参照。 - 結果メッセージ構築:
createToolResultMessageは finalized した結果をToolResultMessage(role/toolCallId/content/details/isError/timestamp) に包み、呼び出し側が context に push する。packages/agent/src/agent-loop.ts:680-690参照。
設計動機
なぜ prepare は直列、execute は並列なのか? beforeToolCall フックは通常、認可、ログ、レートリミットのような副作用のあることをするので、並列に発火させると競合しやすい (例: OAuth token を同時にリフレッシュする)。prepare が直列ならフックが順序通り走る。execute が並列なのは、ツール自体 (ファイル読み、bash 実行、HTTP リクエスト) が互いに独立で、直列にすると wall time を無駄にするからだ。
なぜ tool_execution_end は「完了順」、toolResult message は「元の順序」で送られるのか? parallel モードでは Promise.all の後にループで toolResultMessage を構築するが、emitToolExecutionEnd は各ツールが finalize し終わった瞬間に呼ぶ——UI はツールが完了した瞬間に結果を受け取れる。toolResult message は LLM に食わせるもので、順序が乱れるとモデルが混乱するため、assistant メッセージ内の toolCall 順に送る。この「イベントは完了順、メッセージは元順」という分離で、UI 応答が速く、LLM コンテキストが安定する。
なぜ immediate と prepared の 2 種の outcome があるのか? ツールが見つからない、引数検証失敗、beforeToolCall block これらは execute まで行かず、直接 error result を返す。これを通常の execute 結果と同じ FinalizedToolCallOutcome 形に包むことで、下流の emitToolExecutionEnd/createToolResultMessage は 2 つのパスを区別しなくて済む。
主要ファイル
packages/agent/src/agent-loop.ts:350-365—executeToolCallsのモードディスパッチ。いずれかのツールが sequential を宣言すればバッチ全体が sequential に。packages/agent/src/agent-loop.ts:372-422—executeToolCallsSequential:for (const toolCall of toolCalls)で 1 本ずつ 5 段階を踏む。packages/agent/src/agent-loop.ts:424-483—executeToolCallsParallel:ループで prepare/immediate を振り分け、実行待ちを() => Promiseクロージャに詰め、最後にPromise.allで並列化。packages/agent/src/agent-loop.ts:511-513—shouldTerminateToolBatch:バッチ全体の finalize がすべてterminate: trueでないと終了しない。packages/agent/src/agent-loop.ts:515-527—prepareToolCallArguments:ツール自身のprepareArgumentsで引数互換性を処理、同じオブジェクトならコピーしない。packages/agent/src/agent-loop.ts:529-579—prepareToolCall:ツール表引き、検証、beforeToolCallフック。PreparedToolCallまたはImmediateToolCallOutcomeを返す。packages/agent/src/agent-loop.ts:581-616—executePreparedToolCall:tool.executeを呼び、partial result はtool_execution_update、例外は error に。packages/agent/src/agent-loop.ts:618-661—finalizeExecutedToolCall:afterToolCallを呼び、フィールド単位の上書き。フックが throw しても error に。packages/agent/src/agent-loop.ts:680-690—createToolResultMessage:finalized をToolResultMessageに包む。
sequential モードの核心ループ、1 本のツールが 5 段階を終えてから次へ:
// 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);
}parallel モードは prepare 直列、execute 並列で、イベント順序が鍵の違い:
// 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 は即座に end を送る
finalizedCalls.push(finalized);
continue;
}
finalizedCalls.push(async () => { // Promise.all まで遅延
const executed = await executePreparedToolCall(preparation, signal, emit);
const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
await emitToolExecutionEnd(finalized, emit); // finalize が終わったら end を送る
return finalized;
});
}
const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => typeof entry === "function" ? entry() : Promise.resolve(entry)));
// その後、元順で toolResultMessage を構築 ...データフロー
3 つの tool calls を持つ assistant メッセージ、parallel モードでの時系列:
境界と失敗
- ツール不在:
prepareToolCallがツール表にtoolCall.nameを見つけられない時、immediateerror result を返す。throw せずバッチ全体を止めない。packages/agent/src/agent-loop.ts:536-543参照。 - 引数検証失敗:
validateToolArgumentsが throw すると catch されてimmediateerror result に変わり、そのツールのtool_execution_endは依然送られる。packages/agent/src/agent-loop.ts:572-578参照。 - beforeToolCall block:
beforeToolCallが{ block: true, reason }を返すと、そのツールは error result になり、reason が result content に書かれる。packages/agent/src/agent-loop.ts:558-565参照。 - execute の throw:
executePreparedToolCallが例外を catch し、isError: trueの result を返す。partial result イベントは先にPromise.allで待ち受けてから返す。packages/agent/src/agent-loop.ts:608-615参照。 - afterToolCall の throw:
finalizeExecutedToolCallがフックの例外を catch し、error result で上書きする。他のツールの finalize には影響しない。packages/agent/src/agent-loop.ts:650-654参照。 - バッチ全体の終止:
shouldTerminateToolBatchはバッチ全体がterminate: trueでないと true を返さず、runLoopはこれに基づいてhasMoreToolCalls=falseで内側を抜ける。packages/agent/src/agent-loop.ts:511-513とpackages/agent/src/agent-loop.ts:206-214参照。
小ねた
ツール実行は 5 段階に切られる:prepare/execute/finalize/emitEnd/createToolResultMessage。sequential はバッチ全体を直列、parallel は prepare 直列・execute 並列で、イベントは完了順・toolResult は元順。beforeToolCall/afterToolCall フックは prepare と finalize 段階で呼ばれ、例外はすべて error result に変換されてバッチ全体を止めることはない。フックの形と AgentTool のフィールドは 型契約、5 段階の関数が runLoop 内側でどう呼ばれるかは 二重 while ループ 参照。