Skip to content

feat(core): add CodeModeOnly programmatic tool calling MVP - #10414

Closed
DragonnZhang wants to merge 10 commits into
QwenLM:mainfrom
DragonnZhang:dragon/code-mode-only
Closed

feat(core): add CodeModeOnly programmatic tool calling MVP#10414
DragonnZhang wants to merge 10 commits into
QwenLM:mainfrom
DragonnZhang:dragon/code-mode-only

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds a default-off tools.codeModeOnly MVP that narrows the model-visible tool surface to a structured exec({source}) control tool plus an audited set of direct-only controls. Ordinary registered tools remain available inside an isolated JavaScript runtime through deterministic tools.* bindings and a read-only ALL_TOOLS catalog, while tool_search, tool_call, and recursive exec calls are excluded from nested execution. Direct mode remains the default and preserves the existing deferred-tool bridge.

Each execution uses a fresh QuickJS isolate in a worker with hard source, output, nested-call, memory, cancellation, and ten-second CPU limits. Nested calls re-enter the existing Core or ACP scheduling paths so registry lookup, parameter validation, permissions, approvals, hooks, telemetry, UI lifecycle events, and cancellation continue to apply to the real nested tool. OpenAI-compatible, Anthropic, and Gemini/Qwen declaration paths preserve the same structured source-only schema.

This is intentionally the CodeModeOnly MVP slice. Hybrid CodeMode, wait and persistent cells, store/yield helpers, multimodal projection, and native freeform provider input remain outside this PR.

Why it's needed

Large top-level tool surfaces consume prompt context and force additional model-to-tool round trips for deterministic branching, fan-out, filtering, and aggregation. Programmatic tool calling keeps that orchestration local while retaining Qwen Code's existing permission and lifecycle controls, and it provides a rollback-safe experimental path because the feature is disabled by default.

Reviewer Test Plan

How to verify

  1. Start without tools.codeModeOnly and confirm the provider receives the existing direct declarations, does not receive exec, and can still discover then invoke a deferred tool through tool_search and tool_call.
  2. Enable tools.codeModeOnly, restart, and confirm the provider receives only exec plus the audited direct-only controls. Confirm ordinary tools and the deferred bridge controls are absent from the top-level declarations, while the exec schema requires exactly one string source field.
  3. Execute JavaScript that calls tools.read_file, including sequential and concurrent calls, and confirm the outer response belongs to exec while nested permission, hook, telemetry, and lifecycle events use the real tool name.
  4. Confirm malformed JavaScript fails, numeric and boolean source values are rejected, recursive controls are unavailable, and Node, process, import, network, console, shared-memory, and WebAssembly capabilities are absent.
  5. Run an infinite loop and cancel an awaited nested request. The loop should terminate at approximately ten seconds, cancellation should close the nested request, and the CLI should exit without a surviving worker or OOM.

Evidence (Before & After)

Before: Direct mode exposed the existing 11-tool headless surface and the deferred bridge could discover and invoke cron_list; no code execution control existed.

After: repeated OpenAI-compatible and Anthropic captures exposed a stable 10-tool CodeModeOnly surface with the required source-only schema. exec successfully called read_file; source: 42 and source: true returned is_error: true; syntax errors remained failures; nested bridge/control calls were unavailable; an infinite loop timed out at 10,006 ms and the provider completed the following round; SIGINT cancelled an awaited nested request and closed its socket. Focused Core tests passed 406/406 and the full ACP session suite passed 719/719. Full build, typecheck, lint, bundle, and package preparation completed successfully.

Tested on

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

Environment (optional)

macOS 27.0 arm64, Node.js v24.18.0, standalone bundled CLI, and local fake OpenAI-compatible and Anthropic providers.

Risk & Scope

  • Main risk or tradeoff: this adds a bundled QuickJS/WASM worker and reentrant nested scheduling to core execution paths. The setting is default-off and the runtime fails closed when its worker asset is unavailable. The core production diff exceeds 1,000 lines, so this PR carries the repository's non-blocking maintainer-awareness advisory.
  • Not validated / out of scope: Windows and Linux runtime probes, a live Gemini network request, the local interactive TUI approval probe that is currently blocked by an unrelated Ink render failure, Hybrid CodeMode, wait/cell lifecycle, persistent store/yield helpers, multimodal output projection, and native freeform provider input.
  • Breaking changes / migration notes: none. Direct mode remains the default; enabling or disabling tools.codeModeOnly requires a restart.

