Skip to content

ツール実行 sequential/parallel

源码版本v0.73.1

executeToolCallsrunLoop の内側で assistant メッセージ内の tool calls を処理するサブフローだ。ツール呼び出しのライフサイクルを 5 段階に切る:prepareToolCall (表引き+検証+beforeToolCall フック) → executePreparedToolCall (実際に tool.execute を走らせる) → finalizeExecutedToolCall (afterToolCall フックで上書き) → emitToolExecutionEndcreateToolResultMessage。sequential と parallel の 2 モードはこの 5 段階を共有し、違いはスケジュール順だけ:sequential は 1 本ずつ終わってから次、parallel は直列 prepare の後に Promise.all で並列 execute+finalize。

責務

  1. モードディスパッチ:executeToolCallsconfig.toolExecution と「いずれかのツールの executionMode === "sequential"」をチェックし、どちらかが真なら sequential に進む。packages/agent/src/agent-loop.ts:350-365 参照。
  2. 準備段階:prepareToolCall がツールを探し、prepareToolCallArguments で旧引数に互換性を持たせ、validateToolArguments で検証し、beforeToolCall フックで block 可能。packages/agent/src/agent-loop.ts:529-579 参照。
  3. 実行段階:executePreparedToolCalltool.execute(id, args, signal, onUpdate) を呼ぶ。partial result は tool_execution_update イベントで UI に流れ、例外は error result に変換される。packages/agent/src/agent-loop.ts:581-616 参照。
  4. 仕上げ段階:finalizeExecutedToolCallafterToolCall フックを呼び、content/details/isError/terminate をフィールド単位で上書きする。フック自身が throw しても error result に変換される。packages/agent/src/agent-loop.ts:618-661 参照。
  5. 結果メッセージ構築: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 コンテキストが安定する。

なぜ immediateprepared の 2 種の outcome があるのか? ツールが見つからない、引数検証失敗、beforeToolCall block これらは execute まで行かず、直接 error result を返す。これを通常の execute 結果と同じ FinalizedToolCallOutcome 形に包むことで、下流の emitToolExecutionEnd/createToolResultMessage は 2 つのパスを区別しなくて済む。

主要ファイル

sequential モードの核心ループ、1 本のツールが 5 段階を終えてから次へ:

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

parallel モードは prepare 直列、execute 並列で、イベント順序が鍵の違い:

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 は即座に 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 を見つけられない時、immediate error result を返す。throw せずバッチ全体を止めない。packages/agent/src/agent-loop.ts:536-543 参照。
  • 引数検証失敗:validateToolArguments が throw すると catch されて immediate error 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-513packages/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 ループ 参照。