Repository navigation
feat(core): add CodeModeOnly tool execution - #10607
Conversation
E2E Test ReportScope
Results
Known baseline failure
Platform coverage
|
Critical review follow-upAll three Critical findings are fixed in this update.
The settled-promise job-error suggestion was fixed at the same time: errors raised by jobs triggered from a nested tool result are now checked, propagated, and disposed. Verification:
The root build and affected GitHub jobs still stop at the existing SDK browser-daemon size gate ( |
CodeModeOnly hides `tool_search` and binds every deferred tool — schema included — into the `exec` description, so progressive discovery has nothing left to do. The session prelude still announced those tools as "reachable via `tool_search`", pointing the model at a tool that is never declared and re-billing the text on every cached prefix. Worse, tools already callable as `tools.web_fetch(...)` looked gated behind a lookup step that does not exist. The subagent and fork-resume callers already opted out, but the main-session prelude sites did not, so the gate now lives where the prelude is built rather than in each caller.
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
E2E test report (tmux, interactive TUI) — no Critical foundReviewed head Review summary (Critical-only pass)
Interactive TUI e2e (bundled CLI built from this PR head)Setup:
Notes
中文摘要:对 head |
CodeModeOnly hides tool_search and binds every deferred schema into the exec description, so text that tells the model to look a tool up describes a surface it cannot reach. Return an empty deferred summary from the registry itself - this also covers the fork-resume reminder that bypassed the earlier call-site gate - and point the image zoom hint at tools.zoom_image.
In CodeModeOnly the model can only call exec, so the tool guidance and the worked examples described a surface it does not have: bare tool names it cannot call, an instruction to issue several tool calls in one response, and examples in a direct tool-call syntax that does not exist there. Give "Using Your Tools" a code-mode branch that routes every tool through tools.<name>, states which tools are direct controls instead, and replaces multi-call parallelism with batching inside one exec program. Swap the four model-family example sets for one shared exec set, since the syntax differences they exist for do not apply. The mechanics of exec itself stay in its tool description; the prompt carries only policy.
…-10377 # Conflicts: # packages/core/src/core/client.ts
CodeModeOnly hides tool_search and never surfaces a nested call as a history functionCall, so a signature collapsed to Record<string, unknown> could never be filled in later: parameter names were reachable only by guessing, or by reading them off validation errors one at a time. Deferred tools keep their registry state; only the generated signature gains the schema.
npm chmods dist/index.js when it links the bin at install time, but the build deletes that exact file and tsc re-emits it as 0644. After any rebuild node_modules/.bin/node-repl-mcp therefore points at a non-executable target and spawning it fails with EACCES (exit 126) before the shebang is ever read. OR the exec bits into the emitted mode instead of setting 0o755, so a restrictive umask is preserved rather than widened.
# Conflicts: # packages/core/src/tools/shell.test.ts
…o dragon/code-mode-only-10377
# Conflicts: # packages/cli/src/acp-integration/session/Session.ts
Resolve the single shell.test.ts import-block conflict from QwenLM#10607 (CodeModeOnly) by keeping both imports (formatShellExitCode + runWithToolCallSource).
What this PR does
This PR adds an experimental, opt-in CodeModeOnly tool mode while preserving Direct mode as the default. In CodeModeOnly, the model receives one structured
exec({ source })function plus the small set of control-plane tools that must remain directly visible; ordinary, deferred, and MCP tools remain registered and are invoked programmatically throughtools.xxx(args)inside isolated JavaScript.Each exec runs in a fresh QuickJS WASM context hosted by a dedicated child process with bounded memory, guest CPU time, source size, IPC frames, and output. The guest CPU budget and parent watchdog pause only while the guest is suspended on registered host tools, so long shell/build calls keep their own scheduler timeout while JavaScript loops remain bounded. The guest has no Node.js, filesystem, network, module loading, timers, console, WebAssembly, shared memory, or inherited application secrets, and unfinished nested calls are cancelled when the program settles or the parent turn is cancelled.
Nested calls reuse the existing execution semantics instead of recursively entering the active scheduler or directly executing tools. Permission checks, approval, argument validation, hooks, telemetry, cancellation, concurrency rules, ACP recording, and real nested tool names remain observable while the provider receives only the outer exec response. Read-only teammates and restricted forks receive
execas an audited gateway with the same exact nested-tool allowlist enforced in the description, binding plan, and Core dispatch. Tool declarations, normalized JavaScript names, collision handling, andALL_TOOLSmetadata are deterministic across registration order.The bundled and standalone distributions include the isolated runtime host. Direct mode keeps the existing deferred discovery and invocation path unchanged.
Why it's needed
Large top-level tool lists consume prompt tokens, reduce prompt-cache stability, and force the model to coordinate multi-step work through repeated model/tool round trips. CodeModeOnly provides a smaller, stable request surface and lets the model compose ordinary tool calls with JavaScript while retaining Qwen Code's permission, policy, telemetry, and lifecycle guarantees.
Reviewer Test Plan
How to verify
tools.codeModeOnlytotrue, restart Qwen Code, and confirm provider requests containexecplus audited direct-only control tools but do not contain ordinary tools,tool_search, ortool_call.execwithconst result = await tools.read_file({ file_path: "/absolute/path" }); text(result);and confirm the nested read passes through validation, permissions, hooks, telemetry, and cancellation, then returns as the outer exec response without scheduler deadlock.Promise.allboth complete, MCP/deferred names appear inALL_TOOLS, normalized names are callable, and collisions produce a deterministic warning instead of silent replacement.tools.exec, and parent cancellation; confirm each execution terminates with a bounded error and leaves no runtime host process behind.fork_tools; confirmexecremains callable while itstools.*bindings exactly match the effective execution allowlist.Evidence (Before & After)
Before: Qwen Code only had the Direct request surface; ordinary tools were sent as top-level function declarations and there was no isolated JavaScript bridge.
After: A real bundled-CLI E2E run sent 28 declarations in Direct mode and 16 declarations in CodeModeOnly mode. Both modes completed two provider requests; CodeModeOnly did not expose
read_fileortool_search, whileexecsuccessfully invoked the registeredread_filetool and returned its real result to the next provider turn. A second bundled-CLI E2E ran a real 40-second foreground shell command throughexecand completed in 41.972 seconds. The expanded Core regression passed 543/543, provider/converter tests passed 326/326, CLI configuration/schema tests passed 410/410, packaging tests passed 34/34, and the targeted ACP regression passed.Tested on
Environment (optional)
macOS, Node.js 22+, local bundled CLI with a deterministic OpenAI-compatible test server, QuickJS WASM child runtime, and package-level Vitest suites. Root typecheck, lint, package builds, and bundle generation passed. The root build reaches the pre-existing SDK browser-daemon size gate and fails at 220220 bytes versus the 220160-byte baseline limit; the same 60-byte failure is present on the base revision.
Risk & Scope
tools.codeModeOnly: trueand a restart. Safe and bare modes continue to use Direct mode.Linked Issues
Closes #10377
中文说明
本 PR 做了什么
本 PR 新增一个实验性、显式开启的 CodeModeOnly 工具模式,同时保留 Direct 作为默认模式。在 CodeModeOnly 中,模型只会收到一个结构化的
exec({ source })函数,以及必须直接可见的少量控制面工具;普通、deferred 和 MCP 工具仍然保持注册,并在隔离 JavaScript 中通过tools.xxx(args)编程式调用。每次 exec 都在专用子进程托管的全新 QuickJS WASM context 中运行,并限制内存、guest CPU 时间、源码大小、IPC frame 和输出。只有 guest 挂起等待已注册 host tool 时,CPU 预算和父进程 watchdog 才会暂停,因此耗时较长的 shell/build 调用保留自身 scheduler timeout,而 JavaScript 死循环仍受固定预算约束。Guest 环境不提供 Node.js、文件系统、网络、模块加载、timer、console、WebAssembly、共享内存或继承的应用密钥;程序完成或父 turn 取消时,未完成的嵌套调用也会被取消。
嵌套调用复用现有执行语义,而不是递归进入正在运行的 scheduler,或者直接执行工具。权限检查、approval、参数验证、hooks、telemetry、取消、并发规则、ACP 记录和真实嵌套工具名称仍然可观测,而 provider 只会收到外层 exec 响应。只读 teammate 和受限 fork 仍可把
exec作为审计网关,并在描述、binding plan 和 Core dispatch 三处执行同一份精确嵌套工具 allowlist。工具声明、规范化 JavaScript 名称、冲突处理和ALL_TOOLS元数据不受注册顺序影响,具备确定性。打包产物和 standalone 发行包均包含隔离 runtime host。Direct 模式保持现有 deferred 发现与调用路径不变。
为什么需要
大量顶层工具列表会消耗 prompt token,降低 prompt cache 稳定性,并迫使模型通过多次模型/工具往返来组织多步工作。CodeModeOnly 提供更小、更稳定的请求面,让模型能用 JavaScript 组合普通工具调用,同时保留 Qwen Code 的权限、policy、telemetry 和生命周期保障。
Reviewer 测试计划
如何验证
tools.codeModeOnly设为true,重启 Qwen Code,确认 provider 请求只包含exec和经审计的 direct-only 控制工具,不包含普通工具、tool_search或tool_call。const result = await tools.read_file({ file_path: "/absolute/path" }); text(result);调用exec,确认嵌套 read 经过参数校验、权限、hooks、telemetry 和取消机制,然后作为外层 exec 响应返回,且不会产生 scheduler 死锁。Promise.all均可完成,MCP/deferred 名称出现在ALL_TOOLS中,规范化名称可调用,名称冲突会产生确定性警告而不是静默覆盖。tools.exec以及父级取消;确认每次执行都以有界错误结束,且不留下 runtime host 进程。fork_tools的 fork;确认exec仍可调用,且其tools.*binding 与有效 execution allowlist 完全一致。证据(修改前与修改后)
修改前:Qwen Code 只有 Direct 请求面;普通工具作为顶层 function declarations 发送,且没有隔离的 JavaScript bridge。
修改后:真实 bundle CLI E2E 在 Direct 模式发送 28 个声明,在 CodeModeOnly 模式发送 16 个声明。两种模式都完成了两次 provider 请求;CodeModeOnly 没有暴露
read_file或tool_search,而exec成功调用已注册的read_file工具,并将真实结果返回给下一个 provider turn。另一条真实 bundle CLI E2E 通过exec运行了 40 秒前台 shell 命令,并在 41.972 秒完成。扩展 Core 回归测试 543/543 通过,provider/converter 测试 326/326 通过,CLI 配置/schema 测试 410/410 通过,打包测试 34/34 通过,ACP 定向回归通过。测试平台
环境(可选)
macOS、Node.js 22+、使用确定性 OpenAI-compatible 测试服务的本地 bundle CLI、QuickJS WASM 子进程 runtime,以及 package 级 Vitest 测试。根级 typecheck、lint、package build 和 bundle 生成通过。根级 build 运行到已存在的 SDK browser-daemon size gate 时失败,大小为 220220 bytes,而基线限制为 220160 bytes;同样的 60-byte 失败在基线修订上也存在。
风险与范围
tools.codeModeOnly: true并重启。Safe 和 bare 模式继续使用 Direct 模式。关联 Issue
Closes #10377