Skip to content

工具執行 sequential/parallel

源码版本v0.73.1

executeToolCallsrunLoop 內層處理 assistant 訊息裡 tool calls 的子流程。它把工具呼叫的生命週期切成五段:prepareToolCall(查表+校驗+beforeToolCall 鉤子)→ executePreparedToolCall(真跑 tool.execute)→ finalizeExecutedToolCall(afterToolCall 鉤子覆蓋)→ emitToolExecutionEndcreateToolResultMessage。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 上下文穩定。

為什麼有 immediateprepared 兩種 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 主迴圈