Linked Issues

Refs #10377

中文说明

本 PR 做了什么

本 PR 新增默认关闭的 tools.codeModeOnly MVP,将模型顶层可见工具收敛为结构化的 exec({source}) 控制工具和经过审计的少量 direct-only 控制面。普通已注册工具仍可在隔离 JavaScript runtime 中通过确定性的 tools.* 绑定和只读 ALL_TOOLS 目录使用;tool_searchtool_call 和递归 exec 不允许作为嵌套调用。Direct 模式继续作为默认值,并保持现有 deferred-tool bridge 行为。

每次执行都会在 worker 中创建全新的 QuickJS isolate,并施加源码、输出、嵌套调用、内存、取消和十秒 CPU 硬限制。嵌套调用重新进入现有 Core 或 ACP 调度路径,因此 registry lookup、参数校验、权限、审批、hooks、遥测、UI 生命周期事件和取消仍作用于真实嵌套工具。OpenAI-compatible、Anthropic 和 Gemini/Qwen 的声明路径保持相同的结构化 source-only schema。

本 PR 有意只交付 CodeModeOnly MVP。Hybrid CodeMode、wait 和持久 cell、store/yield helpers、多模态结果投影及 provider 原生 freeform 输入不在本 PR 范围内。

为什么需要

大量顶层工具 schema 会占用 prompt context,并让确定性的分支、fan-out、过滤和聚合产生更多模型—工具往返。程序化工具调用把这些编排留在本地,同时保留 Qwen Code 现有的权限与生命周期控制;由于功能默认关闭,也提供了可安全回滚的实验路径。

Reviewer 测试计划

如何验证

  1. 不启用 tools.codeModeOnly 启动,确认 provider 收到现有 Direct 声明、不包含 exec,并且仍能通过 tool_searchtool_call 发现及执行 deferred tool。
  2. 启用 tools.codeModeOnly 并重启,确认 provider 只收到 exec 和经过审计的 direct-only 控制工具。普通工具及 deferred bridge 控制工具不应出现在顶层声明中,exec schema 应严格要求唯一的字符串 source 字段。
  3. 执行调用 tools.read_file 的 JavaScript,包括顺序与并发调用;确认外层响应属于 exec,而嵌套权限、hook、遥测和生命周期事件使用真实工具名。
  4. 确认错误 JavaScript 会失败,数字和布尔 source 会被拒绝,递归控制工具不可用,并且不存在 Node、process、import、网络、console、共享内存和 WebAssembly 能力。
  5. 运行无限循环,并取消一个正在等待的嵌套请求。无限循环应在约十秒终止;取消应关闭嵌套请求;CLI 退出后不应残留 worker 或发生 OOM。

证据(Before & After)

Before:Direct 模式暴露原有 11 项 headless 工具面,deferred bridge 能发现并调用 cron_list,但不存在代码执行控制工具。

After:多轮 OpenAI-compatible 和 Anthropic 抓包均得到稳定的 10 项 CodeModeOnly 工具面及严格 source-only schema。exec 成功调用 read_filesource: 42source: true 返回 is_error: true;语法错误继续正确失败;嵌套 bridge/control 调用不可用;无限循环在 10,006 ms 超时,provider 随后正常完成下一轮;SIGINT 能取消正在等待的嵌套请求并关闭 socket。Core 聚焦测试 406/406、ACP session 全套 719/719;完整 build、typecheck、lint、bundle 和 package preparation 均成功。

测试平台

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

环境(可选)

macOS 27.0 arm64、Node.js v24.18.0、standalone bundled CLI,以及本地 fake OpenAI-compatible 和 Anthropic providers。

风险与范围

  • 主要风险或取舍:核心执行路径新增 bundled QuickJS/WASM worker 和可重入嵌套调度。该设置默认关闭,worker 资产缺失时 runtime 会 fail closed。core 生产代码 diff 超过 1,000 行,因此按仓库规则附带非阻塞 maintainer-awareness advisory。
  • 未验证 / 不在范围:Windows 和 Linux runtime 探针、真实 Gemini 网络请求、因无关 Ink render 错误而受阻的本地交互式 TUI 审批探针、Hybrid CodeMode、wait/cell 生命周期、持久 store/yield helpers、多模态输出投影和 provider 原生 freeform 输入。
  • 破坏性变更 / 迁移说明:无。Direct 模式仍为默认值;启用或关闭 tools.codeModeOnly 后需要重启。

