执行器:调度、审批、超时与结果

一批工具调用怎样按并发安全分组、哪些并行哪些串行;单个调用怎样走完校验、PreToolUse、权限审批、执行、PostToolUse 与结果投影;超时的默认值与取消语义;错误和大结果怎样变成给模型的内容,又怎样投影给界面;执行事件与遥测。

作者 David更新于 15 篇(共 47 篇)

上一篇的契约回答“这个工具是什么”,执行器回答“这一次调用怎么跑”。每个 AgentRuntime 持有一个 ToolExecutorImplapps/zcode-cli/packages/core/src/tool/executor/impl.ts:16),由 createRuntimeToolExecutor 一次注入注册表、权限服务、钩子执行器,以及文件系统、执行、HTTP、子 Agent、工作流等二十多个端口(apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:148)。模型给出工具调用后,回合循环经运行时方法 scheduleToolsexecuteTools 进入执行器(apps/zcode-cli/packages/core/src/runtime/methods/tools.ts:38tools.ts:63)。回合何时调用它、结果怎样写回历史,见回合循环与 TurnMachine一次模型请求:流式、工具并发与恢复;本篇只讲执行器内部。

执行器对外只有四个方法:execute 跑单个调用,executeBatch 并发跑一组,executeSchedule 按分组逐组跑并产出批次事件,trackExternalBackgroundTask 把不是本回合启动的后台任务(恢复中的工作流 run)纳入追踪(apps/zcode-cli/packages/core/src/tool/executor/types.ts:140)。代码住在 apps/zcode-cli/packages/core/src/tool/executor

文件职责
../scheduler.ts分组:哪些调用可以放在一起并发
impl.tsbatch-runner.ts门面与默认值;逐组执行、回合停止后取消剩余调用
call-runner.ts单个调用的完整流水线
validation.tshook-flow.tspermission-*.tsapproval-gate.ts输入输出校验、钩子、权限判定与审批闸
timeout.ts超时解析、可暂停的 deadline、取消
result-serialization.tsresult-content-projection.ts../result-persistence-format.ts结果预算、截断、落盘
result-display.tsdisplay-text.tsbash-result-display.ts给界面的 display 投影
errors.tsturn-control.ts错误结果、回合控制
events.tstelemetry.tsmodel-status-sink.ts会话事件、遥测、工具内部模型请求的状态
background-task*.tsworkflow-*.ts后台任务追踪与工作流展示,分别见后台任务与通知动态工作流(三)

怎么用

执行器没有专门的命令,影响它的旋钮是这几个:

旋钮默认出处
配置 toolConcurrency.maxConcurrency,或环境变量 ZCODE_MAX_TOOL_CONCURRENCY一组最多并发 10 个apps/zcode-cli/packages/contracts/src/config/index.ts:343apps/zcode-cli/packages/adapters/src/config/env-config.adapter.ts:56
环境变量 BASH_DEFAULT_TIMEOUT_MSBASH_MAX_TIMEOUT_MS120000、600000 毫秒apps/zcode-cli/packages/core/src/tool/bash-timeout-policy.ts:6bash-timeout-policy.ts:18
调用入参 timeout(Bash)、timeout_msjs只有声明 allowCallOverride 的工具认apps/zcode-cli/packages/core/src/tool/executor/timeout.ts:196
PreToolUse、PermissionRequest、PostToolUse、PostToolUseFailure 钩子默认关闭生命周期 Hooks 与工作区信任

在界面上看到的是它发出的事件:工具行从“排队”到“运行”到“完成”或“失败”,需要审批时弹出确认,大输出只显示预览并给出落盘路径。

一批调用怎样分组

