工具执行 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 结果。见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。
sequential 模式的核心循环,一条工具走完五段才下一条:
// 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 主循环。