关联 Issue

Refs #10377

@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

Local black-box E2E report

Tested the committed standalone bundle on macOS 27.0 arm64 with Node.js v24.18.0 against local OpenAI-compatible and Anthropic protocol servers. The tests exercised node dist/cli.js end to end rather than invoking the runtime classes directly.

Direct regression

  • The provider received the existing 11-tool headless surface and no exec declaration.
  • tool_search(select: cron_list) followed by tool_call(cron_list) succeeded; cron_list remained absent from the direct declaration list.
  • CLI exited 0.

CodeModeOnly surface and execution

  • Five consecutive OpenAI-compatible requests exposed the same 10 direct controls: agent, enter_worktree, exec, exit_worktree, get_goal, list_agents, send_message, skill, task_stop, and update_goal.
  • read_file, tool_search, and tool_call were absent from the top-level surface. Anthropic /v1/messages exposed the same surface.
  • exec required source:string and rejected additional properties across both provider schemas.
  • exec → tools.read_file successfully read the first line of the repository package.json.
  • source: 42 and source: true both returned is_error: true with The 'source' parameter must be a string.
  • Invalid JavaScript returned is_error: true with the QuickJS syntax error. Nested tools.tool_search, tools.tool_call, and tools.exec were unavailable.

Isolation, timeout, and cancellation

  • process, require, Buffer, fetch, console, WebAssembly, Atomics, and SharedArrayBuffer were all unavailable in the guest.
  • while (true) {} returned Code mode execution timed out after 10000ms. at 10,006 ms. The provider completed the following round, and the CLI exited 0 in 11.91 seconds without OOM or a surviving worker.
  • Cancelling an awaited nested web_fetch closed the pending socket immediately. The CLI followed the cancellation path with code 130 and did not send a second provider round.

Automated regression evidence

  • Focused CodeMode and scheduler suites: 406/406 passed.
  • Full ACP session suite: 719/719 passed.
  • CLI config suite: 357/357 passed.
  • /context diagnostics: 19/19 passed.
  • Full npm run build, npm run typecheck, npm run lint, npm run bundle, and npm run prepare passed.

Bundle hashes used for the final post-fix probes:

  • dist/cli.js: 24a05b14d1007c31c8a7c039024b8d1e9d6a4fc8
  • dist/codeModeWorker.js: e187ce93a187ac078f6cb2a1a5cf8621136ade5d

Not covered locally: Windows/Linux runtime probes, a live Gemini request, and interactive TUI approval because the local probe hit an unrelated Ink rendering failure. Gemini declarations and approval routing are covered by unit tests.

@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 d0fa0a5. 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

# Conflicts:
#	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.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.ts
#	packages/sdk-typescript/README.md
#	packages/sdk-typescript/src/types/types.ts
#	packages/vscode-ide-companion/schemas/settings.schema.json
@DragonnZhang

DragonnZhang commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@/tmp/10414_note_fixed.md

# Conflicts:
#	packages/core/src/config/config.test.ts
#	packages/core/src/tools/tool-search.ts
…ge contract (QwenLM#10414)

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 36ecff8 — 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 (#10410 and this PR), 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 的失败已在 36ecff8 修复——根因确认为契约变更,而非环境问题。

非环境问题的证据: 在本 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 桥接重设计的分支上(#10410 与本 PR),与上一条说明中的工作假设一致。

根因。 该 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 and others added 2 commits August 30, 2026 15:26
QwenLM#10414)

verifyBundleArtifacts now requires dist/codeModeWorker.js (added with the
CodeModeOnly worker), but createBundleArtifacts never created it, so every
preparePackage-driven fixture test exited early with 'Required package
artifact not found' (process.exit 1) before reaching the audio-capture
paths under test — e.g. 'fails packaging when required audio-capture
package JSON is invalid' got the exit instead of the expected throw.
Fixture now ships the empty worker alongside cli.js; package-assets suite
33/33 green locally (1 pre-existing root-user skip).
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