一次模型回复里可能有多个工具调用。scheduleTools 先从注册表取每个调用的旗标(tools.ts:42):依赖一律为空,副作用范围优先取 permission.sideEffectScopereadOnly 只有在声明只读范围为 none 时才算真,所以声明了只读、范围却是 session 的 TodoWrite、Agent 在调度眼里不算只读。然后交给 ToolScheduler,判定能否并发的规则在 apps/zcode-cli/packages/core/src/tool/scheduler.ts:85

  private canRunInParallel(tool: ToolDependency): boolean {
    const hasToolName = typeof tool.toolName === "string" && tool.toolName.length > 0;
    const hasSafetyMetadata =
      tool.readOnly !== undefined ||
      tool.destructive !== undefined ||
      tool.concurrentSafe !== undefined ||
      tool.sideEffectScope !== undefined;

    if (!hasToolName && !hasSafetyMetadata) {
      return true;
    }

    const readOnly = tool.readOnly ?? (hasToolName ? this.readOnlyTools.has(tool.toolName!) : false);
    if (tool.destructive) return false;
    if (tool.concurrentSafe === true) return true;
    if (tool.concurrentSafe === false) return false;
    if (readOnly) return true;
    return tool.sideEffectScope === "none";
  }

内置工具都声明了 concurrentSafe,所以实际规则只剩两条:破坏性的一律串行,其余看 concurrentSafe。后面两个兜底分支只对注册表里查不到的名字起作用:名字在一张写死的只读名单里就并发(scheduler.ts:233),否则串行。

分组保持模型给出的顺序(scheduler.ts:154):连续的可并发调用攒成一组,满 10 个另起一组;遇到一个不可并发的调用,先把手里的组交出去,它自己单独成组。模型依次调用 Read aRead bEdit cWebFetch dBash eRead f,得到五组:[a, b][c][d][e][f]。调度器还带着拓扑排序和环检测,但运行时从不填依赖(tools.ts:49),这套机制目前闲置。

按这条规则,40 个内置条目里有 13 个总是独占一组:Write、Edit、Bash、TodoWrite、CronCreate、CronUpdate、CronDelete(唯一声明了破坏性的内置工具)、OffPeakCreate、EnterPlanMode、ExitPlanMode、submit_resultjs、SaveWorkflow。其余都可以并发,包括 Agent:一次派出的多个子 Agent 会同时跑。MCP 工具在注解声明只读或幂等时可并发,声明破坏性时串行。

executeToolSchedule 逐组执行,组内 Promise.allapps/zcode-cli/packages/core/src/tool/executor/batch-runner.ts:56)。任何结果带了“停止回合”的控制信号,后面各组都不再执行,每个调用拿到一条合成的取消结果:“Tool cancelled because a previous tool result requested a turn stop.”(batch-runner.ts:16batch-runner.ts:92)。旧版本还会在前一组的非并发安全工具失败后跳过后续所有组,注释说这会让 TodoWrite 之类的本地失败误截断后面的 Agent 调用,已经改成继续执行、各自由权限与 handler 决定结果(batch-runner.ts:130)。

executeToolSchedule 把每组交给 executeBatch 时只转发了 automationTurn,没有转发 offPeakTurnbatch-runner.ts:80),而回合里的工具恰好都走这条路径。回合过滤仍会在闲时执行轮把 OffPeakCreate、SendMessage 挡在清单之外,但 Bash 拒绝 run_in_background 只靠这个标记(apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:139)。从代码看,这道拒绝在闲时执行轮里不会生效。

流式输出期间还有一条旁路:只读、并发安全、非破坏、无需审批、无需用户交互且范围为 none 的调用,会在模型还在输出时就单独调度执行(apps/zcode-cli/packages/core/src/runtime/methods/streaming-tool-coordinator.ts:339);它同样经过 executeTools,走的是同一条流水线。何时触发、怎样合并结果,见一次模型请求

单个调用的流水线

executeToolCall 是一次调用的全部生命周期(apps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:65):

图表加载中…

逐段看:

阶段做什么出处
查找别名换成规范名;空名或查不到时直接返回错误并补发 ToolCallError,否则界面上的工具行会一直停在“输入中”call-runner.ts:121
投影与归一化用当前模型投影契约(校验与模型看到的是同一份 schema),JSON 字符串入参先解析,再跑 zodcall-runner.ts:101call-runner.ts:166
准入校验JSON Schema 不通过就返回 InputValidationError,格式见上一篇call-runner.ts:172
工具自校验与解析validateInput 做语义校验,resolveInput 把入参换成执行事实;都在钩子之前,免得先弹一次注定失败的确认窗call-runner.ts:187call-runner.ts:207
PreToolUse钩子可以拒绝、要求确认、改写入参或追加上下文;改写后的入参重新校验call-runner.ts:232call-runner.ts:268
权限权限服务判定 allow、deny、ask;ask 时发出审批请求并等待call-runner.ts:289
开始ToolCallStarted,带上按执行入参重算的只读与副作用范围call-runner.ts:329
执行建执行上下文,带超时跑 handler;handler 以返回值表达的业务失败转成异常,走统一的失败出口call-runner.ts:442call-runner.ts:451
收尾校验输出,序列化,跑 PostToolUse,追加钩子上下文,生成 display,挂回合控制,发 ToolCallResult,再交给后台任务追踪call-runner.ts:456call-runner.ts:543

resolveInput 的位置是刻意的。注释列了三条理由:此后钩子、项目权限规则、权限事件载荷、审批预览和 handler 读到的都是同一份归一化输入,策略不会被绕开,跨客户端版本可见,确认的内容与执行的内容逐字节相同,不存在“批准 A 跑 B”(apps/zcode-cli/packages/core/src/tool/types.ts:308)。

权限在流水线里的位置。判定细节归权限模式与规则,这里只看顺序(apps/zcode-cli/packages/core/src/tool/executor/permission-flow.ts:41):先由权限服务结合项目规则给出结论,再叠加 PreToolUse 的决定,再过一道记忆文件的特殊规则(permission-flow.ts:86permission-flow.ts:93)。PreToolUse 的 allow 可以免掉一次普通确认,但抹不掉 alwaysAsk 声明出来的确认;它的 ask 则能把 allow 升级成确认(apps/zcode-cli/packages/core/src/tool/executor/hook-flow.ts:201hook-flow.ts:219)。结论是 ask 时,工具自己的 prepareApproval 只能把它收窄成放行或补一张预览,prepareApproval 抛错时照样询问、只是没有预览,不会变成静默放行(apps/zcode-cli/packages/core/src/tool/executor/approval-gate.ts:61)。随后发出 PermissionRequested,让客户端的应答与 PermissionRequest 钩子链竞速,先到者生效(permission-flow.ts:184)。注释记下了原来串行等待的后果:同步钩子阻塞期间确认窗已经渲染,应答却还没登记,用户每次点击都被静默丢弃,“确认窗永久死亡”(permission-flow.ts:179)。没有注入交互端口时执行器用默认的拒绝 broker,理由是 “No permission client configured”(impl.ts:26apps/zcode-cli/packages/core/src/permission/broker.ts:28)。

超时与取消

计时从权限通过之后才开始(call-runner.ts:348):

  const timeoutMs = resolveTimeoutMs(entry, executionInput, deps.defaultTimeoutMs, {
    model,
  });
  const executionAbortController = new AbortController();
  const unlinkParentAbort = linkAbortSignal(options?.signal, executionAbortController);
  // 可暂停的 deadline:本次调用内部的模型请求在准入闸门前排队时暂停计时。排队的两端
  // 以本 toolCallId 的 ModelNetworkStatus 会话事件到达,所以在事件出口拦一层即可,handler 无感。
  const deadline = new ToolDeadline(timeoutMs);
  const emitEvent =
    deps.emitEvent === undefined
      ? undefined
      : async (event: SessionEvent): Promise<void> => {
          observeToolAdmissionClock(event, canonicalToolCall.id, deadline);
          await deps.emitEvent(event);
        };

所以等用户审批的时间不算进工具超时。审批本身有没有时限,取决于 permissionTimeoutMs:这个字段在整个仓库里没有赋值处,core 层的审批等待因此不计时,只会随回合中止而结束(broker.ts:101)。

超时时长由 resolveTimeoutMs 决定(timeout.ts:181):

export function resolveTimeoutMs(
  entry: ToolEntry,
  input: unknown,
  defaultTimeoutMs: number,
  context?: ToolExecutionModelContext,
): number | undefined {
  const policy = entry.timeout;
  if (policy?.kind === "none") {
    return undefined;
  }

  const defaultMs = policy?.defaultMs ?? entry.metadata.timeoutMs ?? defaultTimeoutMs;
  const entryResolvedMs = entry.resolveTimeoutBudgetMs?.(input, context);
  const requestedMs =
    entryResolvedMs ??
    (policy?.allowCallOverride && isRecord(input) && typeof input.timeout_ms === "number"
      ? input.timeout_ms
      : policy?.allowCallOverride && isRecord(input) && typeof input.timeout === "number"
        ? input.timeout
        : defaultMs);
  const cappedMs = policy?.maxMs === undefined ? requestedMs : Math.min(requestedMs, policy.maxMs);
  const cleanupGraceMs = Math.max(0, Math.trunc(policy?.cleanupGraceMs ?? 0));
  return Math.max(1, Math.trunc(cappedMs)) + cleanupGraceMs;
}

优先级是:kind: "none" 不计时;工具自己的 resolveTimeoutBudgetMs;允许覆盖时读入参的 timeout_mstimeout;契约的 defaultMsmetadata.timeoutMs;最后才是执行器默认的 300000 毫秒(impl.ts:32)。结果不超过 maxMs,再加上清理宽限。宽限是给工具自己的超时和适配层清理留的余量:Bash 的命令超时由它自己执行,执行器的看门狗晚 6 秒才动手。常见工具的数字:

工具默认上限调用覆盖清理宽限出处
Bash120000600000timeout6000bash.ts:493
js30000120000timeout_ms2000apps/zcode-cli/packages/core/src/tool/handlers/node-repl.ts:399
Read30000;读 PDF 指定页时 150000150000apps/zcode-cli/packages/core/src/tool/handlers/read.ts:510apps/zcode-cli/packages/core/src/tool/handlers/read-pdf.ts:51
WebFetch6000060000apps/zcode-cli/packages/core/src/tool/handlers/webfetch.ts:239
WebSearch60000120000声明允许,但入参没有超时字段apps/zcode-cli/packages/contracts/src/tools/websearch.ts:147
MCP 工具服务描述里的 timeoutMs,缺省 30000apps/zcode-cli/packages/core/src/mcp/index.ts:124
Agent、TaskOutput、submit_resultescalate不计时apps/zcode-cli/packages/core/src/tool/handlers/agent.ts:267

其余内置工具多是 30000 毫秒,SendMessage、RespondToCoordinator、TaskStop 为 10000,ReadSessionContext 为 300000,EvalWorkflowSnippet 为 660000。

可暂停的 deadline。工具内部也会发模型请求(WebSearch 本身就是一次模型调用,WebFetch 要让模型回答问题),这些请求要在进程级的准入闸门前排队。ToolDeadline 在排队期间暂停计时,拿到放行再续,剩余时长守恒;注释说超时守的是“provider 挂了”,不是“我们自己的队列长”,否则限流时 WebSearch、WebFetch 会被逐个逼成超时,模型再补搜,越限流越吵(timeout.ts:13)。暂停与恢复的信号来自带本调用 ID 的 ModelNetworkStatus 事件(timeout.ts:81)。为了让每个工具内部请求都有这条事件,执行器交给 handler 的模型会套一层默认的状态出口;原先只有 WebSearch 自己设了,实测 18 次 WebFetch 排队 20 到 45 秒后按 60 秒超时被取消,错误里记的排队时长却是 0(apps/zcode-cli/packages/core/src/tool/executor/model-status-sink.ts:14)。超时错误的上下文里会带上累计排队时长 queuedMs,用来区分“慢”和“等”。

