Skip to content

feat(core): preserve prompt cache for deferred tools - #10410

Open
DragonnZhang wants to merge 20 commits into
QwenLM:mainfrom
DragonnZhang:dragon/deferred-tool-call-bridge
Open

feat(core): preserve prompt cache for deferred tools#10410
DragonnZhang wants to merge 20 commits into
QwenLM:mainfrom
DragonnZhang:dragon/deferred-tool-call-bridge

Conversation

@DragonnZhang

@DragonnZhang DragonnZhang commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR replaces deferred-tool schema revelation with a stable two-step bridge. tool_search lets the model review a deferred tool's schema without changing the declared tool list, and tool_call validates and invokes that deferred tool through the existing execution pipeline.

The bridge works consistently across interactive scheduling, headless mode, ACP sessions, permissions, approvals, hooks, concurrency, telemetry, subagent restrictions, and lazy tool loading. Model-facing requests and responses retain the stable bridge identity while policy and execution use the underlying target identity. If tool search is disabled or either bridge is unavailable, deferred tools are declared eagerly as a safe fallback.

Configuration, SDK types, generated settings, UI labels, tests, and design documentation are updated to describe the new behavior.

Why it's needed

The previous deferred-tool flow revealed schemas by mutating the model's declared tool list after tool_search. That changes the prompt prefix and breaks prompt-cache reuse precisely when deferred tools are discovered, increasing latency and token cost and creating inconsistent behavior across execution paths.

This is a clean, independent reimplementation of the bridge idea, inspired by Hermes Agent's tool_search followed by tool_call flow. It supersedes the earlier implementation in #8276 without reusing its code.

Reviewer Test Plan

How to verify

  1. Configure enough MCP tools to trigger deferral with tool search enabled, then ask the model to use a deferred tool. Confirm that it first reviews the schema through tool_search, invokes it through tool_call, and never adds the deferred schema to later model requests.
  2. Compare the declared tools before and after discovery. Confirm that the tool_search and tool_call declarations remain byte-stable and that the rest of the declared tool list does not change.
  3. Exercise target permissions and restrictions through the bridge. Explicit deny rules must win, allowed targets must execute normally, recursive bridge targets and visible non-deferred targets must be rejected, and lazy-loading failures must be returned as tool errors.
  4. Repeat a deferred invocation through headless mode and an ACP session. Confirm that target concurrency, progress, completion, approvals, hooks, and telemetry use the underlying target while the model-facing function response remains tool_call with the original call ID.
  5. Disable tool search, or make either bridge unavailable. Confirm that deferred tools are declared eagerly instead of becoming unreachable.

Local verification completed: a live development-CLI smoke test used a disposable stdio MCP server with the tool forced behind the bridge and observed tool_searchtool_call → one underlying MCP invocation, the exact expected result, and prompt-cache reads on both continuation turns. In addition, 12 core test files passed with 2,281 tests; 4 CLI/ACP/headless test files passed with 1,229 tests and 1 skipped; npm run build, npm run typecheck, npm run lint, and git diff --check all passed.

Evidence (Before & After)

N/A — this changes tool routing and model request structure rather than user-visible TUI output.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Local workspace on macOS with Node.js 22+, a live development-CLI run against a disposable stdio MCP server, package-level Vitest suites, and monorepo build, typecheck, and lint checks.

Risk & Scope

  • Main risk or tradeoff: Deferred calls now pass through a common resolver before scheduling, so a routing mistake could affect permissions, execution identity, or reporting across multiple frontends. The affected paths have focused regression coverage.
  • Not validated / out of scope: The raw provider request body was not captured for a byte-for-byte live declaration comparison, and Windows/Linux were not tested locally.
  • Breaking changes / migration notes: No public breaking change. With tools.toolSearch.enabled set to false, both bridge tools are disabled and deferred tool schemas are declared eagerly.

Linked Issues

Supersedes #8276.

中文说明

这个 PR 做了什么

这个 PR 用一个稳定的两阶段桥接流程替换了 deferred tool 的动态 schema reveal。tool_search 让模型查看 deferred tool 的 schema,但不会改变已声明的工具列表;tool_call 则负责校验目标,并通过现有执行链路调用该 deferred tool。

这套桥接在交互式调度、headless 模式、ACP session、权限、审批、hooks、并发、遥测、subagent 限制和 lazy tool 加载中保持一致。模型侧请求和响应保留稳定的桥接工具身份,而策略判断和实际执行使用底层目标工具身份。如果 tool search 被禁用,或者任一桥接工具不可用,deferred tools 会被直接声明,作为安全回退。

配置、SDK 类型、生成的 settings、UI 文案、测试和设计文档也一并更新,以说明新的行为。

为什么需要它

之前的 deferred-tool 流程会在 tool_search 之后通过修改模型已声明的工具列表来 reveal schema。这样会改变提示词前缀,恰好在发现 deferred tools 时破坏提示词缓存复用,从而增加延迟和 token 成本,并让不同执行路径的行为不一致。

这是对该桥接思路的一次干净、独立的重新实现,流程参考了 Hermes Agent 的 tool_search 后接 tool_call 设计。它取代了 #8276 中较早的实现,但没有复用其中的代码。

Reviewer 测试计划

如何验证

  1. 配置足够多的 MCP tools 以触发 defer,并启用 tool search,然后让模型使用一个 deferred tool。确认模型先通过 tool_search 查看 schema,再通过 tool_call 调用,并且后续模型请求中不会新增该 deferred schema。
  2. 对比发现工具前后的已声明工具。确认 tool_searchtool_call 的声明保持字节级稳定,其他已声明工具列表也不发生变化。
  3. 通过桥接测试目标工具的权限和限制。显式 deny 规则必须优先,允许的目标应正常执行,递归调用桥接工具和调用非 deferred 的可见工具应被拒绝,lazy loading 失败应以工具错误返回。
  4. 分别通过 headless 模式和 ACP session 调用 deferred tool。确认并发、进度、完成状态、审批、hooks 和遥测使用底层目标工具身份,而模型侧 function response 仍是 tool_call,并保留原始 call ID。
  5. 禁用 tool search,或者让任一桥接工具不可用。确认 deferred tools 会被直接声明,而不是变得无法调用。

本地验证已完成:使用一次性 stdio MCP server 运行了真实开发版 CLI 冒烟测试,将目标工具强制置于 bridge 后,实际观察到 tool_searchtool_call → 一次底层 MCP 调用、完全一致的预期结果,以及两个 continuation turn 上的提示词缓存读取。此外,12 个 core 测试文件通过,共 2,281 个测试;4 个 CLI/ACP/headless 测试文件通过,共 1,229 个通过、1 个跳过;npm run buildnpm run typechecknpm run lintgit diff --check 均通过。

证据(Before & After)

不适用——这个改动影响的是工具路由和模型请求结构,不是用户可见的 TUI 输出。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS 本地 workspace,Node.js 22+,针对一次性 stdio MCP server 运行了真实开发版 CLI,并运行了 package 级 Vitest suites,以及 monorepo build、typecheck 和 lint 检查。

风险与范围

  • 主要风险或取舍:Deferred calls 现在会在调度前经过统一 resolver,因此路由错误可能影响多个 frontend 中的权限、执行身份或结果上报。相关路径已有针对性的回归测试覆盖。
  • 未验证 / 不在范围内:没有抓取原始 provider request body 来进行实时的工具声明字节级对比,本地也未测试 Windows/Linux。
  • Breaking changes / 迁移说明:没有公开 breaking change。当 tools.toolSearch.enabled 设为 false 时,两个桥接工具都会被禁用,deferred tool schemas 会被直接声明。

关联 Issue

取代 #8276

@DragonnZhang

DragonnZhang commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

E2E Test Report

Status: passed locally with a live stdio MCP server and the development CLI.

Environment:

  • macOS, Node.js 22+, local npm run dev entrypoint
  • Model: claude-opus-5
  • Tool Search enabled with threshold: 0 so the MCP tool was forced behind the bridge
  • A disposable stdio MCP server exposing one deterministic read-only tool

Observed live flow:

  1. The development CLI connected to the disposable MCP server.
  2. The model called tool_search with select:mcp__deferred-smoke__bridge_smoke_echo.
  3. After receiving the reviewed schema, the model called tool_call with the deferred target name and { "value": "cache-bridge-smoke-2026" }.
  4. The MCP server recorded exactly one underlying invocation and returned BRIDGE_SMOKE_OK:cache-bridge-smoke-2026.
  5. The model returned that exact result and the run completed successfully in three turns.
  6. Provider usage reported 39,321 cached input tokens on the tool_call turn and 39,594 cached input tokens on the final turn, confirming that prompt-cache reads continued across both bridge steps.

Additional verification:

  • Full npm run build passed after the commit.
  • 12 focused core test files passed with 2,281 tests.
  • 4 focused CLI/ACP/headless test files passed with 1,229 tests and 1 skipped.
  • npm run typecheck, npm run lint, and git diff --check passed.
  • Regression coverage exercises stable declarations, deferred target resolution, permissions, restrictions, lazy-loading errors, scheduler behavior, ACP, headless mode, and model-facing response identity.

The raw provider request body was not captured in this smoke run, so byte-for-byte declaration equality remains covered by automated tests rather than a live payload trace.

@DragonnZhang
DragonnZhang marked this pull request as ready for review August 28, 2026 13:51
@DragonnZhang
DragonnZhang enabled auto-merge August 28, 2026 13:51
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

…call-bridge

