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 结果。见 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 不用区分两种路径。

关键文件 ​

sequential 模式的核心循环,一条工具走完五段才下一条:

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 主循环。