取消。每次调用有自己的 AbortController,与回合的中止信号相连(timeout.ts:206)。调用开始前回合已中止,直接返回取消结果(call-runner.ts:157);handler 运行中被中止,executeWithTimeout 立即以工具声明的 cancellation.userVisibleMessage 结束(timeout.ts:152);超时则先用超时错误中止 handler 的信号,再以 Tool execution timed out after <毫秒数>ms 结束(timeout.ts:133)。执行器不会等 handler 真正退出,清理靠 handler 响应中止信号自己完成,契约里的 cleanup 级别只被记进错误上下文,并不强制。取消和超时都会触发 PostToolUseFailure 钩子,前者带 isInterrupt: truehook-flow.ts:177)。

错误怎样变成给模型的结果

失败一律收口成 success: falseToolExecutionResult,由 createErrorResult 组装(apps/zcode-cli/packages/core/src/tool/executor/errors.ts:6)。它只为两类错误生成专门写给模型的 modelContent:首次参数校验失败,和工具以返回值表达的业务失败(包成 <tool_use_error>)。其余错误给模型的就是投影后的错误消息:沿 cause 链最多展开 12 层,取第一条不属于包装层的消息,空白压缩成单个空格,最长 500 个字符(apps/zcode-cli/packages/core/src/errors/error-payload.ts:216error-payload.ts:363)。空工具名、回合控制和钩子上下文则是之后另外补进结果的。常见情形:

情形模型看到的内容出处
工具不存在Tool not found: <name>call-runner.ts:128
首次参数校验失败<tool_use_error>InputValidationError: ...</tool_use_error>apps/zcode-cli/packages/core/src/tool/executor/validation.ts:71
工具自校验、解析或 handler 的业务失败<tool_use_error> 包着的 message,错误码另存errors.ts:20
PreToolUse 拒绝钩子给的理由,缺省为 Blocked by PreToolUse hookcall-runner.ts:246
权限拒绝权限服务或审批给出的理由permission-flow.ts:128
超时、取消Tool execution timed out after ...ms、工具的取消提示语timeout.ts:137timeout.ts:157
输出不符合 schemaTool output failed runtimeOutputSchema validation,不可恢复validation.ts:15
前一个结果要求停止回合合成的取消结果batch-runner.ts:16

写进历史时,失败结果会被标成错误结果;成功结果若输出里有 isError,或是被打断的 Bash,也按错误处理(apps/zcode-cli/packages/core/src/runtime/helpers/tool-result.ts:67)。钩子追加的上下文以 [Hook additional context] 开头、逐条编号接在结果后面(hook-flow.ts:234),失败结果和提前拒绝的结果也会带上,以免钩子明明给了诊断、模型却看不到(call-runner.ts:688)。

少数结果还会改变回合走向,由 turn-control.ts 统一挂上:

情形效果出处
stopTurnOnSuccess 的工具成功(submit_result本回合结束apps/zcode-cli/packages/core/src/tool/executor/turn-control.ts:152
Plan 模式下 ExitPlanMode 被拒且没有修改意见本回合结束,等用户继续讨论turn-control.ts:44
ExitPlanMode 或工作流确认被拒并附了修改意见给模型一句“未获批准”,意见作为真实用户消息引导进当前回合turn-control.ts:60turn-control.ts:107
CronCreate 撞上全局 20 个任务的上限换成固定提示,取消同批后续调用,回合只剩一次纯文本收尾turn-control.ts:14turn-control.ts:20

大结果:预算、截断与落盘

AGENTS.md 要求“大体积 tool 结果不应直接回灌模型上下文”(apps/zcode-cli/AGENTS.md:65),落实在 serializeOutputapps/zcode-cli/packages/core/src/tool/executor/result-serialization.ts:46)。它先用工具的 formatModelContent 把输出变成模型内容;空输出换成 (<工具名> completed with no output),免得模型把静默成功误读成结果缺失(result-serialization.ts:57)。之后按预算分三路:

  1. 字节数不超过 maxModelBytesmaxInlineBytes 中较小的那个(result-serialization.ts:78),原样交给模型。
  2. 超限且策略为 artifact:全文写进 artifact,模型只拿到一个信封,里面是原始大小、落盘路径和前 2000 个字符的预览(apps/zcode-cli/packages/core/src/tool/result-persistence-format.ts:5)。工具可以用 formatPersistedModelContent 换成自己的写法,Bash 和 TaskOutput 就是这样。
  3. 策略为 truncate,或落盘失败、没有 artifact 存储:按预览方向保留开头或结尾,再接一行 [Tool output truncated by resultBudget: ...] 说明原始字节数和上限(result-serialization.ts:200)。落盘失败不会把一次成功的调用变成失败(result-serialization.ts:322)。