# Conflicts:
#	packages/core/src/skills/bundled/review/SKILL.md
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — gate review below.

  • Template: complete ✓
  • Problem: real and mechanical, not speculative — the current flow mutates the model's declared tool list on every tool_search reveal, which changes the request prefix and breaks prompt-cache reuse exactly at discovery time; the repo's own prompt-cache design doc lists ToolSearch reveals as a churn source. Magnitude is unmeasured (no captured request bodies), but the cache-invalidation mechanism is inherent to the existing design. This is a perf/cost improvement, not a bugfix, so no before/after reproduction is expected.
  • Direction: aligned. Deferred-tool handling and prompt-cache efficiency are an active area (claude-code's CHANGELOG has recent entries on reducing prompt-cache costs by reusing cached conversation prefixes, plus several deferred-tools fixes; no direct reference to a tool_call bridge, but the area is clearly relevant). For the record: this supersedes your own unmerged fix(core): preserve prompt cache across deferred tool discovery #8276, which you closed ~10 minutes before opening this one — reviewed on its own merits as a clean reimplementation.
  • Size: core infrastructure change (packages/core/src/tools/**, core/**, config/**, permissions, plus the CLI/ACP/headless consumers). Of the 2,256 changed lines: ~940 production logic, ~1,120 tests, ~191 docs, ~6 generated/schema. That clears the 500-production-line bar for core changes, so this is escalated for maintainer awareness — not blocking (it's a feat, and the tests-to-production ratio is healthy).
  • Approach: scope feels right. Keeping the declaration list frozen and routing schema review + invocation through two byte-stable bridge tools is the minimal design that solves this — eager declaration defeats the token savings, and stable sorting alone cannot keep the prefix from changing. Unwrapping at the scheduler under the target identity (while model-facing responses keep the bridge name and call id) preserves permissions, hooks, telemetry, and concurrency semantics across all three execution frontends. The eager-declaration fallback when either bridge half is unavailable is the right failure direction. One deliberate behavior change worth a reviewer's eye: DeepSeek models no longer auto-disable tool_search, since the bridge makes that eager-reveal workaround unnecessary.
  • Risk: packages/cli/src/acp-integration/session/Session.ts matches this repo's revert-correlated high-risk paths — not a blocker, but it means full CI evidence and full review depth are required before approval.

Also noting: the merge conflict with main (bundled review skill doc) was resolved by the merge commit that landed mid-review — I verified the resolution combines both sides correctly and leaves all bridge production code byte-identical.

Moving on to code review. 🔍

@chiga0 flagging for awareness: core change at ~940 production lines — final call will be deferred to a maintainer.

中文说明

门控审查结果如下。

  • 模板:完整 ✓
  • 问题:真实且是机制性的,不是理论猜测——现有流程每次 tool_search reveal 都会改写模型的已声明工具列表,从而改变请求前缀、恰好在发现时破坏 prompt-cache 复用;仓库自己的 prompt-cache 设计文档也把 ToolSearch reveal 列为 churn 来源。影响幅度没有量化(未抓请求体),但缓存失效机制是现有设计固有的。这是性能/成本改进而非 bug 修复,因此不要求 before/after 复现。
  • 方向:对齐。deferred tool 与 prompt-cache 效率是活跃方向(claude-code 的 CHANGELOG 近期有多条关于复用缓存前缀降低 prompt-cache 成本和 deferred-tools 修复的记录;没有直接提到 tool_call 桥,但该领域明显相关)。备注:本 PR 取代你自己未合并的 fix(core): preserve prompt cache across deferred tool discovery #8276(开本 PR 前约 10 分钟关闭),按干净的重新实现独立评审。
  • 规模:核心基础设施改动(packages/core/src/tools/**core/**config/**、权限,以及 CLI/ACP/headless 消费方)。共 2,256 行改动:约 940 行生产逻辑、约 1,120 行测试、约 191 行文档、约 6 行生成/schema。超过核心改动 500 生产行门槛,按规则升级给维护者知悉——不因此阻塞(feat 类型,测试与生产代码比例健康)。
  • 方案:范围合理。保持声明列表冻结、通过两个字节级稳定的桥工具完成 schema 审查与调用,是解决该问题的最小设计——直接声明会牺牲 token 节省,仅靠稳定排序无法阻止前缀变化。在调度层按目标工具身份解包(模型侧响应保留桥名称与 call id)能在三个执行前端保持一致的权限、hooks、遥测与并发语义。任一桥工具不可用时回退为直接声明,方向正确。一个有意的行为变更值得评审者留意:DeepSeek 模型不再自动禁用 tool_search,因为桥已使那个 eager-reveal 变通不再必要。
  • 风险packages/cli/src/acp-integration/session/Session.ts 命中本仓库与合并后回滚相关的高风险路径——不是阻塞项,但意味着批准前需要完整 CI 证据与完整审查深度。

另注:与 main 的合并冲突(bundled review skill 文档)已由评审过程中推送的 merge commit 解决——已核实该解决正确合并了双方内容,且桥的全部生产代码逐字节未变。

进入代码审查 🔍

@chiga0 提请关注:约 940 行生产代码的核心改动,最终结论将交由维护者定夺。

Qwen Code · qwen3.8-max

Reviewed at 4883931d0d223011f60fd9c04da9a3d72560f76e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Code review

My independent take before reading the diff: the only design that keeps the prefix stable while preserving deferral's token savings is a frozen declaration list plus two fixed bridge tools — reveal schemas as tool output, wrap invocation in an executor tool, unwrap at the scheduler under the target identity. That is exactly what this PR does, and I did not find a simpler path it missed.

No critical blockers in the ~940 production lines. The fail-closed rails check out:

  • Resolver (tool-call.ts): rejects bridge recursion, visible/non-deferred targets, and unknown targets; lazy-load factory failures surface as tool errors; arguments are structuredCloned; ToolCallInvocation.execute() refuses direct execution outside the scheduler.
  • Permissions: the scheduler checks enablement for tool_call itself and the legacy no-PermissionManager deny path keeps the envelope wrapped so the normal reject fires; after unwrap, deny/ask rules, hooks (PreToolUse receives the target name and target args — pinned by test), approvals, concurrency, and telemetry all see the target identity. autoMode's safe-allowlist explicitly excludes the bridge (test pins it).
  • Model-facing identity: success, error, cancellation, and timeout responses all keep the tool_call name with the original call id across the scheduler, ACP executor, and agent-core events.
  • Fallbacks: either bridge half missing (deny rule, --exclude-tools, tools.toolSearch.enabled: false) → deferred schemas are declared eagerly rather than becoming unreachable; both halves are deny-pushed together.
  • Subagents: plan-lifecycle and leader-only policies are enforced in the resolver; closed-list agents (review-agent family) exclude both bridge halves; ordinary subagents inherit deferred tools as before.
  • No convention violations spotted (kebab-case files, collocated tests, cross-package import via the core export).

Non-blocking notes:

  • Deliberate behavior change: DeepSeek models no longer auto-disable tool_search (the bridge replaces the eager-reveal workaround). Tests are updated accordingly, but a DeepSeek user's session changes shape — worth a maintainer's nod.
  • The model can still emit a direct call to a hidden deferred tool; it gets a clear error pointing at the bridge. Fine, but model compliance is the one thing tests can't pin.
sequenceDiagram
    participant P1 as Model
    participant P2 as tool_search
    participant P3 as CoreToolScheduler
    participant P4 as ToolRegistry
    participant P5 as PermissionManager
    participant P6 as target deferred tool
    P1->>P2: query or select name (review schema)
    P2->>P4: ensureTool (load only, no reveal)
    P2-->>P1: schema in functions block, declaration list unchanged
    P1->>P3: tool_call envelope with name and arguments
    P3->>P5: is tool_call enabled
    P3->>P4: resolve target, reject recursion visible or unknown
    P3->>P5: permissions, hooks, approvals under target identity
    P3->>P6: execute
    P6-->>P3: result
    P3-->>P1: function response keeps tool_call name and original call id
Loading
Files changed (30 of 58 shown)
File What changed
packages/core/src/tools/tool-call.ts new bridge tool plus resolver, the security core of the change
packages/core/src/tools/tool-search.ts reveal and setTools sync removed, now returns schemas only
packages/core/src/core/coreToolScheduler.ts unwraps the bridge at schedule time, keeps model-facing name on every response path
packages/core/src/agents/runtime/agent-core.ts defers TOOL_CALL events and preToolUse hooks until bridge resolution
packages/core/src/core/client.ts eager-reveal fallback when either bridge half is missing
packages/core/src/tools/tool-registry.ts reveal semantics narrowed to session setup, comments refreshed
packages/core/src/permissions/permission-manager.ts tool_call joins the tool_search allowlist exemption, deny still wins
packages/core/src/permissions/rule-parser.ts tool_call name aliases
packages/core/src/subagents/builtin-agents.ts closed-list agents also exclude tool_call
packages/cli/src/acp-integration/session/Session.ts ACP executor unwraps the bridge with its per-call policies
packages/cli/src/nonInteractiveCli.ts headless concurrency, progress, completion use the target identity
packages/cli/src/config/config.ts explicit opt-out only, DeepSeek auto-disable removed
packages/cli/src/config/settingsSchema.ts setting descriptions updated
packages/core/src/core/environmentContext.ts deferred-tools reminder describes the two-step flow
packages/core/src/tools/tool-names.ts tool_call name constant
packages/core/src/index.ts exports the resolver and types
packages/core/src/tools/tools.ts shouldDefer doc updated
packages/core/src/skills/bundled/review/SKILL.md wording for reaching report_findings via the bridge, merge conflict resolved correctly
docs/design/deferred-tool-call-bridge.md new design doc
packages/core/src/core/coreToolScheduler.test.ts +314 lines: routing, deny, timeout, abort paths
packages/core/src/tools/tool-call.test.ts resolver unit tests
packages/core/src/tools/tool-search.test.ts behavior flipped: reviewing no longer reveals
packages/core/src/core/client.test.ts eager-reveal fallback cases incl. tool_call-only absence
packages/cli/src/nonInteractiveCli.test.ts headless target-identity concurrency test
packages/cli/src/acp-integration/session/Session.test.ts ACP bridge coverage
packages/core/src/permissions/permission-manager.test.ts exemption and deny-wins coverage
packages/core/src/agents/runtime/agent-core.test.ts event identity for bridged calls
packages/sdk-typescript/src/types/types.ts + README wording: reachable through tool_search + tool_call
packages/web-shell/client/i18n.tsx + toolFormatting.ts ToolCall display names
packages/cli/src/i18n/locales/*.js ToolCall display names in four locales
…and 28 more files docs, generated schema, remaining test updates

Testing — the PR's own CI, read via the API

Unattended run — PR code is never built or executed here. Evidence below is the PR's own CI on the reviewed commit, fetched once via the API; pending checks are reported as pending, not polled. The author's self-reported local results (dev-CLI smoke test with a stdio MCP server, 2,281 + 1,229 unit tests on macOS) are their claim, not independently re-run.

The unit suite Test (ubuntu-latest, Node 22.x) is still running on this commit. Test (macos/windows-latest) are skipped — same on recently merged upstream PRs (checked #10402), so that is this repo's normal matrix behavior, not caused by this PR. No failures so far; the finalize job updates the table below once CI settles.

Final CI results for 4883931 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
Secret scan (TruffleHog) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle the central claim: @qwen-code /verify — that the declared tool list stays byte-stable across a deferred-tool discovery and the prompt-cache prefix keeps hitting is a wire-format claim no unit suite can prove, and the only live evidence so far is the author's own macOS smoke test. As a sponsored run (the author lacks write access): a maintainer's @qwen-code /verify approves the head it is written against, the run carries a pre-execution risk screen and a full workspace wipe, and its report should be read with the same skepticism as the fork's own CI logs. Not verified here: live declaration stability, Windows/Linux behavior.

Real-scenario (tmux) testing: not attempted — unattended CI run; the live-behavior signal comes from the sandboxed lane above.

中文说明

代码审查:我在读 diff 前的独立方案就是"冻结声明列表 + 两个固定桥工具",与本 PR 一致,且没找到被它遗漏的更简路径。约 940 行生产代码中未发现阻塞性问题:解析器对各种非法目标(递归、可见工具、未知工具、工厂失败)均失败关闭;权限、hooks、审批、并发、遥测在解包后全部使用目标工具身份(测试钉住了 PreToolUse 收到目标名与目标参数);模型侧响应(成功/错误/取消/超时)在调度器、ACP、headless 三条路径上都保留 tool_call 名称与原 call id;任一桥工具缺失时回退为直接声明,不会让工具失联。

非阻塞提醒:DeepSeek 模型不再自动禁用 tool_search(有意的行为变更,测试已更新);模型仍可能直接调用隐藏的 deferred 工具,会收到指向桥的清晰报错——模型遵从性是测试无法钉住的部分。

测试:无人值守运行,不构建/执行 PR 代码。以上证据来自所审提交的 PR 自身 CI(API 一次性读取,不轮询)。单测套件仍在运行中;macOS/Windows 矩阵腿 skipped 为仓库常态(对照已合并的 #10402 相同)。作者自报的本地验证(macOS 冒烟测试与单测数字)是其声明,未独立复现。

沙箱验证可以落定核心主张:@qwen-code /verify(赞助运行,维护者触发,含执行前风险筛查与工作区清理;报告应像对待 fork CI 日志一样审慎阅读)——声明列表在发现过程中字节级稳定、prompt-cache 前缀持续命中是请求体层面的主张,单测无法证明。此处未验证:真实会话中的声明稳定性、Windows/Linux 行为。

Qwen Code · qwen3.8-max

Reviewed at 4883931d0d223011f60fd9c04da9a3d72560f76e · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review on substance, but this is a ~940-production-line core change from a fork with a model-facing contract shift, and policy puts that final call in a maintainer's hands rather than mine.

Stepping back: this is the design I would have written, and the execution matches it. The frozen-declaration-list approach is the only one that keeps both the prompt-cache prefix and deferral's token savings; the unwrap-at-the-scheduler split (target identity for policy and execution, bridge identity for the model) is carried consistently through all three frontends; the failure direction is right everywhere I probed (recursion, visible targets, deny rules, missing bridge halves, aborts mid-resolution). Test coverage is genuine — it pins the semantics (PreToolUse sees the target name, responses keep the bridge name, deny wins over the exemption) rather than the implementation. The author also handled the mid-review merge with main cleanly. In six months this reads like code written by someone who understands the machinery, not code trying to impress it.

What keeps this at 3/5 is not any defect I found — it's what triage cannot settle:

  • The headline claim (declaration list byte-stable across discovery, cache keeps hitting) is a wire-format property; the only live evidence is the author's own macOS smoke test. @qwen-code /verify is the lane named in the review comment.
  • The DeepSeek auto-disable removal is a deliberate session-shape change for real users that deserves a maintainer's sign-off, not a bot's.
  • Model compliance with the two-step flow (review via tool_search, invoke via tool_call) is the one runtime behavior no test can pin.

⏸️ Deferring to @chiga0 — core infrastructure change at this scale needs a human decision on direction and maintainability before merge. Nothing here is a request for changes; if the two questions above are answered, this looks ready. CI on the reviewed commit is still running (unit suite pending); the finalize job will update the test table in the review comment when it lands.

中文说明

总体判断:这是一次实质干净的评审——方案正是我会选的设计(冻结声明列表、调度层按目标身份解包),且在三个执行前端贯彻一致;失败方向处处正确(递归、可见目标、deny 规则、桥缺失、解包中被中止);测试钉住的是语义而非实现。作者还在评审过程中干净地处理了与 main 的合并。

保持 3/5 的不是缺陷,而是三件事:(1)核心主张(发现过程中声明列表字节级稳定、缓存持续命中)是请求体层面的性质,目前唯一的活体证据是作者自测,@qwen-code /verify 是已指名的验证通道;(2)移除 DeepSeek 自动禁用是有意的行为变更,应由维护者拍板;(3)模型对两步流程的遵从性无法用测试钉住。

⏸️ 转交 @chiga0 ——此规模的核心基础设施改动需要人类在方向与可维护性上做出决定后再合并。本评论不是要求修改;上述两点有答案后,此 PR 看起来即可就绪。所审提交的 CI 仍在运行(单测待定),finalize 任务会在结束后原地更新评审评论中的测试表格。

Qwen Code · qwen3.8-max

Reviewed at 4883931d0d223011f60fd9c04da9a3d72560f76e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 3a7b9fc. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 1 render-shaping file:

  • packages/web-shell/client/i18n.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 42 passed · 0 failed · 42 total

Flakiness gate: ⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:42 通过 · 0 失败 · 42 总计

抖动门:⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

PR #10410 Deep Verification — feat(core): preserve prompt cache for deferred tools

Verdict: merge-ready — 42/42 scripted assertions passed (34 wire-oracle, 6 mutation-matrix, 2 gates). Verified head: 4883931d0d223011f60fd9c04da9a3d72560f76e (merge commit ee904f951b, base d6533785bd). Two non-blocking findings (one test-precision suggestion, one description note) and one pre-existing-behavior observation.

中文摘要
  • 结论merge-ready。42/42 条脚本化断言全部通过,未发现阻塞性问题。
  • A/B 结论(见下表与 01-ab-declaration-stability-head-vs-base.png):中心主张成立且被证明是本次改动带来的——在 head 上,模型经 tool_search 审查 schema、再经 tool_call 调用 deferred MCP 工具的全过程中,三次模型请求的 tools 声明块逐字节相同,历史消息只追加不重写(提示词前缀稳定);在同场景的 base 对照组上,tool_search 之后第二次请求的声明块发生了变化(被发现的工具 schema 被加入),正是本 PR 要消除的缓存失效行为。回退路径(tools.toolSearch.enabled: false 时 eager 声明、桥接工具消失)与三条路由策略单元(递归桥接拒绝、可见工具拒绝、deny 规则按底层目标身份生效)全部符合声明。
  • Findings(均非阻塞):
    1. (建议)tool-call.test.ts 的递归拒绝用例只断言 errorType,删掉递归守卫后 tool_call 变体仍会经由相邻的可见性检查以相同 errorType 通过——建议同时断言错误消息。生产行为本身有双层防护,无缺陷。
    2. (说明)本 PR 移除了 deepseek 模型自动禁用 tool search 的启发式(有测试固定、属有意为之),但描述中 "Breaking changes" 一节写的是 "No public breaking change",建议明确提及该默认行为变化。
    3. (既有行为观察)对 MCP 工具的 deny 规则是调用期生效:tool_search 仍会返回被禁工具的 schema,调用时才以 "Matching deny rule" 拒绝。此语义非本 PR 引入(tool-registry.ts 的改动全部为注释)。
  • 未覆盖:逐提交归因(depth-2 shallow,仅 merge 提交可达);真实提供商侧的缓存命中率度量(只证明了前缀字节稳定这一必要条件);ACP/hooks/遥测/并发的活体 E2E(由 PR 自带单测覆盖并经变异矩阵固定);lazy 工厂失败路径仅单测覆盖;仓库级门禁未跑(只跑改动文件);Windows 行为;docs/i18n/web-shell 文案仅 diff 审阅。

Central claim + A/B

Central claim. Deferred tools are reviewed via tool_search and invoked via tool_call without ever mutating the model-facing declaration list — the prompt prefix (and thus prompt-cache reuse) stays stable across discovery and invocation, while base reveals the schema into the next request's tools array.

The A/B drove the compiled headless CLI (dist/cli.js) against a scripted mock OpenAI server and a real stdio MCP echo server (scratch git project, tools.toolSearch.threshold: 0 to keep the tool deferred), capturing every request body on the wire. Base control: a HEAD^1 worktree with its own npm ci + build + bundle; readlink -f node_modules/@qwen-code/qwen-code-core from that tree resolves into the base tree, tool_call is absent from the base build, and the two bundles differ behaviorally as expected — the control is clean.

cell build scripted scenario observable oracle result
head-bridge head tool_searchtool_call → final tools block byte-identical across requests 1/2/3; history append-only; result tied to the tool_call id; exactly 1 MCP invocation 9/9
base-reveal base tool_search → direct call → final CONTROL (expected mutation): request-2 tools block differs; revealed schema present; direct call used 7/7
head-fallback head direct call, toolSearch.enabled: false echo declared eagerly in request 1; no bridge tools declared 5/5
head-routing-recursive head tool_call targeting tool_call rejected: tool_call cannot invoke bridge tool "tool_call"; no MCP call 4/4
head-routing-visible head tool_call targeting eager read_file rejected: already visible to the model or is not deferred 4/4
head-routing-deny head tool_search + tool_call on a denied target denied at permission time citing Matching deny rule: "mcp__echo-server__echo" — i.e. evaluated against the underlying target identity; never invoked 5/5

Witness: 01-ab-declaration-stability-head-vs-base.png (cell table) and 02-wire-assertions-six-cells.png (all 34 assertions as printed). The flip is exactly the load-bearing shape: head 3/3 requests byte-stable, base mutates at discovery.

Reviewer Test Plan, per step: (1) deferred flow with no schema added — proven by head-bridge (H1–H3, H5–H7). (2) byte-stable declarations — H2/H3 exact JSON equality of the tools array across all three requests. (3) permissions — deny wins (D1–D3, at the underlying identity), allowed target executes (H7), recursive/visible targets rejected (R1–R3, V1–V3); lazy-load-failure path covered by unit tests only (see Not covered). (4) headless + ACP identity split — the E2E harness itself is the headless path (runNonInteractive), model-facing identity proven on the wire (H5/H6); the Gemini-side functionResponse.name = tool_call and ACP/concurrency/telemetry target-identity are pinned by the PR's unit tests, which this round mutation-proved non-vacuous (M2/M4/M5 below). (5) fallback eager declaration — F1–F4. No step was unexecutable.

Mutation matrix (vacuity of the new tests)

All runs in a scratch worktree at the merge commit; unmutated control green first (447/447 across the three core suites); each mutation restored afterward (final git status clean). Witness: 03-mutation-matrix-five-mutants.png; full log at logs/mutation-matrix.txt.

# mutation (file) suite outcome failure mode observed
none (control) tool-call + tool-search + coreToolScheduler 447/447 green
M1 re-add revealDeferredTool() in returnSchemas (tool-search.ts) — resurrects base behavior tool-search.test.ts KILLED 11/50 select: mode reviews a named tool without revealing itexpected true to be false; keeps function declarations stable… → declaration-list deep-equal mismatch
M2 resolveToolCallBridgeRequest returns request unchanged (coreToolScheduler.ts) coreToolScheduler.test.ts KILLED 3 red routing-identity spy never called; visible-target reject expected 'success' to be 'error'; timeout identity likewise
M3 recursion guard disabled (tool-call.ts) — positive control tool-call.test.ts KILLED 1/2 tool_search variant red; the tool_call variant survived via the sibling visibility check with the same errorType — see Findings
M4 getHeadlessExecutionRequest disabled (nonInteractiveCli.ts) nonInteractiveCli.test.ts KILLED uses deferred target identity for headless bridge concurrency… times out (batch deadlocks when classified under the bridge name)
M5 ACP bridge branch disabled (Session.ts) Session.test.ts (-t "routes tool_call…") KILLED expected "spy" to be called once, but got 0 times — underlying tool never invoked

No mutant regressed a pinned behavior; every load-bearing guard the PR introduces is pinned by a test that fails with the behavioral mismatch it exists to catch. M4's timeout is the behavioral mismatch itself: the test's gate only opens when both bridge calls run concurrently under the target's Read-kind identity.

Targeted gates

  • core (15 changed test files, incl. tool-call/tool-search/tool-registry/coreToolScheduler/client/environmentContext/agent-core/config/permission-manager/autoMode/builtin-agents/askUserQuestion/read-mcp-resource/syntheticOutput/fileUtils): 2318/2318 pass. Run twice; identical. Witness: 04-core-gate-15-changed-files.png. Gate liveness is established by the mutation matrix — the same vitest invocations went red under M1/M2/M3.
  • cli (nonInteractiveCli, Session, config, contextCommand): 1229 pass + 1 skipped — byte-identical to the PR's own reported numbers. junit artifacts preserved under logs/.

Findings

S1 — Suggestion (test precision): the recursion test's tool_call variant is pinned by a sibling guard, not the one it names. Deleting the recursion check in resolveDeferredToolCall (M3) turned only the tool_search variant of rejects recursive bridge target red; the tool_call variant stayed green because the envelope then falls through to isDeferredAndHidden('tool_call') === false, which returns the same INVALID_TOOL_PARAMS errorType the test asserts. Production is still doubly protected (recursion check, then visibility check — both reject), so this is a completeness item, not a defect: assert on the error message (cannot invoke bridge tool) so the named guard is actually pinned. Classification per the matrix taxonomy: redundant defence — the sibling hunk closes the same hazard, so nothing can observe this guard alone through the current assertion.

S2 — Informational: removal of the DeepSeek auto-disable is a user-visible default change the description undersells. Base auto-added tool_search to the deny list for deepseek-(v3|v4|chat) models; head keeps the bridge enabled for them and only an explicit tools.toolSearch.enabled: false disables both halves. The change is deliberate and test-pinned (should keep the stable bridge enabled for deepseek-v4 models, etc.) and is justified by the PR's own mechanism — the bridge keeps the prefix stable, which is what the auto-disable was compensating for. But "Breaking changes / migration notes: No public breaking change" should mention that DeepSeek users' default flips from eager declarations to the bridge.

S3 — Observation (pre-existing, not introduced by this PR): a denied MCP tool's schema is still handed out by tool_search; deny wins at call time. In the deny cell the tool remained registered and reviewable, and the bridge invocation was rejected with Qwen Code requires permission to use "mcp__echo-server__echo" … Matching deny rule — the correct claimed behavior ("explicit deny rules must win"), evaluated at the underlying identity. Registration gating for MCP deny rules is unchanged here (the tool-registry.ts diff is comments plus one debug-log string's wording), so the one wasted round-trip when a model tries a denied tool predates this PR. Reported for awareness only.

Not covered

  • Per-commit attribution: depth-2 checkout — git rev-list HEAD^1..HEAD^2 yields only the merge commit; the feat commit 90a2e34a is not locally reachable (the snapshot lists 2 commits). The aggregate HEAD^1..HEAD diff is what was verified.
  • Real prompt-cache hits: the wire oracle proves the necessary condition (byte-stable tools block + append-only history = stable serialized prefix). Actual cache-hit accounting lives provider-side and was not measured; the PR's "prompt-cache reads on continuation turns" smoke claim could not be replayed without a caching provider. This reproduces the wire shape that preserves caching, not a provider's cache behavior.
  • Live ACP / hooks / approvals / telemetry E2E: covered by the PR's unit tests, which this round proved non-vacuous (M2/M4/M5), but no live ACP session was driven.
  • Lazy-load failure path (ensureTool throws → tool error): unit tests only (tool-call.test.ts), no E2E cell.
  • Repo-wide test/lint/typecheck gates (only changed files were run); Windows behavior; docs accuracy, i18n entries, and web-shell display-name additions were diff-reviewed only.
  • The PR's "12 core files, 2,281 tests" number was not reproduced verbatim — this round ran a slightly different (superset) selection of 15 files → 2,318; the CLI figures matched exactly.

Methodology

Environment: the CI verify container (node:22-bookworm, Node v22.23.2), repo at merge commit ee904f951b with npm ci/npm run build pre-run. The wire harness (harness/) drives the real compiled bundles: a zero-dependency scripted mock OpenAI server (specialized from the e2e-testing skill template) records every request body to JSONL, a zero-dependency stdio MCP server logs each tools/call at the destination, and harness/assert.mjs turns both into 34 scripted pass/fail checks per run (scratch git project per cell, isolated QWEN_RUNTIME_DIR). The base arm was rebuilt from a HEAD^1 worktree (npm ci + npm run build + npm run bundle, ~6 min, logs in logs/base-*.log); the lockfile is untouched by the PR, and the base control was validated by asserting the workspace-link realpath and the absence of tool_call from the base build. Mutation runs used a second scratch worktree with QWEN_VITEST_GUARD_ROOT pointed at the main tree and package-local node_modules symlinked (identical lockfile). Raw per-cell artifacts (captured request bodies, CLI stdout/stderr, MCP call logs, meta) live under run/<cell>/; mutation log at logs/mutation-matrix.txt; junit from both gates under logs/. Evidence images were produced with scripts/verify-capture.mjs.

Flakiness gate log

rounds=5 files=19 skipped=0
file packages/cli/src/acp-integration/session/Session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.test.ts
file packages/cli/src/config/config.test.ts: (cd packages/cli) npx --no-install vitest run ./src/config/config.test.ts
file packages/cli/src/nonInteractiveCli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/nonInteractiveCli.test.ts
file packages/cli/src/ui/commands/contextCommand.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/commands/contextCommand.test.ts
file packages/core/src/agents/runtime/agent-core.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/runtime/agent-core.test.ts
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/core/client.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/client.test.ts
file packages/core/src/core/coreToolScheduler.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/coreToolScheduler.test.ts
file packages/core/src/core/environmentContext.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/environmentContext.test.ts
file packages/core/src/permissions/autoMode.test.ts: (cd packages/core) npx --no-install vitest run ./src/permissions/autoMode.test.ts
file packages/core/src/permissions/permission-manager.test.ts: (cd packages/core) npx --no-install vitest run ./src/permissions/permission-manager.test.ts
file packages/core/src/subagents/builtin-agents.test.ts: (cd packages/core) npx --no-install vitest run ./src/subagents/builtin-agents.test.ts
file packages/core/src/tools/askUserQuestion.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/askUserQuestion.test.ts
file packages/core/src/tools/read-mcp-resource.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/read-mcp-resource.test.ts
file packages/core/src/tools/syntheticOutput.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/syntheticOutput.test.ts
file packages/core/src/tools/tool-call.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/tool-call.test.ts
file packages/core/src/tools/tool-registry.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/tool-registry.test.ts
file packages/core/src/tools/tool-search.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/tool-search.test.ts
file packages/core/src/utils/fileUtils.test.ts: (cd packages/core) npx --no-install vitest run ./src/utils/fileUtils.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/acp-integration/session/Session.test.ts: PPP
  packages/cli/src/config/config.test.ts: PPP
  packages/cli/src/nonInteractiveCli.test.ts: PPP
  packages/cli/src/ui/commands/contextCommand.test.ts: PP
  packages/core/src/agents/runtime/agent-core.test.ts: PP
  packages/core/src/config/config.test.ts: PP
  packages/core/src/core/client.test.ts: PP
  packages/core/src/core/coreToolScheduler.test.ts: PP
  packages/core/src/core/environmentContext.test.ts: PP
  packages/core/src/permissions/autoMode.test.ts: PP
  packages/core/src/permissions/permission-manager.test.ts: PP
  packages/core/src/subagents/builtin-agents.test.ts: PP
  packages/core/src/tools/askUserQuestion.test.ts: PP
  packages/core/src/tools/read-mcp-resource.test.ts: PP
  packages/core/src/tools/syntheticOutput.test.ts: PP
  packages/core/src/tools/tool-call.test.ts: PP
  packages/core/src/tools/tool-registry.test.ts: PP
  packages/core/src/tools/tool-search.test.ts: PP
  packages/core/src/utils/fileUtils.test.ts: PP

verdict: timeout
summary: only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 1 · packages/cli/src/config/config.test.ts: P (exit 0)
round 1 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/commands/contextCommand.test.ts: P (exit 0)
round 1 · packages/core/src/agents/runtime/agent-core.test.ts: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/core/client.test.ts: P (exit 0)
round 1 · packages/core/src/core/coreToolScheduler.test.ts: P (exit 0)
round 1 · packages/core/src/core/environmentContext.test.ts: P (exit 0)
round 1 · packages/core/src/permissions/autoMode.test.ts: P (exit 0)
round 1 · packages/core/src/permissions/permission-manager.test.ts: P (exit 0)
round 1 · packages/core/src/subagents/builtin-agents.test.ts: P (exit 0)
round 1 · packages/core/src/tools/askUserQuestion.test.ts: P (exit 0)
round 1 · packages/core/src/tools/read-mcp-resource.test.ts: P (exit 0)
round 1 · packages/core/src/tools/syntheticOutput.test.ts: P (exit 0)
round 1 · packages/core/src/tools/tool-call.test.ts: P (exit 0)
round 1 · packages/core/src/tools/tool-registry.test.ts: P (exit 0)
round 1 · packages/core/src/tools/tool-search.test.ts: P (exit 0)
round 1 · packages/core/src/utils/fileUtils.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 2 · packages/cli/src/config/config.test.ts: P (exit 0)
round 2 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/commands/contextCommand.test.ts: P (exit 0)
round 2 · packages/core/src/agents/runtime/agent-core.test.ts: P (exit 0)
round 2 · packages/core/src/config/config.test.ts: P (exit 0)
round 2 · packages/core/src/core/client.test.ts: P (exit 0)
round 2 · packages/core/src/core/coreToolScheduler.test.ts: P (exit 0)
round 2 · packages/core/src/core/environmentContext.test.ts: P (exit 0)
round 2 · packages/core/src/permissions/autoMode.test.ts: P (exit 0)
round 2 · packages/core/src/permissions/permission-manager.test.ts: P (exit 0)
round 2 · packages/core/src/subagents/builtin-agents.test.ts: P (exit 0)
round 2 · packages/core/src/tools/askUserQuestion.test.ts: P (exit 0)
round 2 · packages/core/src/tools/read-mcp-resource.test.ts: P (exit 0)
round 2 · packages/core/src/tools/syntheticOutput.test.ts: P (exit 0)
round 2 · packages/core/src/tools/tool-call.test.ts: P (exit 0)
round 2 · packages/core/src/tools/tool-registry.test.ts: P (exit 0)
round 2 · packages/core/src/tools/tool-search.test.ts: P (exit 0)
round 2 · packages/core/src/utils/fileUtils.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 3 · packages/cli/src/config/config.test.ts: P (exit 0)
round 3 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)

Evidence images

01-ab-declaration-stability-head-vs-base

02-wire-assertions-six-cells

03-mutation-matrix-five-mutants

04-core-gate-15-changed-files

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

DragonnZhang and others added 3 commits August 29, 2026 00:42
…call-bridge

# Conflicts:
#	docs/developers/sdk-typescript.md
#	docs/users/configuration/settings.md
#	packages/cli/src/config/settingsSchema.ts
#	packages/core/src/agents/runtime/agent-core.ts
#	packages/core/src/config/config.ts
#	packages/core/src/core/client.test.ts
#	packages/core/src/core/client.ts
#	packages/core/src/core/coreToolScheduler.test.ts
#	packages/core/src/permissions/permission-manager.test.ts
#	packages/core/src/permissions/permission-manager.ts
#	packages/core/src/tools/tool-registry.test.ts
#	packages/core/src/tools/tool-registry.ts
#	packages/sdk-typescript/README.md
#	packages/sdk-typescript/src/types/types.ts
#	packages/vscode-ide-companion/schemas/settings.schema.json
@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

CI note: Integration Tests and Test fail in integration-tests/sdk-typescript/sdk-mcp-server.test.ts (4 tests) — the declared tool list (29 tools, starting agent, ask_user_question, …) is missing the expected mcp__sdk-async__delayed_response / mcp__sdk-error-test__maybe_fail entries.

Investigation so far (this is NOT a merge-conflict regression):

  • This branch's tool-registry.ts diff vs main is comments-only (bridge wording); no registration logic differs from main.
  • 4ac606dd1 (fix(core): restore deferred MCP tools on resumed sessions) is present in this branch AND in branches whose CI passed (e.g. perf(cli): reduce TUI render overhead #9970), so it is not the differentiator.
  • The identical assertion set also fails on feat(core): add CodeModeOnly programmatic tool calling MVP #10414, which shares this PR's tool_search/tool_call bridge redesign — the common factor is the bridge, not the merge.

Working hypothesis: the SDK E2E asserts eager MCP declarations, while under the bridge redesign those tools are (or should be) reached through the deferred bridge — so either the E2E needs adapting to the bridge flow, or there is a declaration/preload gap for MCP tools in the bridge path. Left to the review round / author judgment; no automatic code change applied.

中文说明

CI 说明:Integration TestsTestintegration-tests/sdk-typescript/sdk-mcp-server.test.ts(4 个测试)失败——声明的工具列表(29 个,以 agentask_user_question 等开头)缺少预期的 mcp__sdk-async__delayed_response / mcp__sdk-error-test__maybe_fail

已排查(这不是合并冲突回归):本分支 tool-registry.ts 相对 main 只有注释差异(桥接措辞),注册逻辑与 main 一致;4ac606dd1(恢复续话会话中延迟 MCP 工具的修复)同时存在于本分支与 CI 通过的其他分支(如 #9970),不是区分因素;完全相同的断言集也在 #10414 上失败,而两个分支的共同点是 tool_search/tool_call 桥接重设计,不是合并。

工作假设:SDK E2E 断言 MCP 工具即时声明,而桥接重设计下这些工具经由延迟桥接触达——因此要么 E2E 需要适配桥接流程,要么桥接路径上存在 MCP 工具声明/预加载缺口。交由评审轮次/作者判断,未自动改码。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explored to full depth (tool budget reached): chunk 11: executing tool-call.test.ts to confirm green — the review worktree has no node_modules , and a full monorepo install + workspace build to satisfy vitest's gl…; chunk 8: execute coreToolScheduler.test.ts bridge tests (no node_modules; install+build exceeds budget).

中文说明

未探索到全部深度(达到工具调用预算):chunk 11:executing tool-call.test.ts to confirm green — the review worktree has no node_modules , and a full monorepo install + workspace build to satisfy vitest's gl…;chunk 8:execute coreToolScheduler.test.ts bridge tests (no node_modules; install+build exceeds budget)

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/core/src/agents/runtime/agent-core.ts
Comment thread packages/core/src/agents/runtime/agent-core.ts
Comment thread packages/vscode-ide-companion/schemas/settings.schema.json Outdated
Comment thread docs/users/configuration/settings.md Outdated
Comment thread packages/cli/src/config/settingsSchema.ts Outdated
Comment thread docs/users/configuration/settings.md Outdated
Comment thread packages/core/src/core/coreToolScheduler.test.ts
Comment thread packages/core/src/tools/tool-call.ts Outdated
Comment thread packages/core/src/core/client.ts
Comment thread packages/core/src/core/coreToolScheduler.ts
…wenLM#10410)

Security/correctness:
- Re-check the execution allowlist against the resolved tool_call target in
  the scheduler (fork allowlist previously only saw the wrapper name).
- Skip the pending bridge TOOL_CALL start on abort so a cancelled call never
  emits a start event after its synthetic TOOL_RESULT.
- Route INVALID_TOOL_PARAMS bridge resolution errors through the
  validation-retry loop so repeated bad envelopes get the stop directive.
- Reject a hidden deferred target when tool_search is unregistered, and make
  the TOOL_NOT_REGISTERED remedy conditional on tool_search existing.
- Seed announcedMcpToolNames for eagerly revealed MCP tools so a later
  disconnect is still announced when the bridge is incomplete.
- buildDeferredToolsReminder returns null when the bridge is incomplete.

Docs (bridge-registered caveats, two-half reachability, DeepSeek removal,
schema regeneration, table cells, wording): settings.md, settingsSchema.ts +
regenerated settings.schema.json, sdk types.ts/README/sdk-typescript.md,
design doc, skills/types.ts, web-shell i18n.

Tests: allowlist re-check, abort ordering, retry directive, leader-only and
tool_search-absent rejections, revealed-tool exclusion, bridge availability
both directions, PM bridge gate, cancel-after-resolution rename, incomplete-
bridge reminder, MCP disconnect announcement, zoom hint wording.
…call-bridge

# Conflicts:
#	packages/core/src/tools/tool-search.ts
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

…ge contract (QwenLM#10410)

Under the ToolSearch + ToolCall bridge, tools fetched via tool_search
select: stay hidden from the model-facing declaration list so the
prompt-cache prefix remains stable (they are invoked through tool_call;
direct invocation by name still executes, as the resume test already
exercises). Flip the three fresh-session advertisement assertions from
'tool is advertised after select:' to 'tool remains hidden', matching
the contract documented in tool-search.ts. The resume-restoration test
is unchanged and already passes at the merge commit.
@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

CI note update (follow-up to the 2026-08-28 note above): the sdk-mcp-server.test.ts failures are now fixed in f96769f — root cause confirmed as a contract change, not an environment issue.

Evidence that it is not environmental: in the same CI window where this PR failed the suite, other branches' runs passed it — sdk-typescript/sdk-mcp-server.test.ts ran green (4/4, 14.7s) on the fix/session-cd-folder-trust-stale-settings run, and Integration Tests (no-AK) is green on #9503/#9683. The failure is specific to branches carrying the tool_search/tool_call bridge redesign (this PR and #10414), which matches the working hypothesis in the earlier note.

Root cause. The E2E asserted the pre-bridge contract: after tool_search select:, the selected tools appear in the model-facing declaration list. Under this PR's bridge that is intentionally no longer true — tool-search.ts documents it: "its declaration remains hidden so the model-facing tool list and prompt-cache prefix stay stable"; selected tools are reached through tool_call (coreToolScheduler.ts resolves the bridge request to the underlying tool before scheduling). The new-session advertisement assertions in three tests therefore failed at the merge commit while the same tools still execute fine by name.

Fix. Test-only change (no production code): flip the three fresh-session assertions from "tool is advertised after select:" to "tool remains hidden", with a comment stating the bridge contract. The fourth test (keeps previously used MCP tools available when resuming a session) already passes unchanged — its fresh-session phase demonstrates the direct-invocation execution this adaptation relies on, and its resume-phase assertion is covered by the deliberate restore-on-resume behavior. Verified: tsc -p integration-tests/tsconfig.json reports zero errors in the test program.

中文说明

CI 说明更新(承接上方 2026-08-28 的说明):sdk-mcp-server.test.ts 的失败已在 f96769f 修复——根因确认为契约变更,而非环境问题。

非环境问题的证据: 在本 PR 该套件失败的同一 CI 时间窗内,其他分支的运行通过了它——fix/session-cd-folder-trust-stale-settings 的运行中 sdk-typescript/sdk-mcp-server.test.ts 全绿(4/4,14.7 秒),#9503/#9683Integration Tests (no-AK) 也是绿的。该失败只出现在携带 tool_search/tool_call 桥接重设计的分支上(本 PR 与 #10414),与上一条说明中的工作假设一致。

根因。 该 E2E 断言的是桥接之前的契约:tool_search select: 之后,被选中的工具应出现在面向模型的声明列表中。在本 PR 的桥接下这不再是事实——tool-search.ts 明确记载:"其声明保持隐藏,使面向模型的工具列表与 prompt-cache 前缀保持稳定";被选中的工具经由 tool_call 触达(coreToolScheduler.ts 在调度前把桥接请求解析到底层工具)。因此三个测试中新会话的声明断言在合并提交上失败,而这些工具本身按名直接调用仍能正常执行。

修复。 仅测试变更(不动产品代码):把三处新会话断言从"select: 之后工具被声明"翻转为"工具保持隐藏",并加注释说明桥接契约。第四个测试(keeps previously used MCP tools available when resuming a session)无需改动本就通过——它的新会话阶段正好演示了本适配所依赖的按名直接执行行为,其续话阶段断言由刻意的续话恢复(restore-on-resume)行为覆盖。已验证:tsc -p integration-tests/tsconfig.json 对测试程序零报错。

@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

CI note: the red checks on this PR in the last ~6h are runner-fleet degradation, not PR content.

Evidence:

  • In the 00:00–06:40Z window, Test (ubuntu-latest) repeatedly hit its 60-minute timeout-minutes (and web-shell E2E Smoke its 20-minute limit) on unrelated branches — including main's own push runs (e.g. 33284933831 and 33279084985, both killed at ~60m), so no branch can pass this lane while the fleet is slow. The identical job passed within budget earlier tonight (run 33286728200) and on this branch's 2026-08-28 runs, and other lanes on the same reruns completed fast (e.g. feat(core): preserve prompt cache for deferred tools #10410's Test ubuntu finished in 21 minutes in the 06:00Z window), i.e. fleet speed oscillates rather than the suite having grown.
  • The same window also produced runner-local artifacts that vanish on rerun: a PEP 668 pip install yamllint rejection in Install linters, SIGTERM (exit 143) during web-shell Install dependencies, Playwright browserType.launch kills (kill ESRCH), and the hosting-block helper failures caused by a stray /tmp/package.json ("type": "module") on one self-hosted runner.
  • Content-relevant suites are green on the current head: Integration Tests (no-AK) passes on this PR's latest commits.

Response is mechanical rerun until the fleet recovers; no code change is indicated. If a maintainer sees this note going stale (fleet recovered), disregard.

中文说明

CI 说明:本 PR 最近约 6 小时的红灯是 runner 集群降级所致,不是 PR 内容问题。

证据:

  • 在 00:00–06:40Z 窗口内,互不相关的分支反复撞上 Test (ubuntu-latest) 的 60 分钟 timeout-minutes(以及 web-shell E2E Smoke 的 20 分钟上限)——包括 main 自己的 push 运行(如 33284933831 与 33279084985,均在约 60 分钟被杀),即集群变慢期间任何分支都过不了这一通道。同一作业在今晚更早(run 33286728200)及本分支 2026-08-28 的运行中都能在预算内通过,且同批重跑中其他作业很快完成(如 feat(core): preserve prompt cache for deferred tools #10410 的 Test ubuntu 在 06:00Z 窗口 21 分钟跑完)——是集群速度在波动,不是套件膨胀。
  • 同一窗口还出现了重跑即消失的 runner 本地产物:Install linters 中 PEP 668 拒绝 pip install yamllint、web-shell Install dependencies 期间被 SIGTERM(exit 143)、Playwright browserType.launch 被杀(kill ESRCH),以及某台自托管 runner 上残留的 /tmp/package.json"type": "module")导致的 hosting-block 辅助测试失败。
  • 与内容相关的套件在当前 head 上是绿的:Integration Tests (no-AK) 在本 PR 最新提交上通过。

对策是机械性重跑直至集群恢复,无需任何代码改动。若维护者看到本帖时集群已恢复,请忽略。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/acp-integration/session/Session.ts:10607 — [review] ACP errorResponse model-facing rename unpinned (success-path-only test)
  • packages/core/src/permissions/permission-manager.ts:774 — [probe] tool_call eager-allowlist exemption has no test (mutant flips probe registered->deferred)
  • packages/cli/src/acp-integration/session/Session.ts:10784 (+2 locations) — [review] bridge gates' isToolEnabled failure paths untested (ACP + scheduler)
  • packages/core/src/agents/runtime/agent-core.ts:1838 — [probe] preToolUse identity for bridged calls unobserved by tests (mutant: 49 passed with hook deleted)
  • packages/core/src/agents/runtime/agent-core.ts:1988 — [probe] one-shot pendingToolCallStarts.delete semantics unpinned (mutant delete->has: 49 passed)
  • packages/core/src/core/coreToolScheduler.test.ts:1034 — [probe] PM denial of resolved bridge target unpinned (asked-but-not-enforced; probe mutant executes denied target, suite green)
  • packages/cli/src/acp-integration/session/Session.ts:10804 — [probe] bridged TOOL_NOT_REGISTERED escapes the ACP invalid-params stagnation detector (probe flip)
  • packages/core/src/agents/runtime/agent-core.ts:2188 — [probe] abort fallback naming arm (?? req.name) unpinned (mutant matrix)
  • packages/core/src/core/coreToolScheduler.test.ts:1035 — [probe] bridge approval flow untested (all ten tests under YOLO; probe: awaiting_approval reachable)
中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 9 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment thread packages/core/src/core/coreToolScheduler.ts
Comment thread docs/users/configuration/settings.md Outdated
Comment thread packages/cli/src/config/settingsSchema.ts Outdated
Comment thread packages/cli/src/config/settingsSchema.ts Outdated
Comment thread packages/vscode-ide-companion/schemas/settings.schema.json Outdated
Comment thread packages/vscode-ide-companion/schemas/settings.schema.json Outdated
Comment thread docs/users/configuration/settings.md Outdated
Comment thread integration-tests/sdk-typescript/sdk-mcp-server.test.ts
Comment thread integration-tests/sdk-typescript/sdk-mcp-server.test.ts
Comment thread integration-tests/sdk-typescript/sdk-mcp-server.test.ts
DragonnZhang and others added 5 commits August 30, 2026 17:25
… fallback (QwenLM#10410)

The no-PermissionManager bridge deny gate matched deny entries exactly
(canonicalToolName is alias-resolution only — no lowercase/trim), but the
_schedule legacy-deny fallback it delegates to compares
excludedTool.toLowerCase().trim() === normalizedToolName. Config stores
permissions.deny entries verbatim, so an entry like 'Tool_Call' or
' tool_call ' slipped past the gate's exact compare: the envelope was
unwrapped and the fallback then checked the deny list only against the
resolved TARGET name — executing a call the same config denies when it
arrives as a direct tool_call (pre-diff behavior). Mirror the loop's
normalization on the gate so both paths deny identically (R2-1).

Adds three parameterized cases ('Tool_Call', ' tool_call ', 'TOOL_CALL')
extending the legacy-deny pin; removing the normalization turns all three
red while the exact-entry case stays green (verified). Full
coreToolScheduler.test.ts 395/395.
…racking, and prune pinning (QwenLM#10410)

Three fix-induced findings from the bridge rounds:

R1-15: the unreachable-tools warning's remedy clause now names
tools.disabled alongside deny rules and --exclude-tools, matching the
cause enumeration the PR documents in settingsSchema.ts; the warning test
pins the mention.

R1-28: an eager-reveal of an MCP tool (incomplete bridge) now also records
the name in announcedMcpToolNames. The seed alone only reaches that set via
rememberAnnouncedDeferredTools, which runs exclusively in startChat — so a
server registering after the initial startChat used to disconnect silently
and the model kept calling a dead server's tools. New test covers the
mid-session reveal, the disconnect announcement, and a reconnect/disconnect
flap announcing twice; removing the add turns it red (verified).

R1-18: new interleaved test pinning the batch-start prune as the only
mechanism that clears a tool_call-keyed retry counter across a successful
bridged execution (recording keys on the model-facing name, prune keys on
post-resolution names); disabling the prune fires RETRY LOOP DETECTED
prematurely and turns the test red (verified).

client.test.ts + coreToolScheduler.test.ts: 770/770 green.
…M#10410)

Four fix-induced doc findings, applied together across all copies:

R1-12 (9 locations): 'the demoted tools are out of reach for that session'
is now qualified with the two escape hatches the code has — tools also
listed in tools.visible are declared upfront, and resumed sessions
re-declare demoted tools referenced by direct calls in the transcript
(consistent with client.ts's own 'would be false for this session'
comment).

R1-25 (settings.md tools.core/discoveryCommand/migration rows): 'stay
reachable through tool_search + tool_call' now carries the 'while both
bridge tools are registered' condition.

R1-9 (threshold, 3 copies): 'Set 0 to always keep deferred tools behind
the bridge' overstated — two reveal paths ignore the threshold; replaced
with the qualified wording naming resumed-history replay, tools.visible,
and the incomplete-bridge fallback.

R2-2 (4 locations): the unaffected/bypass lists now name tool_call next to
tool_search, matching isExemptFromEagerAllowList which exempts both
identically.
…10410)

R2-9: the flipped not.toContain declaration assertions now carry positive
controls on the same snapshot — the post-select list must contain both
alwaysLoad bridge tools (tool_search, tool_call) — so an emptied or
index-shifted declaration list can no longer pass them vacuously.

R2-15: the first test now scripts the fake model to invoke the selected
tools through tool_call envelopes (the documented post-select path),
exercising resolveDeferredToolCall end-to-end through query(); results are
paired by the model-facing name ('tool_call') with content checks, since
the scheduler keeps modelFacingName on the wire. Reverting the script to
direct-by-name calls would remove the suite's only end-to-end signal for a
broken invocation half.

typecheck -p integration-tests clean.
@wenshao

wenshao commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real local stack, PR head cd12dd31ac vs merge-base 168a88c02e

I built a live rig for this PR and ran 21 full CLI sessions through it. The headline is the one thing the PR description lists as not validated: "The raw provider request body was not captured for a byte-for-byte live declaration comparison." That gap is now closed with wire evidence, and the A/B against main shows the regression this PR removes.

Verdict: the behaviour claimed in the description holds on a real stack. I recommend merging. Two non-blocking notes at the end.

Rig

Nothing mocked below the CLI boundary:

  • CLI — the bundled node <worktree>/dist/cli.js, built from each side (npm run build && npm run bundle), headless -p and ACP --acp.
  • Provider — an OpenAI-compatible HTTP server that writes every request body to disk verbatim before responding, with a scripted tool-call sequence per turn.
  • MCP — a disposable stdio server on @modelcontextprotocol/sdk exposing 9 tools, declared in user-scope $QWEN_HOME/settings.json, logging every invocation itself.
  • Deferral — forced with tools.toolSearch.threshold = 0 so the probe tools genuinely stay behind the bridge.

Both sides ran the same prompt, the same MCP server and the same settings. Only the build differs.

1 · The declared tool block is byte-identical across the session (and is not, on main)

declaration stability

request 0 after tool_search after invoke distinct blocks
main @168a88c02e 14 tools / 40 837 B / 6e50202b3646 15 / 41 276 / adc146a7c3f8 15 / 41 276 / adc146a7c3f8 2
PR #10410 @cd12dd31ac 15 tools / 41 441 B / f79c8b8eb8e1 15 / 41 441 / f79c8b8eb8e1 15 / 41 441 / f79c8b8eb8e1 1

On main, tool_search splices mcp__probe__weather_lookup (+439 B) into the declared list, so the entire 40 837-byte tools block changes at exactly the turn the deferred tool is discovered. On this PR the block is the same 41 441 bytes with the same SHA-256 in all three requests, and messages[] is append-only in both builds — so on the PR side the full logical prefix (system + tools + prior turns) stays reusable, and on main it does not.

Measured cost of the fix, same captures: +613 B constant for the always-declared tool_call envelope, against −439 B per deferred tool that would otherwise be spliced in mid-session. For reference, the incomplete-bridge fallback declaring all 9 MCP tools eagerly costs 70 578 B (+70 %).

2 · The two-step bridge works end to end, with a real MCP round trip

live bridge trace

tool_search(select:mcp__probe__weather_lookup) returns the schema inside <functions>…</functions> as tool output; tool_call then reaches the MCP server, whose own log shows exactly one CALL weather_lookup city=Hangzhou units=metric, and PROBE_MCP_RESULT city=Hangzhou units=metric temp=21C comes back to the model under tool_call_id=call_bridge_1 — the original call ID, model-facing name still tool_call. Keyword mode ("weather city lookup") reaches the same tool, and two tool_call envelopes in one assistant turn both resolve and execute, each keeping its own call ID.

The identity split is observable from three different vantage points in the same runs:

  • model — sees tool_searchtool_call; the declared list never changes.
  • hooks — a PreToolUse hook fires once, with tool_name = mcp__probe__weather_lookup and the target's arguments. A hook matcher on tool_call, configured in the same run, did not fire.
  • ACP clienttool_call arrives pending with _meta.toolName=tool_call, then flips to in_progress with _meta.toolName=run_shell_command, title="Shell: echo ACP_BRIDGE_OK", kind=execute, and completes with rawOutput="ACP_BRIDGE_OK" (shell exit 0).

3 · Policy, refusals and fallbacks

policy matrix

Every refusal path in the description reproduces, and in each the MCP server records zero invocations: recursive tool_calltool_call, tool_calltool_search, a visible non-deferred target (read_file), and an unregistered target.

Two points worth calling out, because the round-2 review deferred both as untested:

  • permissions.deny on the resolved target wins. With permissions.deny = ["mcp__probe__weather_lookup"], the bridged call returns "…requires permission to use "mcp__probe__weather_lookup" … Matching deny rule: "mcp__probe__weather_lookup"" and the MCP server is never called. The deny is evaluated against the target, not the wrapper.
  • Approval is evaluated under the target's identity. Under approvalMode = default in headless, the bridged call is refused with the same wording main emits for the equivalent direct deferred call — the target name, not tool_call. (The unit-test gap the reviewer flagged is real; the production behaviour is correct.)

Fallbacks all keep deferred tools reachable — no configuration stranded one:

configuration declared tool_search tool_call MCP schemas direct call
bridge complete (baseline) 15 yes yes hidden n/a
tools.toolSearch.enabled = false 36 no no eager works
permissions.deny = ["tool_search"] 37 no yes eager works
permissions.deny = ["tool_call"] 37 yes no eager n/a (bridge denied)

The startup reminder on the wire matches the new contract: "The following tools are reachable through tool_search and tool_call. Review a schema with select:<name> or a keyword query, then invoke it with tool_call."

4 · Mutation probe on the load-bearing guards

Five mutants against src/tools/tool-call.test.ts + src/tools/tool-search.test.ts + src/core/coreToolScheduler.test.ts (459 tests, green at baseline):

mutant outcome
drop the isDeferredAndHidden target guard killed
drop the scheduler's isToolExecutionAllowed re-check on the resolved target killed
model-facing response name → resolved target name killed (3 tests)
drop the incomplete-bridge unreachability gate killed
drop the recursive-bridge guard survived — see note 2

5 · Suites, typecheck, lint

  • packages/core: 22 597 passed / 22 628, 1 failure — exit-worktree.test.ts timed out under parallel load and passes 16/16 in isolation on both this branch and the base. Environmental.
  • packages/cli (run from the package dir): 26 298 passed / 26 375, 7 failures across acpAgent.test.ts, AuthDialog.test.tsx, systemController.test.ts — all three files pass in isolation (535/535, 25/25, 25/25 with an isolated QWEN_HOME). Known host-pollution / parallel-load flakes, not this PR.
  • npm run typecheck — pass. npm run lint:ci — pass.

Non-blocking notes

  1. packages/core/src/tools/tool-search.ts:326 is not Prettier-formattedconst schemaBlocks = reviewed.map( (tool) => should wrap. It slips through CI because the Prettier step runs prettier --write . rather than --check, so it reformats silently and exits 0. npm run format fixes it.
  2. The recursive-bridge guard is not pinned by its test. rejects recursive bridge target %s asserts only errorType: INVALID_TOOL_PARAMS, which the downstream isDeferredAndHidden rejection also returns — so deleting the dedicated guard leaves the suite green. Behaviour is still correct either way (I confirmed both distinct messages live), but asserting on the message would make the guard load-bearing.

What I did not cover

  • MCP tools under ACP. MCP servers from user-scope settings did not register in my ACP sessions (gating in my rig, not a PR behaviour), so the ACP bridge path was exercised with a tools.eager-demoted built-in (run_shell_command) instead of an MCP tool. Same resolveDeferredToolCall call site in Session.ts.
  • Provider-side cache counters. The evidence here is the byte-level precondition (identical tools block + append-only messages), not a measured cache hit-rate against a live provider.
  • Subagent execution allowlist — pinned by mutation (M2 above) and unit tests, not driven through a live subagent run.
  • macOS only (Node v24.18.1). Windows and Linux untested, matching the PR's own table.
中文说明

维护者验证 — 本地真实环境,PR head cd12dd31ac 对比 merge-base 168a88c02e

我为这个 PR 搭了一套真实链路的验证台,跑了 21 次完整 CLI 会话。重点是 PR 描述里自己列为未验证的那一条:"没有抓取原始 provider request body 来进行实时的工具声明字节级对比"。这个缺口现在用链路证据补上了,并且与 main 的 A/B 直接展示了这个 PR 消除的回归。

结论:描述中的行为在真实链路上成立,建议合入。 文末两条非阻断意见。

验证台

CLI 边界以下没有任何 mock:

  • CLI — 打包后的 node <worktree>/dist/cli.js,两侧各自 npm run build && npm run bundle,分别跑 headless -p 和 ACP --acp
  • Provider — OpenAI 兼容的 HTTP 服务,在响应前把每一个 request body 原样落盘,按轮次脚本化返回 tool call。
  • MCP — 基于 @modelcontextprotocol/sdk 的一次性 stdio server,暴露 9 个工具,声明在 user scope 的 $QWEN_HOME/settings.json,服务端自己记录每一次调用。
  • defer — 用 tools.toolSearch.threshold = 0 强制探针工具真的留在 bridge 后面。

两侧同一个 prompt、同一个 MCP server、同一份 settings,只有构建不同。

1 · 已声明工具块在整个会话中逐字节相同(main 上则不然)

request 0 tool_search 之后 调用之后 不同的块数
main @168a88c02e 14 个 / 40 837 B / 6e50202b3646 15 / 41 276 / adc146a7c3f8 15 / 41 276 / adc146a7c3f8 2
PR #10410 @cd12dd31ac 15 个 / 41 441 B / f79c8b8eb8e1 15 / 41 441 / f79c8b8eb8e1 15 / 41 441 / f79c8b8eb8e1 1

main 上,tool_searchmcp__probe__weather_lookup(+439 B)插进已声明列表,于是整个 40 837 字节的 tools 块恰好在发现 deferred tool 的那一轮发生变化。这个 PR 上三次请求都是同样的 41 441 字节、同样的 SHA-256;两侧的 messages[] 都是只追加的——所以 PR 这一侧完整的逻辑前缀(system + tools + 之前的轮次)保持可复用,main 那一侧则不行。

同一批抓包量到的代价:固定 +613 B(始终声明的 tool_call 信封),换取每个 deferred tool −439 B 的中途插入。作为参照,bridge 不完整时的回退把 9 个 MCP 工具全部提前声明,代价是 70 578 B(+70%)。

2 · 两阶段桥接端到端可用,并且真的打到了 MCP

tool_search(select:mcp__probe__weather_lookup) 把 schema 包在 <functions>…</functions>作为工具输出返回;随后 tool_call 打到 MCP server,服务端日志里恰好一条 CALL weather_lookup city=Hangzhou units=metricPROBE_MCP_RESULT city=Hangzhou units=metric temp=21Ctool_call_id=call_bridge_1(原始 call ID)回到模型,模型侧工具名仍然是 tool_call。关键词模式("weather city lookup")同样能找到该工具;一轮里发两个 tool_call 信封也都能各自解析并执行,各自保留自己的 call ID。

身份分离在同一批运行里可以从三个不同视角观察到:

  • 模型侧 — 看到的是 tool_searchtool_call,已声明列表始终不变。
  • hooksPreToolUse 只触发一次tool_name = mcp__probe__weather_lookup,参数是目标工具的参数。同一次运行里配置的 tool_call matcher 没有触发。
  • ACP 客户端tool_call 先以 pending 到达,_meta.toolName=tool_call;随后翻成 in_progress_meta.toolName=run_shell_commandtitle="Shell: echo ACP_BRIDGE_OK"kind=execute;最后 completedrawOutput="ACP_BRIDGE_OK"(shell exit 0)。

3 · 权限、拒绝与回退

描述里列的每条拒绝路径都复现了,并且每一条 MCP server 都记录到次调用:递归的 tool_calltool_calltool_calltool_search、可见的非 deferred 目标(read_file)、未注册的目标。

有两点值得单独指出,因为第 2 轮评审把它们都记为未测:

  • permissions.deny 对解析后的目标生效。 配置 permissions.deny = ["mcp__probe__weather_lookup"] 时,桥接调用返回 "…requires permission to use "mcp__probe__weather_lookup" … Matching deny rule: "mcp__probe__weather_lookup"",MCP server 完全没被调用。deny 判定的是目标,不是外层信封。
  • 审批按目标身份评估。 headless 下 approvalMode = default 时,桥接调用被拒的措辞与 main 上等价的直接 deferred 调用完全一致——出现的是目标名而不是 tool_call。(评审指出的单测缺口确实存在;生产行为是对的。)

回退路径都让 deferred tools 保持可达,没有任何一种配置把工具变成孤儿:

配置 已声明 tool_search tool_call MCP schema 直接调用
bridge 完整(基线) 15 隐藏 不适用
tools.toolSearch.enabled = false 36 提前声明 可用
permissions.deny = ["tool_search"] 37 提前声明 可用
permissions.deny = ["tool_call"] 37 提前声明 不适用(桥接被拒)

链路上的启动提醒也与新契约一致:"The following tools are reachable through tool_search and tool_call. Review a schema with select:<name> or a keyword query, then invoke it with tool_call."

4 · 对承重防线做变异验证

针对 src/tools/tool-call.test.ts + src/tools/tool-search.test.ts + src/core/coreToolScheduler.test.ts(基线 459 个用例全绿)做了 5 个变异体:

变异体 结果
去掉 isDeferredAndHidden 目标校验 被杀
去掉 scheduler 对解析后目标的 isToolExecutionAllowed 复检 被杀
模型侧响应名改成解析后的目标名 被杀(3 个用例)
去掉 bridge 不完整时的不可达判定 被杀
去掉递归桥接校验 存活 — 见第 2 条意见

5 · 测试套件、typecheck、lint

  • packages/core22 597 通过 / 22 628,1 个失败 —— exit-worktree.test.ts 在并行负载下超时,单独跑在本分支和 base 上都是 16/16 通过。属于环境抖动。
  • packages/cli(在包目录里跑):26 298 通过 / 26 375,7 个失败分布在 acpAgent.test.tsAuthDialog.test.tsxsystemController.test.ts —— 三个文件单独跑都通过(535/535、25/25、隔离 QWEN_HOME 后 25/25)。是已知的宿主污染 / 并行负载抖动,与本 PR 无关。
  • npm run typecheck 通过;npm run lint:ci 通过。

非阻断意见

  1. packages/core/src/tools/tool-search.ts:326 没有过 Prettier —— const schemaBlocks = reviewed.map( (tool) => 应该换行。之所以能溜过 CI,是因为 Prettier 那一步跑的是 prettier --write . 而不是 --check,会静默重写并以 0 退出。npm run format 即可修复。
  2. 递归桥接校验没有被它的测试钉住。 rejects recursive bridge target %s 只断言了 errorType: INVALID_TOOL_PARAMS,而下游 isDeferredAndHidden 的拒绝路径返回的是同一个 errorType —— 所以删掉这个专门的 guard,套件依然全绿。两条路径的行为都正确(两种不同措辞我都在真实链路上确认过),但断言到 message 才能让这个 guard 真正承重。

未覆盖的部分

  • ACP 下的 MCP 工具。 user scope settings 里的 MCP server 在我的 ACP 会话中没有注册成功(是我这套台子的 gating 问题,不是 PR 行为),因此 ACP 桥接路径改用被 tools.eager 降级的内置工具(run_shell_command)验证。走的是 Session.ts 里同一个 resolveDeferredToolCall 调用点。
  • Provider 侧的缓存计数。 这里的证据是字节级前提(tools 块完全一致 + messages 只追加),不是对真实 provider 测出的缓存命中率。
  • Subagent 执行白名单 —— 由上面的变异体和单测钉住,没有通过真实 subagent 运行来驱动。
  • 仅 macOS(Node v24.18.1)。Windows / Linux 未测,与 PR 自己的表格一致。

…#10410)

1. tool-search.ts:326 — Prettier-format the schemaBlocks map (slipped
   through CI because the format step runs --write, not --check).
2. tool-call.test.ts — pin the recursive-bridge guard by its message
   ('cannot invoke bridge tool'), not just errorType: the downstream
   isDeferredAndHidden rejection returns the same INVALID_TOOL_PARAMS, so
   the old errorType-only assertion stayed green with the guard deleted.
   Mutation check confirms the strengthened test now kills that mutant
   (2 failed with the guard removed, 12/12 intact).
@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

Thank you for the thorough real-stack verification and the byte-level declaration evidence — that closes the one gap the description flagged as unvalidated. Both non-blocking notes are addressed in 839bd8b:

  1. tool-search.ts:326 Prettier — reformatted (npm run format equivalent on the file); the schemaBlocks map now wraps correctly.
  2. Recursive-bridge guard not load-bearing — strengthened rejects recursive bridge target %s to assert on the guard's distinct message (toContain('cannot invoke bridge tool')), not just errorType. Mutation check confirms it now pins the guard: deleting the dedicated guard flips exactly the two parameterized cases red (2 failed | 10 passed), where the old errorType-only assertion stayed green because the downstream isDeferredAndHidden rejection returns the same INVALID_TOOL_PARAMS. Full file 12/12 intact; tsc --noEmit on packages/core clean.
中文说明

感谢这份真实链路的验证与字节级声明证据——它补上了描述中唯一标记为未验证的缺口。两条非阻断意见已在 839bd8b 处理:

  1. tool-search.ts:326 Prettier —— 已重新格式化;schemaBlocks 的 map 现在正确换行。
  2. 递归桥接校验未承重 —— 已把 rejects recursive bridge target %s 强化为断言该守卫独有的 message(toContain('cannot invoke bridge tool')),而不只是 errorType。变异检查确认它现在真正钉住了守卫:删除专门的守卫恰好让两个参数化用例变红(2 failed | 10 passed),而旧的仅断言 errorType 的版本因下游 isDeferredAndHidden 拒绝返回同样的 INVALID_TOOL_PARAMS 而保持绿色。全文件 12/12 完整;tsc --noEmit(packages/core)干净。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • ACP bridge gates' isToolEnabled failure paths untested (deny / resolution-error / abort) — already recorded in the round-2 review body's deferred list (review 5060349583) as Session.ts:10784 (+2 locations)
  • Bridge E2E suite outside every npm workspace — already posted in round 2 as R2-19 (comment 3888891678, integration-tests/sdk-typescript/sdk-mcp-server.test.ts); the author's CI-gate claim was verified this round (test:integration:no-ak:sand…
  • Bridged TOOL_NOT_REGISTERED escapes the ACP invalid-params stagnation detector — already recorded in the round-2 review body's deferred list (review 5060349583) as Session.ts:10804

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): chunk 10: running the new tests in this worktree ( cd packages/core && npx vitest run src/core/coreToolScheduler.test.ts ) — the tree has no node_modules or built dist…; chunk 6: run the three new agent-core.test.ts bridge tests and the background-agent-resume reminder test (no node_modules in the review worktree; install+build not attem….

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • packages/core/src/core/coreToolScheduler.test.ts:847 — [review] dead switch: includeToolSearch option never set by any test
  • packages/cli/src/nonInteractiveCli.ts:2288 (+2 locations) — [probe] headless bridge identity map unwitnessed in both directions
  • packages/cli/src/acp-integration/session/Session.ts:10980 — [probe] daemon stagnation detector collapses all bridge failures into one bucket
  • packages/core/src/tools/tool-call.test.ts:67 — [probe] envelope-validation catch untested through the resolution path
  • packages/core/src/tools/tool-call.test.ts:201 — [review] bridge-half-unavailable branches have zero test pins
  • packages/core/src/tools/tool-call.ts:77 — [probe] select: is case-insensitive but tool_call is exact-match
  • packages/core/src/core/client.ts:1837 — [probe] queue-time eager-reveal seed delete is unwitnessed and redundant — delete it
  • packages/core/src/core/coreToolScheduler.test.ts:1214 — [probe] bridge gate's isToolEnabled-throw branch unwitnessed
  • packages/core/src/core/coreToolScheduler.ts:2591 — [probe] truncation-corrupted bridge envelopes miss the recovery guidance
  • packages/cli/src/nonInteractiveCli.ts:2053 — [probe] cancelled bridge calls recorded under target identity flip skillsModifiedInSession
  • packages/core/src/skills/types.ts:47 (+4 locations) — [review] old-model 'tool_search loads tools' wording left standing in three places
  • packages/core/src/core/environmentContext.ts:227 — [probe] fork-resume reminder advertises the bridge beyond the fork's allowlist
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):chunk 10:running the new tests in this worktree ( cd packages/core && npx vitest run src/core/coreToolScheduler.test.ts ) — the tree has no node_modules or built dist…;chunk 6:run the three new agent-core.test.ts bridge tests and the background-agent-resume reminder test (no node_modules in the review worktree; install+build not attem…

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 12 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.3)

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

10 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • includeToolSearch dead switch in the scheduler-test helper — already recorded in the round-3 review body's deferred list (review 5061525570) as coreToolScheduler.test.ts:847
  • truncation-corrupted bridge envelopes miss TRUNCATION_PARAM_GUIDANCE — already recorded in the round-3 review body's deferred list (review 5061525570) as coreToolScheduler.ts:2591
  • tool_call eager-allowlist exemption has no test pin — already recorded in the round-2 review body's deferred list (review 5060349583) as permission-manager.ts:774
  • ACP bridge unwrap failure paths (enablement gate / resolution-error mapping / abort) untested — already recorded in the round-2 review body's deferred list (review 5060349583) as Session.ts:10784 (+2 locations) and disclosed as duplicate in…
  • headless bridge identity map unwitnessed in the output-finalization direction — already recorded in the round-3 review body's deferred list (review 5061525570) as nonInteractiveCli.ts:2288 (+2 locations)
  • E2E bridge suite outside every npm workspace — already posted in round 2 as R2-19 (comment 3888891678, integration-tests/sdk-typescript/sdk-mcp-server.test.ts); the author's CI-gate claim (test:integration:no-ak:sandbox:none) was verified i…
  • bridge gate isToolEnabled-throw branch unwitnessed — already recorded in the round-3 review body's deferred list (review 5061525570) as coreToolScheduler.test.ts:1214
  • PM/deny re-check of the resolved bridge target unpinned — already recorded in the round-2 review body's deferred list (review 5060349583) as coreToolScheduler.test.ts:1034
  • bridged TOOL_NOT_REGISTERED escapes the ACP invalid-params stagnation detector — already recorded in the round-2 review body's deferred list (review 5060349583) as Session.ts:10804 and confirmed as a duplicate in the round-3 body
  • bridge approval flow untested (awaiting_approval reachable, all bridge tests under YOLO) — already recorded in the round-2 review body's deferred list (review 5060349583) as coreToolScheduler.test.ts:1035

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; packages/cli, sdk-typescript, vscode-ide-companion and web-shell suites also did not run locally (Agent 7's whole-call budget expired in the build phase). packages/core was re-run locally: 22582 passed; 36 residual failures all in files this PR does not touch (hook process-spawn, fs-lease, timing, session-env shapes), 71 of the original 107 disappeared when this review session's QWEN_HOME env leak was excluded, and CI's ubuntu Test matrix is green at this exact commit — classified environmental.

Not explored to full depth (tool budget reached): "agent 1c": none — no check was cut short.; "agent reverse-audit (round 2)": none — the full chunk (diff lines 2952-3346) was read untruncated and every check above was completed..

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • integration-tests/sdk-typescript/sdk-mcp-server.test.ts:208 — [probe] post-select E2E assertions are membership-only; declaration-list equality (the prompt-cache stability property) unpinned
  • packages/core/src/core/client.ts:1783 — [probe] rememberAnnouncedDeferredTools re-seed liveness guard and dead-seed drop have no test witness
  • packages/core/src/core/environmentContext.test.ts:518 — [probe] incomplete-bridge reminder gate's TOOL_SEARCH term unwitnessed
  • packages/core/src/core/client.ts:2622 — [probe] resumed sessions seed recentCompletedToolNames with bridge names instead of resolved targets, degrading memory-recall tool hints
  • packages/core/src/core/client.test.ts:2870 — [probe] missingHalves warning enumeration half-unwitnessed (two mutants pass the committed suite)
  • packages/core/src/core/coreToolScheduler.test.ts:1035 — [probe] PostToolUse resolved-name pin missing for bridged calls
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 10 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; packages/cli, sdk-typescript, vscode-ide-companion and web-shell suites also did not run locally (Agent 7's whole-call budget expired in the build phase). packages/core was re-run locally: 22582 passed; 36 residual failures all in files this PR does not touch (hook process-spawn, fs-lease, timing, session-env shapes), 71 of the original 107 disappeared when this review session's QWEN_HOME env leak was excluded, and CI's ubuntu Test matrix is green at this exact commit — classified environmental。

未探索到全部深度(达到工具调用预算):"agent 1c"none — no check was cut short."agent reverse-audit (round 2)"none — the full chunk (diff lines 2952-3346) was read untruncated and every check above was completed.

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment thread packages/core/src/tools/tool-call.ts Outdated
…ol_call bridge (QwenLM#10410)

R4-1 (review round 4): the tool_call bridge bypassed the subagent/teammate
tool-exclusion set. EXCLUDED_TOOLS_FOR_SUBAGENTS / EXCLUDED_TOOLS_FOR_TEAMMATES
were enforced only at declaration level (prepareTools isExcluded), but the
bridge makes invocation independent of declaration without carrying the
exclusion set over — resolveDeferredToolCall enforced only the plan-lifecycle
and leader-only policies, tool_search had no exclusion filter, and the
scheduler's post-unwrap re-checks never consulted the set. A wildcard/
general-purpose subagent (or teammate) could therefore discover (tool_search)
and execute (tool_call) control-plane tools it must not reach (team_delete,
cron_*, workflow, send_message, ...); the probe executed team_delete
end-to-end through the bridge from a subagent context. The merge base
rejected the same trigger (undeclared names fail the declaredToolNames gate),
so the bridge introduced the path.

Fix: enforce the same context-aware exclusion set in resolveDeferredToolCall,
after the plan-lifecycle/leader-only checks (so those keep their specific
messages). To avoid a circular import (agent-core imports coreToolScheduler,
which imports tool-call), the exclusion sets and getExcludedToolsForCurrentContext
moved from agent-core.ts to subagent-plan-tool-policy.ts (already imported by
tool-call.ts); agent-core.ts re-exports them for existing consumers. The check
is guarded by isSubagentLikeExecutionContext so the leader context is not gated.

Adds a mutation-checked test (rejects an exclusion-set target team_delete /
workflow bridged from a subagent context): removing the exclusion check turns
both red. tool-call.test.ts 14/14; tool-search + coreToolScheduler 447;
subagent-plan-tool-policy 6; agent-core + background-agent-resume 99; core
tsc --noEmit clean.
…re (TS6133)

The exclusion sets that spread SUBAGENT_PLAN_LIFECYCLE_TOOLS moved to
subagent-plan-tool-policy.ts so the tool_call bridge can enforce them
without a circular import; the import in agent-core.ts was left behind
and tsc --build flags it as unused (TS6133).
@DragonnZhang
DragonnZhang dismissed qwen-code-ci-bot’s stale review August 31, 2026 13:54

Dismissing stale review: the sole finding R4-1 (bridge bypasses the subagent/teammate tool-exclusion set) was addressed after this review's commit (839bd8b):

  • bad5b1e: resolveDeferredToolCall now enforces the same context-aware exclusion set as prepareTools (EXECUTION_DENIED), checked after the plan-lifecycle/leader-only checks so those keep their specific messages; exclusion sets moved to subagent-plan-tool-policy.ts to avoid a circular import and re-exported from agent-core.ts for existing consumers.
  • Mutation-checked: new it.each cases in tool-call.test.ts fail when the check is removed.
  • 192ffba: follow-up fix for the TS6133 unused-import the refactor left behind.

The review was submitted 2026-08-31T11:52 against pre-fix commit 839bd8b; no open findings remain against current head.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • envelope-validation catch untested through the resolution path (tool-call.test.ts:67) — already recorded in the round-3 review body's deferred list (review 5061525570) as tool-call.test.ts:67 — [probe]

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; this round's harness test phase also did not run (whole-call budget exhausted by install + 16 workspace builds — packages/core build verified green, and the new tool-call.test.ts passes 14/14 when run directly).

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • packages/core/src/tools/tool-call.ts:90 — [probe] case-sensitive tool_call resolution contradicts case-insensitive tool_search discovery (select:Read_File resolves, tool_call Read_File → TOOL_NOT_REGISTERED)
  • packages/core/src/tools/tool-call.test.ts:106 — [probe] unknown-target present-side remedy ('Run tool_search again') unpinned; absent-side-constant mutant survives the suite

Convergence: round 5 posted 7 inline comment(s), 7 of them reported for the first time; the previous round posted 1 (1 new). Findings keep coming back to the same files: packages/core/src/tools/tool-call.ts (findings in round 4; 3 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; this round's harness test phase also did not run (whole-call budget exhausted by install + 16 workspace builds — packages/core build verified green, and the new tool-call.test.ts passes 14/14 when run directly)。

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 5 轮发布了 7 条行内评论,其中 7 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。发现反复回到同一批文件:packages/core/src/tools/tool-call.ts(第 4 轮已出过发现,本轮又有 3 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment thread packages/core/src/tools/tool-call.ts Outdated
Comment thread packages/core/src/tools/tool-call.ts Outdated
Comment thread packages/core/src/tools/tool-call.ts Outdated
Comment thread packages/core/src/tools/tool-call.test.ts Outdated
Comment thread packages/core/src/tools/tool-call.test.ts
Comment thread packages/core/src/tools/tool-call.test.ts
Comment thread packages/core/src/tools/tool-call.test.ts Outdated
…wenLM#10410)

R4-1 follow-up (Critical): the flat exclusion denied AgentTool through the
bridge even while maxSubagentDepth permitted the spawn — prepareTools
depth-gates AgentTool (re-admits it while spawnBlockReason === null) but the
bridge consumed the raw set. Add the shared predicate
isToolExcludedForCurrentContext (subagent-plan-tool-policy.ts) that carries
the depth-gated re-admission, consumed by BOTH prepareTools and
resolveDeferredToolCall so the layers cannot drift; the scheduler threads the
real configured maxSubagentDepth through. Without the configured depth the
bridge fails closed on AgentTool (the raw-set floor). The predicate is
deliberately ungated so prepareTools keeps its frameless fail-closed
contract; the bridge and tool_search apply their own isSubagentLikeExecution
context gate, leaving the leader session untouched.

R5-1: mirror the exclusion on the discovery side — collectCandidates and the
select: blocked predicate now drop context-excluded tools, so subagents are
no longer advertised the schemas of tools they are forbidden to invoke
(schema leak + wasted turns). Leader re-inspection stays unrestricted.

R5-2: move the plan-lifecycle / leader-only / exclusion checks ahead of the
isDeferredAndHidden gate (relative order kept). Real excluded tools are
frequently not deferred (workflow/team_delete/todo_write shouldDefer=false,
enter_plan_mode constructed shouldDefer=false), so the old gate order
misrouted them into the factually-wrong 'already visible — call it directly'
INVALID_TOOL_PARAMS denial and the scheduler's malformed-envelope retry
counting.

R5-3..R5-6 test pins: SEND_MESSAGE subagent denial + teammate resolve/deny
cases discriminate the context-aware selector; the plan-tool denial pins the
dedicated message; allow-side cases pin the context gate (leader and
in-frame non-excluded resolution); the legacy alias case pins canonical-name
membership keying. Fixtures now use the real non-deferred shape.

Mutation-verified: nine mutants (check removed, selector swapped both ways,
plan check removed, envelope-name keying, flat AgentTool, bridge gate
removed, blanket in-frame denial, tool-search filter removed) all turn the
new tests red; suites green (tool-call 22, tool-search 56, policy 6,
agent-core 49, coreToolScheduler 396).
@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

Round-5 review batch addressed in 074b4ad (all seven requested findings; verified before pushing, per the convergence note):

R4-1 follow-up (Critical) — the flat exclusion no longer denies agent while the depth policy permits the spawn: new shared predicate isToolExcludedForCurrentContext (subagent-plan-tool-policy.ts) carries prepareTools' depth-gated AgentTool re-admission and is consumed by both prepareTools and resolveDeferredToolCall, with the scheduler threading the real configured maxSubagentDepth through. Unknown depth fails closed (raw-set floor); the leader session is unaffected.

R5-1 — discovery side closed in this PR: collectCandidates() and the select: blocked predicate drop context-excluded tools via the same predicate; blocked entries report the shared denial message; leader re-inspection stays unrestricted (pinned).

R5-2 — plan-lifecycle / leader-only / exclusion checks moved ahead of the isDeferredAndHidden gate (relative order kept), so non-deferred excluded tools (the real shape) get the specific EXECUTION_DENIED denial instead of the factually-wrong "already visible — call it directly" INVALID_TOOL_PARAMS; fixtures pin the real shape.

R5-3..R5-6 — SEND_MESSAGE + teammate resolve/deny cases discriminate the selector both ways; the plan-tool denial pins its message; allow-side cases pin the context gate (in-frame non-excluded + leader); the legacy-alias case pins canonical-name membership keying.

Mutation-verified: nine mutants (check removed; selector swapped both ways; plan check removed; envelope-name keying; flat AgentTool; bridge gate removed; blanket in-frame denial; tool-search filter removed) all turn the new tests red. Green suites: tool-call 22, tool-search 56, policy 6, agent-core 49, coreToolScheduler 396.

Recorded, intentionally not in this batch (deferred list from the round-5 body): the case-sensitive tool_call vs case-insensitive select: resolution asymmetry, and the unknown-target remedy pin — carried forward to the next round / follow-up.


第 5 轮评审要求的 7 项发现已在 074b4ad 全部处理并推送前验证完毕:R4-1 后续(新增共享谓词,prepareTools 与桥接共用深度门控的 AgentTool 重新准入,调度器透传真实配置深度,深度未知 fail-closed,leader 不受影响);R5-1 发现侧过滤关闭(候选与 select: 阻断均按上下文门控,leader 复查不受限);R5-2 三个策略检查前移至 deferred 门之前(保持相对顺序,fixture 钉真实形状);R5-3..R5-6 测试补齐(选择器双向区分、plan 消息钉、放行侧两用例、遗留别名钉规范名键控)。九个变异体全部变红;各套件全绿。评审体中列出的两项延迟项(大小写不一致、未知目标补救钉)按轮次纪律留给下一轮/跟进。

Conflicts resolved in favor of the bridge-aware documentation and the
tool_search + tool_call wording from this PR, while carrying forward main's
new content: the automatic DeepSeek opt-out condition and the structured_output
exempt-list entry are folded into the bridge pairing descriptions
(settingsSchema, vscode schema, settings.md, SDK types/README/docs tables);
main's new visibleTools registry test is kept alongside this PR's renamed
reveal test; main's expanded settings table is kept with the six
bridge-touched rows merged.
Case-insensitive resolution: tool_search's select: resolves requested names
case-insensitively, but the tool_call half resolved them exactly, so a schema
reviewed as e.g. Read_File was not callable through the bridge. The
invocation half now falls back to a case-insensitive match against the
registered names (exact-case names keep the fast path), and the fallback feeds
the recursive bridge guard, so casing cannot dodge it either.

Remedy pin: 'rejects an unknown deferred target' now pins the present-side
remedy ('Run tool_search again...'); the absent-side constant was already
pinned by the sibling case, so both remedy branches are mutation-checked.

Mutation-verified: removing the fallback turns 2 tests red; dropping the
remedy suffix turns 1 red; suite green (24).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants