工具執行 sequential/parallel
executeToolCalls 是 runLoop 內層處理 assistant 訊息裡 tool calls 的子流程。它把工具呼叫的生命週期切成五段:prepareToolCall(查表+校驗+beforeToolCall 鉤子)→ executePreparedToolCall(真跑 tool.execute)→ finalizeExecutedToolCall(afterToolCall 鉤子覆蓋)→ emitToolExecutionEnd → createToolResultMessage。sequential 和 parallel 兩種模式共用這五段,差別只在排程順序:sequential 一條走完再下一條,parallel 先循序 prepare,再 Promise.all 並發執行+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,鉤子自己拋錯也轉成 error result。見packages/agent/src/agent-loop.ts:618-661。 - 構造結果訊息:
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 不用區分兩種路徑。
關鍵檔案
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)逐條走完五段。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,欄位級覆蓋,鉤子拋錯也轉 error。packages/agent/src/agent-loop.ts:680-690—createToolResultMessage:把 finalized 包成ToolResultMessage。
循序模式的核心迴圈,一條工具走完五段才下一條:
// 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 ...資料流
一個 assistant 訊息帶 3 個 tool calls,parallel 模式下的時序:
邊界與失敗
- 工具找不到:
prepareToolCall在工具表裡找不到toolCall.name,回傳immediateerror result,不拋錯不打斷整批,見packages/agent/src/agent-loop.ts:536-543。 - 參數校驗失敗:
validateToolArguments拋錯被 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 拋錯:
executePreparedToolCallcatch 例外,回傳isError: true的 result,partial result 事件先Promise.all等完再回傳,見packages/agent/src/agent-loop.ts:608-615。 - afterToolCall 拋錯:
finalizeExecutedToolCallcatch 鉤子例外,覆蓋成 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。
小結
工具執行被切成五段:prepare/execute/finalize/emitEnd/createToolResultMessage。sequential 整批循序,parallel prepare 循序、execute 並發,事件按完成順序、toolResult 按源順序。beforeToolCall/afterToolCall 鉤子在 prepare 和 finalize 階段呼叫,任何例外都被轉成 error result 而不是中斷整批。鉤子形狀和 AgentTool 欄位,看 型別契約;五段函式被 runLoop 內層呼叫的位置,看 雙層 while 主迴圈。