通用信封的格式(result-persistence-format.ts:29):

export function formatPersistedOutputEnvelope(input: PersistedOutputEnvelopeInput): string {
  const preview = previewFirstChars(input.content, input.previewChars);
  return [
    PERSISTED_OUTPUT_OPEN_TAG,
    `Output too large (${input.formatBytes(input.originalBytes)}). Full output saved to: ${input.persistedPath}`,
    "",
    `Preview (first ${input.formatBytes(input.previewChars)}):`,
    preview.preview,
    preview.hasMore ? "..." : undefined,
    PERSISTED_OUTPUT_CLOSE_TAG,
  ]
    .filter((line): line is string => line !== undefined)
    .join("\n");
}

预览在 2000 个字符内找最后一个换行,换行落在后半段才在那里截断(result-persistence-format.ts:50)。

各工具的预算:

工具给模型的上限(字节)超限策略预览保留
未声明预算的条目100000截断开头
Bash30000落盘结尾
Read262144截断开头
Write、Edit100000截断开头
WebFetch、Glob100000落盘开头
Grep20000落盘开头
WebSearch10000截断开头
Agent120000落盘开头
js65536落盘结尾
TaskOutput、GetWorkflowRun400000,或超过 100000 个字符落盘开头
普通 MCP 工具50000截断开头

数字分别来自 result-serialization.ts:33bash.ts:480read.ts:501apps/zcode-cli/packages/core/src/tool/handlers/edit.ts:285webfetch.ts:226apps/zcode-cli/packages/core/src/tool/handlers/grep.ts:170websearch.ts:138agent.ts:254node-repl.ts:392apps/zcode-cli/packages/core/src/tool/handlers/task-output.ts:31mcp/index.ts:145。WebSearch 声明给模型 20000 字节,但它的 maxInlineBytes 只有 10000,取小者后实际上限是 10000。官方 Computer Use 的截图帧另有保护:图片块与紧随其后的坐标引用必须原样保留、不得截断或重排,文本仍受 256 KiB 的预算约束(result-serialization.ts:101)。

落盘到哪里。artifact 存储由 bootstrap 创建,根目录是存储目录下的 cli/artifactsapps/zcode-cli/packages/bootstrap/src/app/create-app.ts:354),存储目录默认 ~/.zcodeapps/zcode-cli/packages/contracts/src/config/index.ts:302)。每个会话一个子目录,文件名是调用 ID 加 tool-result- 前缀的 UUID,对外的 URI 形如 zcode-artifact://<会话>/<artifact ID>apps/zcode-cli/packages/adapters/src/storage/index.ts:68)。契约里的 retentionsessionprojecttemporary)会随写入请求传下去,但 Node 版存储并不读它,一律写进会话目录。成功结果里的图片、视频等媒体块在写入会话库之前也会各自落成 artifact,库里的工具记录只存附件引用(apps/zcode-cli/packages/core/src/runtime/helpers/tool-result-media-persistence.ts:22)。

