Skip to content

工具執行 sequential/parallel ​

源码版本v0.73.1

executeToolCalls 是 runLoop 內層處理 assistant 訊息裡 tool calls 的子流程。它把工具呼叫的生命週期切成五段:prepareToolCall(查表+校驗+beforeToolCall 鉤子)→ executePreparedToolCall(真跑 tool.execute)→ finalizeExecutedToolCall(afterToolCall 鉤子覆蓋)→ emitToolExecutionEnd → createToolResultMessage。sequential 和 parallel 兩種模式共用這五段,差別只在排程順序:sequential 一條走完再下一條,parallel 先循序 prepare,再 Promise.all 並發執行+finalize。

職責 ​

  1. 模式分發:executeToolCalls 檢查 config.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. 執行階段:executePreparedToolCall 呼叫 tool.execute(id, args, signal, onUpdate),partial result 透過 tool_execution_update 事件串流給 UI,例外轉成 error result。見 packages/agent/src/agent-loop.ts:581-616。
  4. 收尾階段:finalizeExecutedToolCall 呼叫 afterToolCall 鉤子,按欄位覆蓋 content/details/isError/terminate,鉤子自己拋錯也轉成 error result。見 packages/agent/src/agent-loop.ts:618-661。
  5. 構造結果訊息:createToolResultMessage 把 finalized 結果包成 ToolResultMessage(role/toolCallId/content/details/isError/timestamp),由呼叫方推進 context。見 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 兩種 outcome?工具找不到、參數校驗失敗、beforeToolCall block 這些情況,根本沒到 execute 那步,直接回傳一個 error result。把它和正常 execute 的結果用同一個 FinalizedToolCallOutcome 形狀封裝,下游的 emitToolExecutionEnd/createToolResultMessage 不用區分兩種路徑。

關鍵檔案 ​

循序模式的核心迴圈,一條工具走完五段才下一條:

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

資料流 ​

一個 assistant 訊息帶 3 個 tool calls,parallel 模式下的時序:

邊界與失敗 ​

小結 ​

工具執行被切成五段:prepare/execute/finalize/emitEnd/createToolResultMessage。sequential 整批循序,parallel prepare 循序、execute 並發,事件按完成順序、toolResult 按源順序。beforeToolCall/afterToolCall 鉤子在 prepare 和 finalize 階段呼叫,任何例外都被轉成 error result 而不是中斷整批。鉤子形狀和 AgentTool 欄位,看 型別契約;五段函式被 runLoop 內層呼叫的位置,看 雙層 while 主迴圈。