Qwen Code 工具执行状态(Tool Execution Status)设计解析:从终端状态到真实执行结果的精确度量
2026/9/12 17:11:24 网站建设 项目流程

Qwen Code 工具执行状态(Tool Execution Status)设计解析:从终端状态到真实执行结果的精确度量

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

导读

在 Qwen Code 这类终端 AI 编码代理中,工具调用(tool call)是 Agent 与文件系统、Shell、MCP 服务器交互的基本单元。传统的遥测体系只记录"这次工具调用最终成功、失败还是被取消"的终端状态,却无法回答一个更关键的问题:调度器是否真的进入了invocation.execute()执行体?参数校验失败、权限拒绝、执行失败与执行后处理失败在终端状态上都是error,但它们对排障、SLO 与模型行为的意义完全不同。本文基于 docs/design/2026-07-31-tool-execution-status.md 设计文档,结合仓库源码(Core 调度器、ACPSession.runTool、遥测指标与 Span 实现、测试用例),完整解析executionStatus契约的动机、语义、遥测落地方式与兼容性约束。读完你将理解:Qwen Code 如何用"终端状态 × 执行状态"双轴描述一次工具调用,如何据此构建不含取消与未执行噪音的执行失败率 SLI,以及埋点在CoreToolScheduler与 ACPSession.runTool中的具体实现与已知维护风险。

动机:终端状态无法回答"是否真的执行了"

设计文档开篇点明了问题的根源(2026-07-31-tool-execution-status.md):

The terminal tool-call status describes whether the overall call succeeded, failed, or was cancelled. It does not say whether the dispatcher actually enteredinvocation.execute().

终端的tool-call status只描述整次调用最终是成功、失败还是被取消,它不区分调度器是否真正进入了invocation.execute()。由此,以下四类本质不同的失败被混为一谈:

  • 校验失败(Validation failures):工具参数不符合声明式工具 schema,调用尚未进入执行体;
  • 权限拒绝(Permission rejection):被权限规则、hook、Plan 模式拦截,执行体从未运行;
  • 执行失败(Execution failures)execute()已进入但工具本身抛错或返回错误结果;
  • 执行后处理失败(Post-execution failures):工具执行成功,但后续的 hook、结果桥接、持久化等环节出错。

如果只依赖终端状态,这四类全部坍缩为error,无法被准确度量、归因与优化。因此设计引入独立的"执行结果(execution outcome)"维度,在终端状态之外单独记录执行层面的成败。

契约:executionStatus四态与双轴正交模型

字段与类型定义

ToolCallResponseInfo携带一个可选executionStatus字段,取值空间为四态联合类型。该类型定义在 Core 的 packages/core/src/core/turn.ts#L159-L163:

export type ToolExecutionStatus = | 'not_started' | 'success' | 'error' | 'cancelled';

ToolCallResponseInfo本身在 turn.ts#L165-L179 中声明,其中executionStatus?: ToolExecutionStatus为可选字段,以保持与旧录制(JSONL recording)、第三方生产者及子代理结果投影的源码与录制兼容性。

字段语义:

  • not_started:调度器从未进入invocation.execute()(校验失败、权限拒绝、计划模式拦截、重复 callId 等前置拒绝均属此类);
  • successexecute()正常完成;
  • errorexecute()已进入但执行失败;
  • cancelled:执行前、执行中或执行后被取消。

谁写入、谁变成 unknown

设计明确规定:

  • CoreToolScheduler与 ACPSession.runTool总是写入该字段——它们是内置生产者的唯二入口;
  • 旧录制(older recordings)、第三方生产者、子代理结果投影(非交互buildResponse路径,即重放另一个 Agent 报告的结果)可能缺失该字段;
  • 缺失值只在遥测边界(telemetry boundary)归一化为unknown,且绝不从终端调用状态推断——例如终端error不会自动补一个error执行状态,因为前置拒绝与真实执行失败无法从终端状态区分。

终端状态与执行状态的正交关系

两条轴刻意保持独立,设计文档给出了完整的组合矩阵:

Terminal statusExecution statusExample
successsuccessNormal tool completion
successnot_startedProtocol-level synthetic sibling response
errorany valuePre-execution denial, execution error, post-processing error, or batch-hook override
cancelledany valueCancellation before, during, or after execution

按(terminal, execution)二元组逐行解读,唯一非法的组合是success/errorsuccess/cancelled:一次以success终结的调用,执行状态只能是successnot_started。例如"协议层合成的兄弟响应"(synthetic sibling response)以终端success返回但从未执行,对应success/not_started;而执行失败、批量 hook 覆盖等一律以终端error呈现,此时执行状态可以是任意值。

执行状态冻结时机与批量快照

  • 冻结时机:执行状态在invocation.execute()落定(settle)时冻结,之后的 hooks、结果桥接(result bridging)、持久化(persistence)、批量处理(batch processing)均不能覆盖它。这意味着executionStatus精确反映执行体本身的成败,不受下游处理影响;
  • 批量快照PostToolBatch的启用状态及其父工具 Span 在调度器某批(batch)启动时快照(snapshot)。运行期重新配置 hooks 只影响下一批,不会改变在飞批次的完成行为,避免"边跑边改配置导致同一批内行为不一致"。

遥测落地:事件归一化、执行计数器与执行 Span

tool_call事件的归一化规则

归一化后的tool_call事件新增call_idexecution_status两个维度,且在任何 sink 之前只归一化一次。事件类型定义见 packages/core/src/telemetry/types.ts#L177-L266,其中call_id(第 180 行)与execution_status(第 187 行)均为可选字段;构造器在 types.ts#L224-L225 处直接取call.statuscall.response.executionStatus,并保留success布尔字段以向后兼容。

