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 entered
invocation.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 等前置拒绝均属此类);success:execute()正常完成;error:execute()已进入但执行失败;cancelled:执行前、执行中或执行后被取消。
谁写入、谁变成 unknown
设计明确规定:
CoreToolScheduler与 ACPSession.runTool总是写入该字段——它们是内置生产者的唯二入口;- 旧录制(older recordings)、第三方生产者、子代理结果投影(非交互
buildResponse路径,即重放另一个 Agent 报告的结果)可能缺失该字段; - 缺失值只在遥测边界(telemetry boundary)归一化为
unknown,且绝不从终端调用状态推断——例如终端error不会自动补一个error执行状态,因为前置拒绝与真实执行失败无法从终端状态区分。
终端状态与执行状态的正交关系
两条轴刻意保持独立,设计文档给出了完整的组合矩阵:
| Terminal status | Execution status | Example |
|---|---|---|
success | success | Normal tool completion |
success | not_started | Protocol-level synthetic sibling response |
error | any value | Pre-execution denial, execution error, post-processing error, or batch-hook override |
cancelled | any value | Cancellation before, during, or after execution |
按(terminal, execution)二元组逐行解读,唯一非法的组合是success/error与success/cancelled:一次以success终结的调用,执行状态只能是success或not_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_id与execution_status两个维度,且在任何 sink 之前只归一化一次。事件类型定义见 packages/core/src/telemetry/types.ts#L177-L266,其中call_id(第 180 行)与execution_status(第 187 行)均为可选字段;构造器在 types.ts#L224-L225 处直接取call.status与call.response.executionStatus,并保留success布尔字段以向后兼容。
归一化规则(设计文档原样列出,实现见ToolCallEvent构造器):
- 空工具名(empty tool names)→
unknown_tool; success由终端status重新计算(success = status === 'success',见 types.ts#L226);- 终端错误但缺少错误类型 → 使用
unknown; success与cancelled省略调用级错误字段(error/error_type只在error时携带);- 缺失的执行状态 →
unknown。
qwen-code.tool.execution.count与执行失败率
- 原有的
qwen-code.tool.call.count计数器(终端status维度,由终端遥测契约确立)保持不变,继续服务既有看板; - 新增计数器
qwen-code.tool.execution.count,只使用execution_status与tool_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_ID或telemetry.metrics.includeSessionId显式开启(见 metrics.ts#L72-L86 的注释)。
执行失败率(execution failure rate)定义:
execution_status = error ──────────────────────────────────────── execution_status in {success, error}分母只取{success, error},显式排除cancelled、not_started与unknown——取消和未执行都不算执行失败,这正是引入执行状态的核心价值。
该计数器有两个刻意的信息边界:
- 错误类型、函数名、call ID、消息与 MCP 服务器名留在日志或 Span 中,不进指标标签;
- 故意省略
function_name维度:仅凭指标无法把执行失败率归因到具体工具;需要下钻tool_call日志(同时携带call_id与function_name)定位具体工具。这是隐私与基数控制的权衡——若把函数名放进标签,指标基数将随工具集膨胀。
执行 Span:仅存在于execute()被尝试之后
- 执行 Span只在调度器尝试
execute()之后才存在;它记录工具身份(tool identity)、冻结的执行状态与执行错误类型; - 父工具 Span(parent tool span)继续代表终端调用状态;
- 被取消的 Span 保持未设置(UNSET)而非 error——取消不是错误,见 packages/core/src/telemetry/session-tracing.ts 中
setToolSpanCancelled对SpanStatusCode.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变体可以省略tool与invocation(coreToolScheduler.ts#L601-L609 中两者均为可选),消费方必须先守卫再使用; - 此类"解析前取消"经遥测发出时,
tool_type默认"native",因为工具身份尚未解析——这是tool_type维度在预校验取消场景的已知偏差(known skew),下游统计时需留意。
调度器错误语义的变化:单工具失败不牵连兄弟调用
设计文档明确改变了CoreToolScheduler.schedule()的错误语义:
- 逐调用执行错误不再 reject
schedule();结果通过既有的 update 与 completion 回调以终端error调用形式交付——一个工具失败不会中止其兄弟调用(siblings); - 方法仍返回
Promise<void>,且仍可因调度器级 setup 或队列失败而 reject; handleConfirmationResponse()在 rethrow 之前将确认流错误终态化(terminalize),既保留既有失败信号,又不让调用停留在awaiting_approval状态;- 嵌入方(embedders)应从回调交付的调用中读取终端
status与executionStatus,不要指望任一公开入口返回已完成的调用。
首发范围
第一版覆盖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 ("every
awaitin 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 检查)由CoreToolScheduler与Session.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),仅供参考