结果的两个去向:模型与界面

同一个结果在执行器里投影两次。给模型的是 modelContent:经过预算、落盘信封和钩子上下文,回合方法把它作为工具结果写进下一次请求。给界面的是 ToolCallResult 事件(apps/zcode-cli/packages/core/src/tool/executor/events.ts:51),载荷包括序列化后的文本、display、性能数据,以及是否截断、原始与实际字节数、预算策略、落盘路径。

display 是一个判别联合,共 16 种(apps/zcode-cli/packages/contracts/src/tools/tool-result-metadata.ts:234),由 createToolResultDisplay 按工具名分派(apps/zcode-cli/packages/core/src/tool/executor/result-display.ts:94):Bash 的 bash_output、Write 与 Edit 的 file_diff、MCP 的 mcp_tool、Computer Use 的 cuanode_repl 的截图,子 Agent 与后台任务的四种,以及工作流相关的七种(含 ListModels)。display 不经过结果预算,每种自己限长:file_diff 最多 8 个 hunk、160 行(result-display.ts:37),bash_output 最多 150000 字节(apps/zcode-cli/packages/core/src/tool/executor/bash-result-display.ts:16),而且只在 Bash 输出被截断或落盘时才生成,因为给模型的信封会把正文再缩一遍,还藏起了截断的事实。

结果随后由回合方法写进会话:成功的工具记录保存文本输出和 display、序列化信息等元数据;失败记录除了面向界面的错误消息,还另存一份模型当时真正收到的 modelContent,冷恢复时才能精确重放(apps/zcode-cli/packages/core/src/runtime/methods/turn-tools.ts:334)。事件怎样投影成消息与 part,见会话事件流与持久化投影

执行事件与遥测

一次调用在会话事件流里留下的痕迹:

事件何时发出处
ToolCallScheduled调度后每个调用一条,带分组下标与整批的分组tools.ts:124
PermissionRequestedPermissionResolvedPermissionDenied权限流程events.ts:136events.ts:172events.ts:197
ToolCallStarted权限通过、handler 之前,带重算后的只读与副作用范围events.ts:20
ToolCallProgress由 handler 自己发,比如 Bash 的增量输出bash.ts:369
ModelNetworkStatus工具内部模型请求的排队、放行等状态model-status-sink.ts:21
ToolCallResultToolCallError成功或失败收口;查找失败与校验失败也会发 ToolCallErrorevents.ts:51events.ts:89
ToolBatchComplete每组结束,带成功与失败计数tools.ts:104
CheckpointCreated改文件的工具成功后记录工作区检查点,见检查点、回退与分叉tools.ts:169

ToolCallStarted 带上重算后的旗标是有用意的:动态工作流的 driver 据此在 handler 写下第一个字节之前就知道“这一笔要改工作区”,及时关掉导入缓存(call-runner.ts:327)。

遥测方面,每次调用开一个工具 span(apps/zcode-cli/packages/core/src/tool/executor/telemetry.ts:5)。模型编出来的未注册工具名不进远端 trace,统一记成 unknown,业务错误里仍保留真名供模型自修(call-runner.ts:77)。span 记录权限结论(无需、批准、拒绝)、输出字节数与是否截断,失败时记下阶段(查找、校验、权限、handler、序列化、PostToolUse)和错误类别(call-runner.ts:668)。执行器还汇总两项性能数据:从查找到 PostToolUse 的总耗时 totalMs,以及等审批的 permissionWaitMscall-runner.ts:489);它们随 ToolCallResult 进本地存储,不进模型可见的工具输出(apps/zcode-cli/packages/core/src/tool/types.ts:411)。日志事件是 tool.call.startedtool.call.completedtool.call.failedtool.call.not_found。遥测的采集与上报见遥测、调试与提示词轨迹

下一篇:读、写、改、搜——Read、Write、Edit 与搜索的实现:先读后写的约束、编辑匹配策略、文件状态跟踪与多媒体读取。

本页目录