归一化规则(设计文档原样列出,实现见ToolCallEvent构造器):

  • 空工具名(empty tool names)→unknown_tool
  • success由终端status重新计算(success = status === 'success',见 types.ts#L226);
  • 终端错误但缺少错误类型 → 使用unknown
  • successcancelled省略调用级错误字段(error/error_type只在error时携带);
  • 缺失的执行状态 →unknown

qwen-code.tool.execution.count与执行失败率

  • 原有的qwen-code.tool.call.count计数器(终端status维度,由终端遥测契约确立)保持不变,继续服务既有看板;
  • 新增计数器qwen-code.tool.execution.count只使用execution_statustool_type两个事件级维度。其指标定义见 packages/core/src/telemetry/metrics.ts#L102-L110:
[TOOL_EXECUTION_COUNT]: { description: 'Counts tool execution outcomes.', valueType: ValueType.INT, assign: (c: Counter) => (toolExecutionCounter = c), attributes: {} as { execution_status: ToolExecutionStatus | 'unknown'; tool_type: 'native' | 'mcp'; }, },
  • 全局配置的公共指标属性(如 opt-in 的session.id)也可能附加出现。session.id默认不附加,因为每个 session 都是新值,会产生无界的时间序列扇出;需要通过QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_IDtelemetry.metrics.includeSessionId显式开启(见 metrics.ts#L72-L86 的注释)。

执行失败率(execution failure rate)定义

execution_status = error ──────────────────────────────────────── execution_status in {success, error}

分母只取{success, error}显式排除cancellednot_startedunknown——取消和未执行都不算执行失败,这正是引入执行状态的核心价值。

该计数器有两个刻意的信息边界:

  1. 错误类型、函数名、call ID、消息与 MCP 服务器名留在日志或 Span 中,不进指标标签
  2. 故意省略function_name维度:仅凭指标无法把执行失败率归因到具体工具;需要下钻tool_call日志(同时携带call_idfunction_name)定位具体工具。这是隐私与基数控制的权衡——若把函数名放进标签,指标基数将随工具集膨胀。

执行 Span:仅存在于execute()被尝试之后

  • 执行 Span只在调度器尝试execute()之后才存在;它记录工具身份(tool identity)、冻结的执行状态与执行错误类型;
  • 父工具 Span(parent tool span)继续代表终端调用状态;
  • 被取消的 Span 保持未设置(UNSET)而非 error——取消不是错误,见 packages/core/src/telemetry/session-tracing.ts 中setToolSpanCancelledSpanStatusCode.UNSET的使用;
  • Core 在工具解析与调用校验之后才打开父 Span;更早的终端路径(如解析前被拒)由归一化事件与执行计数器覆盖,不会从未解析的请求名合成 Span(避免为根本未执行的调用伪造可观测对象)。

实现上,执行 Span 的启停由startToolExecutionSpan/endToolExecutionSpan完成(session-tracing.ts#L1241 与 session-tracing.ts#L1302),endToolExecutionSpan在 session-tracing.ts#L1337-L1338 处将execution_status写入结束属性,并根据执行状态决定错误/成功语义。

QwenLogger 的字段边界

QwenLogger接收归一化后的终端状态、执行状态、call ID 与工具类型,但不接收 MCP 服务器名与函数参数。MCP 服务器名保持在 QwenLogger 之外,仅提供给已配置的遥测日志与 Span 导出器(exporter)。这延续了隐私优先的设计:日志系统不沉淀工具入参与服务器拓扑信息。

兼容性与范围:可选字段、JSONL 与调度器行为变化

字段可选性与内部必填形状

  • 公开的 response 与事件字段保持可选;内置生产者(CoreToolScheduler、ACPSession.runTool)内部使用必填形状。例如CoreToolScheduler内部定义CoreToolCallResponseInfo = ToolCallResponseInfo & { executionStatus: ToolExecutionStatus }(packages/core/src/core/coreToolScheduler.ts#L550-L552),强制内置路径必须携带执行状态;
  • 旧 JSONL 录制不迁移、不回溯填充;新录制在工具结果中写入executionStatus,字段是加性的(additive),忽略未知字段的回放读取器(replay readers)不受影响;
  • Core、ACP、TUI 与非交互模式的手动录制投影(manual recording projections)拷贝该标量,但不在面向用户的 JSON 输出中暴露——用户可见输出保持稳定。

CancelledToolCall的可选字段与工具类型偏差

  • 在工具解析(tool resolution)之前被取消的调用,其公开的CancelledToolCall变体可以省略toolinvocation(coreToolScheduler.ts#L601-L609 中两者均为可选),消费方必须先守卫再使用;
  • 此类"解析前取消"经遥测发出时,tool_type默认"native",因为工具身份尚未解析——这是tool_type维度在预校验取消场景的已知偏差(known skew),下游统计时需留意。

调度器错误语义的变化:单工具失败不牵连兄弟调用

设计文档明确改变了CoreToolScheduler.schedule()的错误语义:

  • 逐调用执行错误不再 rejectschedule();结果通过既有的 update 与 completion 回调以终端error调用形式交付——一个工具失败不会中止其兄弟调用(siblings)
  • 方法仍返回Promise<void>,且仍可因调度器级 setup 或队列失败而 reject;
  • handleConfirmationResponse()在 rethrow 之前将确认流错误终态化(terminalize),既保留既有失败信号,又不让调用停留在awaiting_approval状态;
  • 嵌入方(embedders)应从回调交付的调用中读取终端statusexecutionStatus,不要指望任一公开入口返回已完成的调用。

首发范围

第一版覆盖CoreToolScheduler与 ACPSession.runTool。以下内容明确不在范围内:推测执行(speculation)、直接/fork执行、MCP 内部重试、临时子代理结果对账(provisional subagent result reconciliation)、Shell 退出元数据、重试性(retryability)、所有权(ownership)与通用失败阶段。

发布与看板切换要求

  • Core 与 ACP 必须一起发布(ship together)——两者同时写入该字段,避免新旧混合部署产生不一致;
  • 看板应在部署时间或service.version切换;
  • 单独监控unknown(缺失值比例是数据质量信号);
  • 绝不要用旧的success指标作为执行失败 SLI——它是终端语义,无法区分执行失败与前置拒绝。

源码印证:两处生产者的实现细节

Core 调度器:写入、默认值与执行状态流转

CoreToolScheduler是 Core 侧唯一生产者。从源码可见其执行状态流转:

  • 初始默认let executionStatus: ToolExecutionStatus = 'not_started'(coreToolScheduler.ts#L5170);
  • 执行体捕获异常时置为'error'(coreToolScheduler.ts#L5266、#L5309);
  • 取消(abort)路径判定executionStatus = aborted ? 'cancelled' : 'error'(coreToolScheduler.ts#L6307),并在异常类型缺失时用exceptionErrorType补充错误类型(#L6328);
  • 执行状态写入响应后随回调交付,同时在日志调用处透传execution_status: response.executionStatus(coreToolScheduler.ts#L1120);
  • 无执行状态的历史响应在重建错误响应时回退'not_started'(coreToolScheduler.ts#L1294-L1305)。

此外,重复 provider callId 的防御路径通过createNotStartedToolErrorResponse(turn.ts#L255-L276)生成executionStatus: 'not_started'的响应——重复调用被拒绝时从未执行,语义准确。

ACPSession.runTool:同一契约的第二生产端

ACP 集成侧 packages/cli/src/acp-integration/session/Session.ts 同样强制内部元数据携带executionStatus(第 720-722 行类型约束),并在录制投影(第 11438 行)、缺失回退'not_started'(第 11652 行)、执行状态从'not_started'起始(第 12097 行)以及执行错误/取消判定(第 13787、13905 行)等位置与 Core 保持一致,确保跨层语义统一。

测试验证:四态全覆盖

packages/core/src/core/coreToolScheduler.test.ts 对执行状态进行了系统性断言,四态均有覆盖:

  • 'success':正常完成(第 2261、2325、6587、12788 行等);
  • 'error':执行失败(第 2316、2378、12826、13473、13614 行等);
  • 'cancelled':取消(第 13508、13528、13556 行等);
  • 'not_started':权限拒绝(第 11553 行)、取消于解析前(第 9756、11624 行)、批量中未执行(第 4206、4265、4316 行)、重复 callId 拒绝(第 4777 行)等前置拒绝场景。

这些断言直接印证了设计文档的核心主张:前置拒绝一律not_started,执行失败才error,两者在终端上都是error却能在执行轴区分。

已知维护风险:手埋的"解析前取消"不变量

设计文档在末尾坦承了最重要的维护隐患:

The pre-execution cancellation invariant ("everyawaitin the pre-execution path is followed by an abort check") is enforced by hand-placed checks at each call site inCoreToolSchedulerandSession.runToolrather than by a structural mechanism.

"执行前取消不变量"(每个执行前路径的await后必须跟随一次 abort 检查)由CoreToolSchedulerSession.runTool各调用点手埋检查保障,而非结构性机制。后果是:任一路径新增一个没有后续检查的await,会静默重新引入本设计修复的"过期执行"(stale-execution)缺陷——取消信号到达后旧代码路径仍继续执行。设计文档给出的演进方向是:未来重构应将await包裹进带守卫的 helper;在此之前,这两个路径的评审者必须手动核对不变量

小结

executionStatus是 Qwen Code 遥测体系中一次"语义精确化"的设计:它把一次工具调用拆成终端状态(调用整体结局)与执行状态execute()是否真正发生、结果如何)两条正交轴,使校验拒绝、权限拒绝、执行失败、执行后失败从混同的error中分离出来,从而构建出干净的执行失败率 SLI。其落地遵循严格的边界纪律:字段可选、内置生产者必填、缺失值仅在遥测边界归unknown、执行状态在execute()落定时冻结、执行 Span 只在实际执行后存在、指标故意省略function_name以控制基数。若你正在为 Agent 工具调用体系设计可观测性,本文的契约矩阵、归一化规则与失败率定义可直接作为参考蓝本;若你要为 Qwen Code 贡献代码,请务必遵守解析前取消不变量与"Core/ACP 同步发布"的约束。

延伸阅读:本文的设计源头文档 docs/design/2026-07-31-tool-execution-status.md;类型与响应契约见 packages/core/src/core/turn.ts;调度器实现见 packages/core/src/core/coreToolScheduler.ts;ACP 生产者见 packages/cli/src/acp-integration/session/Session.ts;指标与事件定义见 packages/core/src/telemetry/metrics.ts 与 packages/core/src/telemetry/types.ts;Span 实现见 packages/core/src/telemetry/session-tracing.ts;行为验证见 packages/core/src/core/coreToolScheduler.test.ts。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询