Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6ab42db
feat: add hybrid code mode
DragonnZhang Sep 14, 2026
fda09c0
fix(core): address hybrid code mode review feedback
DragonnZhang Sep 15, 2026
9a0cd57
chore: merge main into code mode branch
DragonnZhang Sep 15, 2026
30b2a6c
fix(code-mode): address review feedback
DragonnZhang Sep 18, 2026
6c919b7
fix(web-shell): map tool mode setting alias
DragonnZhang Sep 18, 2026
02afce8
fix(core): align code mode bindings and reported tool surfaces
DragonnZhang Sep 20, 2026
2ecf53e
merge: sync main and preserve CodeMode context tests
DragonnZhang Sep 20, 2026
4a38632
merge: preserve hybrid execution with stable tool bridge
DragonnZhang Sep 21, 2026
3239c68
fix: repair web-shell alias tests and address code-mode review feedback
qwen-code-ci-bot Sep 22, 2026
f35beb1
Merge remote-tracking branch 'origin/main' into dragon/add-codemode
qwen-code-ci-bot Sep 22, 2026
28df8ee
fix: align hybrid tool guidance with available invocation surfaces
DragonnZhang Sep 22, 2026
a7315eb
fix: address code-mode review feedback on exec binding narrowing and …
qwen-code-ci-bot Sep 22, 2026
3c69762
Merge branch 'main' into dragon/add-codemode
qwen-code-dev-bot Sep 23, 2026
34711cf
fix(core): avoid unverified agent zoom hints and clarify tool exposure
DragonnZhang Sep 23, 2026
ce7e57d
Merge remote-tracking branch 'origin/main' into dragon/add-codemode
qwen-code-ci-bot Sep 23, 2026
fa457bd
fix(core): narrow MCP nested bindings by the agent tools allowlist
qwen-code-ci-bot Sep 23, 2026
ae35809
fix(core): keep CodeModeOnly exec expansion from bypassing MCP narrowing
qwen-code-ci-bot Sep 24, 2026
4c53574
Merge branch 'main' into dragon/add-codemode
qwen-code-dev-bot Sep 24, 2026
22a9a40
fix: preserve code-mode fork boundaries and update mode guidance
DragonnZhang Sep 26, 2026
779d689
merge: refresh main OAuth discovery before publishing code-mode fixes
DragonnZhang Sep 26, 2026
b1561b7
fix: preserve nested-only fork access and legacy mode resets
DragonnZhang Sep 27, 2026
e6fbdcd
fix(core): preserve empty tool policies when syncing main
DragonnZhang Sep 28, 2026
6edd151
fix: merge main and translate current tool mode settings
DragonnZhang Sep 28, 2026
f746b88
fix(core): merge main and preserve hybrid skill lifecycle
DragonnZhang Oct 1, 2026
ed2e58c
fix(core): merge main and preserve hybrid agent permissions
DragonnZhang Oct 3, 2026
4a72ef8
fix(core): align child skill routes and merge current main
DragonnZhang Oct 4, 2026
cad9407
fix(core): retain code-mode behavior with current main
DragonnZhang Oct 5, 2026
1df2386
fix(cli): align tool mode readouts and merge main
DragonnZhang Oct 6, 2026
71ab2c9
fix(core): preserve code mode policy when merging MCP rule fixes
DragonnZhang Oct 7, 2026
68ceea7
Merge branch 'main' into dragon/add-codemode
qwen-code-dev-bot Oct 7, 2026
f669bd7
Merge branch 'main' into dragon/add-codemode
qwen-code-dev-bot Oct 7, 2026
f183ce0
fix(core): align hybrid tool visibility and prompt budgets
DragonnZhang Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/design/2026-09-12-agent-container-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ values.

The policy composes with `isolation: "worktree"` and `working_dir`. It does not
extend the model-visible isolation enum or change `isolation: "remote"`.
Combining it with `tools.codeModeOnly` is rejected before container startup;
the first container registry supports direct tool calls only.
Combining it with `tools.mode: "code_mode_only"` is rejected before container startup;
`tools.mode: "code_mode"` warns and continues with direct tools only, registering
no `exec`. The first container registry supports direct tool calls only.

## Backend policy and definition boundaries

Expand Down
3 changes: 2 additions & 1 deletion docs/design/2026-09-12-agent-container-execution.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ CLI 重启时保留环境变量值,供 Node TLS 证书、设置插值等启动

该策略可与 `isolation: "worktree"` 和 `working_dir` 组合。
它不扩展面向模型的 isolation 枚举,也不改变 `isolation: "remote"`。
与 `tools.codeModeOnly` 的组合会在容器启动前被拒绝;首版容器注册表只支持直接工具调用。
与 `tools.mode: "code_mode_only"` 的组合会在容器启动前被拒绝;`tools.mode: "code_mode"`
会告警并仅以直接工具继续,不注册 `exec`。首版容器注册表只支持直接工具调用。

## 后端策略与定义边界

Expand Down
70 changes: 38 additions & 32 deletions docs/design/code-mode-only.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CodeModeOnly MVP

> Current behavior: Only discovers schemas through top-level tool_search and invokes tools through exec. Full signatures are included when search is unavailable in the current scope; tools.eager can reduce the initial declaration. Hybrid keeps direct tools plus exec with its existing bridge and eager permission boundaries. The original MVP statements below about hiding tool_search or always including full schemas are superseded by the [lazy-loading design](lazy-code-mode.md).

[English](code-mode-only.md) | [简体中文](code-mode-only.zh-CN.md)

## Status

Implemented for [#10377](https://github.com/QwenLM/qwen-code/issues/10377).
Expand All @@ -12,20 +16,20 @@ describe this MVP; `tool_call` stays hidden.

## Goal

Add a `tools.codeModeOnly` setting that replaces the ordinary model-facing
tool surface with one `exec` JavaScript tool plus the small set of tools that
must remain direct control-plane calls. `exec` code can call ordinary tools
Add a `tools.mode: "code_mode_only"` setting that replaces the ordinary
model-facing tool surface with one `exec` JavaScript tool plus the small set of
tools that must remain direct control-plane calls. `exec` code can call ordinary tools
through `tools.<name>(args)` without bypassing Qwen Code's validation,
permissions, approvals, hooks, telemetry, cancellation, concurrency, or output
budgets.

Direct mode is a compatibility boundary: when the setting is false, tool
Direct mode is a compatibility boundary: when `tools.mode` is `direct`, tool
registration, deferred-tool behavior, provider requests, and execution remain
unchanged.

## Non-goals

- Hybrid direct/code exposure.
- Defining hybrid direct/code exposure; see [Code Mode](code-mode.md).
- Persistent cells, globals, or values between `exec` calls.
- Background jobs, `wait`, `yield`, `store`, or `load`.
- Raw/freeform provider calls.
Expand All @@ -37,27 +41,27 @@ unchanged.
```json
{
"tools": {
"codeModeOnly": true
"mode": "code_mode_only"
}
}
```

The setting resolves once to the effective `ToolMode` value `direct` or
`code_mode_only`. `ToolRegistry` and the execution surfaces consume that mode.
`exec` is only registered when the setting is enabled, so disabling the setting
also removes it from diagnostics and registry listings.
The setting resolves once to the effective `ToolMode` value. `ToolRegistry`
and the execution surfaces consume that mode. `exec` is only registered when a
code mode is enabled, so selecting `direct` also removes it from diagnostics
Comment thread
DragonnZhang marked this conversation as resolved.
and registry listings.

## Exposure policy

The registry remains the source of truth. Exposure is a view over registered
tools, never a second registry.

| Category | Model top level | `tools.*` inside `exec` |
| ------------------------------------------ | ----------------- | ----------------------- |
| `exec` | CodeModeOnly only | No |
| Direct control | Yes | No |
| Ordinary registered tool | No | Yes |
| Hidden bridge (`tool_search`, `tool_call`) | No | No |
| Category | Model top level | `tools.*` inside `exec` |
| ------------------------------------------ | ------------------------------------- | ----------------------- |
| `exec` | CodeMode and CodeModeOnly | No |
| Direct control | Yes | No |
| Ordinary registered tool | CodeMode only | Yes |
| Hidden bridge (`tool_search`, `tool_call`) | Existing behavior outside strict mode | No |

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.

[Suggestion] R1-2: N06: The rewritten exposure row conditions the table's only statement about bridge exposure on "strict mode" — a term that exists nowhere in the mode vocabulary, the code, or any other English document — and it groups two tools the code exposes differently.

ToolMode has exactly three values, direct / code_mode / code_mode_only (packages/core/src/tools/code-mode.ts:22-26); no setting, code path, or EN doc defines a "strict mode", and a repo-wide grep over docs/ finds the phrase only on this line (the ZH twin's 严格模式 is glossed only in a different file, docs/design/code-mode.zh-CN.md:104: "tools.mode: "code_mode_only" 选择严格模式"). A user on tools.mode: "code_mode" therefore cannot tell from this table whether hybrid counts as strict, i.e. whether tool_search/tool_call are declared for their session — and the shipped rule splits the pair the row keeps together: tool_search is a top-level direct control under CodeModeOnly (DIRECT_ONLY_TOOLS, code-mode.ts:39-51, admitted by the exposure === 'direct-only' filter in getCodeModeFunctionDeclarations, tool-registry.ts:1126-1131), while tool_call is hidden in every mode (const HIDDEN_TOOLS = new Set<string>(['tool_call']);, code-mode.ts:38). The same file's unchanged Status paragraph states that split correctly ("tool_search is now a top-level direct control … tool_call stays hidden", code-mode-only.md:12-16), so the table and the Status paragraph disagree about whether the two bridge tools share a rule. Every other row in this table names modes explicitly ("CodeMode and CodeModeOnly", "CodeMode only"), making this the one cell a reader cannot resolve to a tools.mode value.

Witness:

`Source: [probe]` sweep — `grep -rn 'strict mode|strict \`|严格模式' **/*.md` over the whole worktree: as a **tool-mode** term the phrase occurs exactly **once** in EN docs (this row) and once in its ZH twin; every other hit is an unrelated domain (TypeScript strict mode, Ajv strict, screen-reader strict, `parseLastEventId`). `ToolMode` has three values (code-mode.ts:22-26) and no setting or code path names one "strict". Mitigation I found and the finder did not weigh: the sibling EN doc this same PR adds glosses it adjectivally — `code-mode.md:22` "the strict `CodeModeOnly` exposure policy" — and

Suggested fix: Replace the cell with mode names and split the pair, e.g. two rows — | tool_search | CodeModeOnly top level; existing Direct/CodeMode behavior | No | and | tool_call | Hidden in every mode | No | — and mirror the change in docs/design/code-mode-only.zh-CN.md:54.

The fix must not violate this existing fact: const HIDDEN_TOOLS = new Set<string>(['tool_call']); and const DIRECT_ONLY_TOOLS = new Set<string>([ToolNames.TOOL_SEARCH, …]) — packages/core/src/tools/code-mode.ts:38-51; the rewritten rows must keep tool_call hidden in every mode and tool_search top-level under CodeModeOnly, matching docs/design/code-mode-only.md:12-16.

Acceptance criterion: N/A (documentation prose; no guard, branch or behaviour to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

改写后的 exposure 表格行把该表唯一一句关于 bridge 暴露的说明限定在 "strict mode" 下——而这个词在模式词表、代码和其余文档中都不存在(真实枚举是 direct / code_mode / code_mode_only)。读者无法把它对应到任何可配置取值。

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

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.

[Suggestion] R1-2: The new Chinese translation states the opposite of the English original (docs/design/code-mode-only.md:144-148) on Code Mode shell concurrency: EN says "Code Mode Bash calls bypass the read-only command classifier, with the model responsible for keeping dependent calls sequential. Other tools retain their existing concurrency classification"; ZH says the batch is "由现有的只读并发分类器处理" and drops the Bash carve-out entirely.

A reader working from the ZH design concludes that shell calls submitted together inside one exec (await Promise.all([...])) are serialized unless the read-only command checker classifies them as read-only. The shipped behavior is the reverse: packages/core/src/core/coreToolScheduler.ts:1557-1563 returns true for source === 'code_mode' + ToolNames.SHELL + Kind.Execute before isShellCommandReadOnly is ever consulted, so two dependent mutating Bash calls in one batch run in parallel. Someone reasoning about ordering safety, or reviewing a nested-call bug from the ZH doc, starts from a false premise; the ZH doc also contradicts docs/design/code-mode-concurrency.zh-CN.md:20-21 ("Code Mode 中的 Bash 调用跳过只读命令判定"), so the two Chinese designs disagree with each other.

Witness:

driving the real `isToolCallConcurrencySafe` (coreToolScheduler.ts:1546-1564) with the same args, varying only `source`: ``` mutating shell (npm install && git push origin main), source=code_mode -> true mutating shell (npm install && git push origin main), source=model -> false read-only shell (git log --oneline -5), source=code_mode -> true read-only shell (git log --oneline -5), source=model -> true ``` i.e. code-mode Bash is classified concurrency-safe *before* `isShellCommandReadOnly` is consulted — the EN sentence is right, the new ZH sentence is the inverse. ---

Suggested fix: Translate the two missing EN sentences into the ZH paragraph, e.g. replace "…Promise.all 调用会进入同一个 batch,由现有的只读并发分类器处理。" with "…Promise.all 调用会进入同一个 batch。Code Mode 中的 Bash 调用跳过只读命令判定,由模型负责让存在依赖的调用保持串行;其他工具沿用现有的并发分类。"

The fix must not violate this existing fact: packages/core/src/core/coreToolScheduler.ts:1557 — // Code Mode lets the model batch independent shell calls explicitly. guarding if (source === 'code_mode' && canonicalName === ToolNames.SHELL && kind === Kind.Execute) { return true; }; the corrected wording must not reassert read-only classification for code-mode Bash, which docs/design/code-mode-concurrency.zh-CN.md:20 also forbids.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文翻译在 Code Mode shell 并发这一点上与英文原文相反:英文说 "Code Mode Bash calls bypass the read-only command classifier",中文写成"由现有的只读并发分类器处理",恰好把豁免说成了适用。同仓库的 code-mode-concurrency.zh-CN.md:21 与英文一致,可证这是翻译错误。实测 isToolCallConcurrencySafe 对同样的 mutating shell 命令在 source=code_mode 时返回 true、source=model 时返回 false。

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

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.

[Suggestion] R1-2: N06: The rewritten exposure row conditions the table's only statement about bridge exposure on "strict mode" — a term that exists nowhere in the mode vocabulary, the code, or any other English document — and it groups two tools the code exposes differently.

ToolMode has exactly three values, direct / code_mode / code_mode_only (packages/core/src/tools/code-mode.ts:22-26); no setting, code path, or EN doc defines a "strict mode", and a repo-wide grep over docs/ finds the phrase only on this line (the ZH twin's 严格模式 is glossed only in a different file, docs/design/code-mode.zh-CN.md:104: "tools.mode: "code_mode_only" 选择严格模式"). A user on tools.mode: "code_mode" therefore cannot tell from this table whether hybrid counts as strict, i.e. whether tool_search/tool_call are declared for their session — and the shipped rule splits the pair the row keeps together: tool_search is a top-level direct control under CodeModeOnly (DIRECT_ONLY_TOOLS, code-mode.ts:39-51, admitted by the exposure === 'direct-only' filter in getCodeModeFunctionDeclarations, tool-registry.ts:1126-1131), while tool_call is hidden in every mode (const HIDDEN_TOOLS = new Set<string>(['tool_call']);, code-mode.ts:38). The same file's unchanged Status paragraph states that split correctly ("tool_search is now a top-level direct control … tool_call stays hidden", code-mode-only.md:12-16), so the table and the Status paragraph disagree about whether the two bridge tools share a rule. Every other row in this table names modes explicitly ("CodeMode and CodeModeOnly", "CodeMode only"), making this the one cell a reader cannot resolve to a tools.mode value.

Witness:

`Source: [probe]` sweep — `grep -rn 'strict mode|strict \`|严格模式' **/*.md` over the whole worktree: as a **tool-mode** term the phrase occurs exactly **once** in EN docs (this row) and once in its ZH twin; every other hit is an unrelated domain (TypeScript strict mode, Ajv strict, screen-reader strict, `parseLastEventId`). `ToolMode` has three values (code-mode.ts:22-26) and no setting or code path names one "strict". Mitigation I found and the finder did not weigh: the sibling EN doc this same PR adds glosses it adjectivally — `code-mode.md:22` "the strict `CodeModeOnly` exposure policy" — and

Suggested fix: Replace the cell with mode names and split the pair, e.g. two rows — | tool_search | CodeModeOnly top level; existing Direct/CodeMode behavior | No | and | tool_call | Hidden in every mode | No | — and mirror the change in docs/design/code-mode-only.zh-CN.md:54.

The fix must not violate this existing fact: const HIDDEN_TOOLS = new Set<string>(['tool_call']); and const DIRECT_ONLY_TOOLS = new Set<string>([ToolNames.TOOL_SEARCH, …]) — packages/core/src/tools/code-mode.ts:38-51; the rewritten rows must keep tool_call hidden in every mode and tool_search top-level under CodeModeOnly, matching docs/design/code-mode-only.md:12-16.

Acceptance criterion: N/A (documentation prose; no guard, branch or behaviour to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

改写后的 exposure 表格行把该表唯一一句关于 bridge 暴露的说明限定在 "strict mode" 下——而这个词在模式词表、代码和其余文档中都不存在(真实枚举是 direct / code_mode / code_mode_only)。读者无法把它对应到任何可配置取值。

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


The direct-control allowlist is centralized and deliberately small. It covers
user interaction (`ask_user_question`), delegation (`agent`), terminal output
Expand All @@ -80,17 +84,20 @@ Before each provider tool sync, the `exec` description is generated from the
current registry. Tools are sorted by canonical name. A name is normalized to
a JavaScript property by replacing invalid identifier characters and prefixing
names that begin with a digit. If two canonical names normalize to the same
property, the lexicographically first name wins and one warning names the
property, an exact canonical match wins over rewritten names. If neither is an
exact match, the lexicographically first name wins. The description names the
Comment on lines +87 to +88

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.

[Suggestion] R1-3: N01: The sentence this diff rewrote (pre-diff: "one warning names the omitted collision") claims the exec description names the dropped binding, but in CodeModeOnly with tool_search registered — the shipped default this same diff's new blockquote declares ("Only discovers schemas through top-level tool_search") — buildExecDescription suppresses the collision block entirely. The pre-diff wording was the accurate one; the new one is false for the mode the document is about.

A registry holding mcp__srv__get-data and mcp__srv__get_data normalizes both to mcp__srv__get_data, so planCodeModeBindings keeps the exact match and records { jsName, kept: 'mcp__srv__get_data', omitted: 'mcp__srv__get-data' } (packages/core/src/tools/code-mode.ts:111-124). In a tools.mode: "code_mode_only" session, getCodeModeFunctionDeclarations computes searchAvailable = !!this.getTool(TOOL_SEARCH) && (!allowedNames || allowedNames.has(TOOL_SEARCH)) → true (packages/core/src/tools/tool-registry.ts:1121-1123, passed at :1135), and buildExecDescription sets const searchAvailable = codeModeOnly && (options.searchAvailable ?? false) → true (code-mode.ts:253), so collisionText = (searchAvailable ? [] : plan.collisions) → [] (code-mode.ts:264) and the description ends with no Name collisions: block (code-mode.ts:325). The repo's own test pins that suppression: packages/core/src/code-mode/code-mode.test.ts:723-739 registers exec + tool_search under code_mode_only with hidden-tool/hidden_tool and asserts expect(description).not.toContain('hidden-tool') and not.toContain('hidden_tool'). The only surface that names the drop is debugLogger.warn inside warnCodeModeCollisions (tool-registry.ts:1158-1166) — debug-gated, not model-facing. Concrete cost: the model is never told the binding was dropped in the default Only configuration, and the maintainer triaging "tool X is unreachable through exec / exec called the wrong MCP tool" reads this document as the normative collision contract, goes looking for a missing line in the description generator, and finds code that is behaving exactly as written. The same false sentence ships in the new Chinese twin ("描述会指出被省略的冲突项。", code-mode-only.zh-CN.md:73-74).

Witness:

`Source: [probe]` — CodeModeOnly registry with `exec` + `tool_search` + colliding `get-data`/`get_data` (non-deferred, so deferral filtering cannot explain the absence): `N01-only {"collisions":[{"jsName":"get_data","kept":"get_data","omitted":"get-data"}],"descriptionHasCollisionsBlock":false,"descriptionNamesOmittedTool":false,"declaredNames":["exec","tool_search"]}` Positive controls (the probe *can* see the block): `N01-only-nosearch {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":true}` · `N01-hybrid {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":t

Suggested fix: Scope the claim to the configuration where it holds and name the other surface, e.g. "When search is unavailable in the current scope, the description names the omitted collision; when tool_search is available the collision block is left out of the description and the drop is logged once through the debug logger." Apply the identical correction to docs/design/code-mode-only.zh-CN.md:73-74.

The fix must not violate this existing fact: const collisionText = (searchAvailable ? [] : plan.collisions) — packages/core/src/tools/code-mode.ts:264, with const searchAvailable = codeModeOnly && (options.searchAvailable ?? false); at :253. The corrected sentence must keep the hybrid case true: decorateCodeModeDeclarations passes codeModeOnly: false (packages/core/src/tools/tool-registry.ts:1087-1092), so hybrid descriptions always emit the block.

Acceptance criterion: N/A (documentation prose). The behaviour the corrected sentence must match is already pinned from both sides: packages/core/src/code-mode/code-mode.test.ts:723-739 (CodeModeOnly + tool_search → neither collision name appears) and :563-591 'describes normalized-name collisions on the hybrid surface' (hybrid description contains '- read-file is omitted because it collides with read_file as tools.read_file.'). Please prove it by removing the fix and confirming that test goes red.

中文说明

本次改写后的句子声称 exec 的 description 会点出被丢弃的 binding,但在 CodeModeOnly 下被丢弃的 binding 恰恰不会进入 description;改写前的措辞("one warning names the omitted collision")才是准确的——warnCodeModeCollisions 在所有模式下都会触发。中英文两份设计文档同句同错。

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

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.

[Suggestion] R1-3: The new ZH version's 状态 section omits the English Status paragraph at docs/design/code-mode-only.md:12-16 ("Partly superseded by Lazy Code Mode: tool_search is now a top-level direct control, and exec omits deferred tool signatures while search is available. The exposure table and the deferred-schema paragraph below describe this MVP; tool_call stays hidden."), and the ZH Sandbox paragraph at line 103 likewise drops EN's clause "whose own scheduler/ACP timeouts remain authoritative" (docs/design/code-mode-only.md:119-121).

The ZH reader never learns that tool_call remains hidden under current behavior, nor that the 暴露策略 table and the deferred-schema paragraph below it describe a superseded MVP rather than shipped behavior — the top blockquote only supersedes "隐藏 tool_search / 始终完整 schema", not the table as a whole. Concretely the ZH table row "隐藏 bridge(tool_search、tool_call)| 严格模式之外沿用既有行为" plus ZH line 76 "CodeModeOnly 会隐藏 tool_search" read as current policy, which the lazy-loading design replaced. At ZH:103 the dropped clause leaves the reader with no statement of which timeout governs while the guest CPU budget and watchdog are paused on a host tool. This violates the repo's own rule in docs/design/README.md: "neither version omits decisions, limitations, acceptance criteria, or follow-up work" and "Do not leave one version with an earlier requirement … that the other version has already answered."

Witness:

`grep -n "Partly superseded|stays hidden|scheduler/ACP timeouts remain authoritative" code-mode-only.md code-mode-only.zh-CN.md` → **3 hits, all in the EN file** (`code-mode-only.md:12`, `:15`, `:120`), zero in the ZH file; and `sed -n '7,11p' code-mode-only.zh-CN.md` → ``` ## 状态 已为 [#10377](…) 实现。 该功能为可选功能,默认关闭。 ``` So the ZH 状态 carries neither the "Partly superseded by Lazy Code Mode … The exposure table and the deferred-schema paragraph below describe this MVP; `tool_call` stays hidden" scope note (EN 12-15) nor, at ZH:103-104, EN's "whose own scheduler/ACP timeouts remain authoritative" (E

Suggested fix: Translate the EN Status paragraph into 状态 (adding the tool_call 仍隐藏 statement and the scope note that the exposure table and deferred-schema paragraph describe the MVP), and add "已注册 host 工具自身的 scheduler/ACP timeout 仍然生效" to the ZH watchdog sentence at line 103.

The fix must not violate this existing fact: docs/design/code-mode-only.md:12-16 is the source text the translation must carry, and docs/design/README.md ("Keep section order and heading levels aligned") requires it land in the existing 状态 section rather than as a new heading.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文版的「状态」小节漏掉了英文 Status 段落(docs/design/code-mode-only.md:12-16)中"Partly superseded by Lazy Code Mode"这一段,导致中英两版结构不同步,违反 AGENTS.md 对双语文档"完整且同步"的要求。

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

Comment on lines +87 to +88

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.

[Suggestion] R1-3: N01: The sentence this diff rewrote (pre-diff: "one warning names the omitted collision") claims the exec description names the dropped binding, but in CodeModeOnly with tool_search registered — the shipped default this same diff's new blockquote declares ("Only discovers schemas through top-level tool_search") — buildExecDescription suppresses the collision block entirely. The pre-diff wording was the accurate one; the new one is false for the mode the document is about.

A registry holding mcp__srv__get-data and mcp__srv__get_data normalizes both to mcp__srv__get_data, so planCodeModeBindings keeps the exact match and records { jsName, kept: 'mcp__srv__get_data', omitted: 'mcp__srv__get-data' } (packages/core/src/tools/code-mode.ts:111-124). In a tools.mode: "code_mode_only" session, getCodeModeFunctionDeclarations computes searchAvailable = !!this.getTool(TOOL_SEARCH) && (!allowedNames || allowedNames.has(TOOL_SEARCH)) → true (packages/core/src/tools/tool-registry.ts:1121-1123, passed at :1135), and buildExecDescription sets const searchAvailable = codeModeOnly && (options.searchAvailable ?? false) → true (code-mode.ts:253), so collisionText = (searchAvailable ? [] : plan.collisions) → [] (code-mode.ts:264) and the description ends with no Name collisions: block (code-mode.ts:325). The repo's own test pins that suppression: packages/core/src/code-mode/code-mode.test.ts:723-739 registers exec + tool_search under code_mode_only with hidden-tool/hidden_tool and asserts expect(description).not.toContain('hidden-tool') and not.toContain('hidden_tool'). The only surface that names the drop is debugLogger.warn inside warnCodeModeCollisions (tool-registry.ts:1158-1166) — debug-gated, not model-facing. Concrete cost: the model is never told the binding was dropped in the default Only configuration, and the maintainer triaging "tool X is unreachable through exec / exec called the wrong MCP tool" reads this document as the normative collision contract, goes looking for a missing line in the description generator, and finds code that is behaving exactly as written. The same false sentence ships in the new Chinese twin ("描述会指出被省略的冲突项。", code-mode-only.zh-CN.md:73-74).

Witness:

`Source: [probe]` — CodeModeOnly registry with `exec` + `tool_search` + colliding `get-data`/`get_data` (non-deferred, so deferral filtering cannot explain the absence): `N01-only {"collisions":[{"jsName":"get_data","kept":"get_data","omitted":"get-data"}],"descriptionHasCollisionsBlock":false,"descriptionNamesOmittedTool":false,"declaredNames":["exec","tool_search"]}` Positive controls (the probe *can* see the block): `N01-only-nosearch {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":true}` · `N01-hybrid {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":t

Suggested fix: Scope the claim to the configuration where it holds and name the other surface, e.g. "When search is unavailable in the current scope, the description names the omitted collision; when tool_search is available the collision block is left out of the description and the drop is logged once through the debug logger." Apply the identical correction to docs/design/code-mode-only.zh-CN.md:73-74.

The fix must not violate this existing fact: const collisionText = (searchAvailable ? [] : plan.collisions) — packages/core/src/tools/code-mode.ts:264, with const searchAvailable = codeModeOnly && (options.searchAvailable ?? false); at :253. The corrected sentence must keep the hybrid case true: decorateCodeModeDeclarations passes codeModeOnly: false (packages/core/src/tools/tool-registry.ts:1087-1092), so hybrid descriptions always emit the block.

Acceptance criterion: N/A (documentation prose). The behaviour the corrected sentence must match is already pinned from both sides: packages/core/src/code-mode/code-mode.test.ts:723-739 (CodeModeOnly + tool_search → neither collision name appears) and :563-591 'describes normalized-name collisions on the hybrid surface' (hybrid description contains '- read-file is omitted because it collides with read_file as tools.read_file.'). Please prove it by removing the fix and confirming that test goes red.

中文说明

本次改写后的句子声称 exec 的 description 会点出被丢弃的 binding,但在 CodeModeOnly 下被丢弃的 binding 恰恰不会进入 description;改写前的措辞("one warning names the omitted collision")才是准确的——warnCodeModeCollisions 在所有模式下都会触发。中英文两份设计文档同句同错。

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

omitted collision.

The description defines:

- a fresh async JavaScript execution environment;
- `tools.<normalizedName>(args)` for nested calls;
- `ALL_TOOLS`, including canonical and JavaScript names;
- `text(value)`, `image(value)`, `audio(value)`, and `exit()`;
- `text(value)`, `image(value)`, `audio(value)`, `generatedImage(value)`,
`setTimeout(callback, delayMs)`, `clearTimeout(timeoutId)`, and `exit()`;
- TypeScript-like signatures generated deterministically from JSON Schema;
- the absence of Node.js, imports, network APIs, timers, and persistent state.
- the absence of Node.js, `process`, `require`, filesystem, network, imports,
`console`, `WebAssembly`, `Atomics`, and persistent state. Pending timers do not keep `exec` alive by themselves.

The nested call returns a JSON-safe object containing the real call id, tool
name, status, output, and structured content. Failed and cancelled calls reject
Expand All @@ -105,8 +112,8 @@ configuration. The parent maps JavaScript names back to canonical registry
names and dispatches each call.

The guest has no Node globals, `require`, `process`, filesystem, sockets,
module loader, `console`, timers, `Atomics`, `SharedArrayBuffer`, or
`WebAssembly`. Dynamic and static imports fail because no module loader is
module loader, `console`, `Atomics`, `SharedArrayBuffer`, or `WebAssembly`.
Dynamic and static imports fail because no module loader is
installed. Runtime memory and stack limits are fixed. QuickJS's interrupt hook
enforces a guest CPU budget. That budget and the parent's fallback watchdog
pause while the guest is suspended on registered host tools, whose own
Expand Down Expand Up @@ -162,14 +169,13 @@ OpenAI-compatible, and Anthropic adapters all receive the structured `exec`
declaration without provider-specific prompting.

Filtered subagent declarations apply the same policy. For a read-only teammate
or a fork with an execution allowlist, `exec` is the audited gateway while the
exact allowed nested names are carried in its invocation context. The same set
generates the description and is checked again before Core dispatch, so an
explicit allowlist can narrow code-mode-callable nested tools without becoming
prompt-only policy, exposing a hidden bridge, or making `exec` recursive.
For cache-compatible forks, an inherited `exec` declaration represents its
ordinary bindings: an omitted `fork_tools` inherits them, while an explicit
list replaces them with the requested subset.
or a fork with an execution allowlist, `exec` is the audited gateway and the
nested names carried in its invocation context are checked again before Core
dispatch. Explicit ordinary-tool entries narrow that nested set. An inherited
or explicitly allowed `exec` instead represents all surviving ordinary
code-mode-callable bindings, while the agent's own `tools` list still narrows
its direct surface. Hidden bridges remain unavailable and `exec` cannot call
itself.

## Failure and rollback

Expand All @@ -178,9 +184,9 @@ closed before scheduling. Invalid arguments continue to fail in the normal
execution chain. A sandbox startup, protocol, timeout, memory, or teardown
failure becomes an `exec` tool error.

Rollback is setting `tools.codeModeOnly` to false. No session migration or
registry cleanup is required because code mode has no persistent state and the
ordinary registry was never replaced.
Rollback is setting `tools.mode` to `direct`. No session migration or registry
cleanup is required because code mode has no persistent state and the ordinary
registry was never replaced.

## Verification

Expand Down
172 changes: 172 additions & 0 deletions docs/design/code-mode-only.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# CodeModeOnly MVP

> 当前行为:Only 模式通过顶层 tool_search 按需加载 schema,并通过 exec 调用。搜索在当前范围不可用时才提供完整签名;tools.eager 可缩小初始声明。Hybrid 继续使用直接工具加 exec,保留其 bridge 与 eager 权限边界。本文以下 MVP 中“隐藏 tool_search / 始终完整 schema”的旧约定已由 [延迟加载设计](lazy-code-mode.zh-CN.md) 取代。

[English](code-mode-only.md) | [简体中文](code-mode-only.zh-CN.md)

## 状态

已为 [#10377](https://github.com/QwenLM/qwen-code/issues/10377) 实现。
该功能为可选功能,默认关闭。
Comment on lines +9 to +10

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.

[Suggestion] R1-3: The new ZH version's 状态 section omits the English Status paragraph at docs/design/code-mode-only.md:12-16 ("Partly superseded by Lazy Code Mode: tool_search is now a top-level direct control, and exec omits deferred tool signatures while search is available. The exposure table and the deferred-schema paragraph below describe this MVP; tool_call stays hidden."), and the ZH Sandbox paragraph at line 103 likewise drops EN's clause "whose own scheduler/ACP timeouts remain authoritative" (docs/design/code-mode-only.md:119-121).

The ZH reader never learns that tool_call remains hidden under current behavior, nor that the 暴露策略 table and the deferred-schema paragraph below it describe a superseded MVP rather than shipped behavior — the top blockquote only supersedes "隐藏 tool_search / 始终完整 schema", not the table as a whole. Concretely the ZH table row "隐藏 bridge(tool_search、tool_call)| 严格模式之外沿用既有行为" plus ZH line 76 "CodeModeOnly 会隐藏 tool_search" read as current policy, which the lazy-loading design replaced. At ZH:103 the dropped clause leaves the reader with no statement of which timeout governs while the guest CPU budget and watchdog are paused on a host tool. This violates the repo's own rule in docs/design/README.md: "neither version omits decisions, limitations, acceptance criteria, or follow-up work" and "Do not leave one version with an earlier requirement … that the other version has already answered."

Witness:

`grep -n "Partly superseded|stays hidden|scheduler/ACP timeouts remain authoritative" code-mode-only.md code-mode-only.zh-CN.md` → **3 hits, all in the EN file** (`code-mode-only.md:12`, `:15`, `:120`), zero in the ZH file; and `sed -n '7,11p' code-mode-only.zh-CN.md` → ``` ## 状态 已为 [#10377](…) 实现。 该功能为可选功能,默认关闭。 ``` So the ZH 状态 carries neither the "Partly superseded by Lazy Code Mode … The exposure table and the deferred-schema paragraph below describe this MVP; `tool_call` stays hidden" scope note (EN 12-15) nor, at ZH:103-104, EN's "whose own scheduler/ACP timeouts remain authoritative" (E

Suggested fix: Translate the EN Status paragraph into 状态 (adding the tool_call 仍隐藏 statement and the scope note that the exposure table and deferred-schema paragraph describe the MVP), and add "已注册 host 工具自身的 scheduler/ACP timeout 仍然生效" to the ZH watchdog sentence at line 103.

The fix must not violate this existing fact: docs/design/code-mode-only.md:12-16 is the source text the translation must carry, and docs/design/README.md ("Keep section order and heading levels aligned") requires it land in the existing 状态 section rather than as a new heading.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文版的「状态」小节漏掉了英文 Status 段落(docs/design/code-mode-only.md:12-16)中"Partly superseded by Lazy Code Mode"这一段,导致中英两版结构不同步,违反 AGENTS.md 对双语文档"完整且同步"的要求。

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


## 目标

新增 `tools.mode: "code_mode_only"` 设置,用一个 `exec` JavaScript 工具和少量
必须保留为直接调用的控制面工具,取代面向模型的普通工具面。`exec` 中的代码可通过
`tools.<name>(args)` 调用普通工具,同时不会绕过 Qwen Code 的校验、权限、审批、
hook、遥测、取消、并发或输出预算。

直接模式是兼容性边界:当 `tools.mode` 为 `direct` 时,工具注册、延迟工具行为、
provider 请求和执行均保持不变。

## 非目标

- 定义混合的直接调用和代码调用;详见 [Code Mode](code-mode.zh-CN.md)。
- 在多次 `exec` 调用间持久化 cell、全局变量或值。
- 后台任务、`wait`、`yield`、`store` 或 `load`。
- 原始/freeform provider 调用。
- 在 code mode 中提供 `tool_search` 或 `tool_call` bridge。
- 提供兼容 Node.js 的 sandbox。

## 配置

```json
{
"tools": {
"mode": "code_mode_only"
}
}
```

该设置会解析一次,得到有效的 `ToolMode` 值;`ToolRegistry` 和各执行面使用这一
模式。只有启用某个 code mode 时才注册 `exec`,因此选择 `direct` 也会将它从
诊断信息和 registry 列表中移除。

## 暴露策略

registry 仍是事实来源。暴露只是注册工具之上的视图,而不是第二套 registry。

| 类别 | 模型顶层调用 | `exec` 内的 `tools.*` |
| ----------------------------------------- | ------------------------ | --------------------- |
| `exec` | CodeMode 和 CodeModeOnly | 否 |
| 直接控制工具 | 是 | 否 |
| 已注册的普通工具 | 仅 CodeMode | 是 |
| 隐藏 bridge(`tool_search`、`tool_call`) | 严格模式之外沿用既有行为 | 否 |

直接控制 allowlist 集中维护且刻意保持精简。它覆盖用户交互
(`ask_user_question`)、委派(`agent`)、终止输出约定、
plan/goal/task/team/worktree/session 控制,以及生命周期无法安全隐藏在解释执行程序
后的 ACP host 控制。新工具默认可在 code mode 中调用;增加仅直接调用或隐藏工具时,
必须显式修改策略。

延迟工具保持注册状态和延迟 registry 状态,并可从 `exec` 调用。生成的 `exec`
描述仍会携带这些工具的完整 schema,以及它们在 `ALL_TOOLS` 中的名称和描述:
CodeModeOnly 会隐藏 `tool_search`,嵌套调用也不会以 `functionCall` 出现在历史记录
中,因此后续 reveal 无法补充描述里遗漏的 schema。CodeModeOnly 会跳过延迟预加载
和 ToolSearch 提醒,因为二者都不属于它面向模型的协议。

## 确定性的 JavaScript 接口

每次向 provider 同步工具前,都会从当前 registry 生成 `exec` 描述。工具按规范
名称排序。名称会通过替换无效标识符字符来规范化为 JavaScript 属性;如果名称以
数字开头,还会添加前缀。如果两个规范名称映射到同一个属性,优先保留与属性精确
一致的规范名称;若都不是精确匹配,则字典序靠前的名称胜出。描述会指出被省略的
冲突项。

描述会定义:

- 全新的异步 JavaScript 执行环境;
- 用于嵌套调用的 `tools.<normalizedName>(args)`;
- 包含规范名称和 JavaScript 名称的 `ALL_TOOLS`;
- `text(value)`、`image(value)`、`audio(value)`、`generatedImage(value)`、
`setTimeout(callback, delayMs)`、`clearTimeout(timeoutId)` 和 `exit()`;
- 从 JSON Schema 确定性生成的类 TypeScript 签名;
- 不提供 Node.js、`process`、`require`、文件系统、网络、import、`console`、
`WebAssembly`、`Atomics` 和持久状态。待处理的 timer
本身不会让 `exec` 保持运行。

嵌套调用返回一个 JSON-safe 对象,其中包含真实 call id、工具名、状态、输出和
structured content。调用失败或取消时,guest promise 会使用 scheduler/ACP 错误
reject。

## Sandbox 与传输

`exec` 在独立子进程中运行编译为 WebAssembly 的 QuickJS。每次调用都会创建全新
的 QuickJS runtime 和 context。子进程通过 stdio 接收精简的 framed JSON 协议;
其中不包含工具实现或 Qwen 配置。父进程把 JavaScript 名称映射回规范 registry
名称,并分派每次调用。

guest 不提供 Node 全局变量、`require`、`process`、文件系统、socket、模块加载器、
`console`、`Atomics`、`SharedArrayBuffer` 或 `WebAssembly`。由于没有安装
模块加载器,动态和静态 import 都会失败。runtime 的内存和 stack 限制固定。
QuickJS 的 interrupt hook 会限制 guest CPU 预算。当 guest 挂起等待已注册的 host
工具时,该预算和父进程的兜底 watchdog 会暂停;在 guest job 再次运行前恢复。
因此,长时间 build 可以继续使用工具声明的 timeout,同时 guest CPU 死循环无法
逃逸固定预算。源码、协议 frame、helper 输出和最终结果都有上限。

取消操作会中止所有嵌套调用并终止子进程。顶层 promise settled 后,也会在 teardown
前取消未 await 的嵌套调用。子进程、timer、promise handle 或 guest 全局变量都不
会在调用结束后存活。

子进程只接收最小化且经过清理的环境,guest 无法检查该环境。独立进程为解释器故障
提供纵深防御;QuickJS/WASM 是 guest 的 capability 边界。

## 重入式分派

`CoreToolScheduler.schedule()` 无法递归调用:子调用会排在仍在运行的父调用之后,
从而形成死锁。因此,scheduler 会在 invocation 边界绑定 async-local
`ToolCallRuntime` context,`exec` 只与该 context 通信。

scheduler runtime 会将同一 event-loop turn 中收到的嵌套调用合并为一批,并通过
使用相同 `Config` 和 observer 配置的 sibling scheduler 执行。这样无需直接调用
`tool.execute()`,仍能沿用现有的构建/校验、权限、确认、hook、执行、截断、遥测和
并发链路。guest 的连续 await 会生成连续 batch;`Promise.all` 调用会进入同一个
batch,由现有的只读并发分类器处理。嵌套 request id 包含父 id,并携带
Comment on lines +123 to +124

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.

[Suggestion] R1-2: The new Chinese translation states the opposite of the English original (docs/design/code-mode-only.md:144-148) on Code Mode shell concurrency: EN says "Code Mode Bash calls bypass the read-only command classifier, with the model responsible for keeping dependent calls sequential. Other tools retain their existing concurrency classification"; ZH says the batch is "由现有的只读并发分类器处理" and drops the Bash carve-out entirely.

A reader working from the ZH design concludes that shell calls submitted together inside one exec (await Promise.all([...])) are serialized unless the read-only command checker classifies them as read-only. The shipped behavior is the reverse: packages/core/src/core/coreToolScheduler.ts:1557-1563 returns true for source === 'code_mode' + ToolNames.SHELL + Kind.Execute before isShellCommandReadOnly is ever consulted, so two dependent mutating Bash calls in one batch run in parallel. Someone reasoning about ordering safety, or reviewing a nested-call bug from the ZH doc, starts from a false premise; the ZH doc also contradicts docs/design/code-mode-concurrency.zh-CN.md:20-21 ("Code Mode 中的 Bash 调用跳过只读命令判定"), so the two Chinese designs disagree with each other.

Witness:

driving the real `isToolCallConcurrencySafe` (coreToolScheduler.ts:1546-1564) with the same args, varying only `source`: ``` mutating shell (npm install && git push origin main), source=code_mode -> true mutating shell (npm install && git push origin main), source=model -> false read-only shell (git log --oneline -5), source=code_mode -> true read-only shell (git log --oneline -5), source=model -> true ``` i.e. code-mode Bash is classified concurrency-safe *before* `isShellCommandReadOnly` is consulted — the EN sentence is right, the new ZH sentence is the inverse. ---

Suggested fix: Translate the two missing EN sentences into the ZH paragraph, e.g. replace "…Promise.all 调用会进入同一个 batch,由现有的只读并发分类器处理。" with "…Promise.all 调用会进入同一个 batch。Code Mode 中的 Bash 调用跳过只读命令判定,由模型负责让存在依赖的调用保持串行;其他工具沿用现有的并发分类。"

The fix must not violate this existing fact: packages/core/src/core/coreToolScheduler.ts:1557 — // Code Mode lets the model batch independent shell calls explicitly. guarding if (source === 'code_mode' && canonicalName === ToolNames.SHELL && kind === Kind.Execute) { return true; }; the corrected wording must not reassert read-only classification for code-mode Bash, which docs/design/code-mode-concurrency.zh-CN.md:20 also forbids.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文翻译在 Code Mode shell 并发这一点上与英文原文相反:英文说 "Code Mode Bash calls bypass the read-only command classifier",中文写成"由现有的只读并发分类器处理",恰好把豁免说成了适用。同仓库的 code-mode-concurrency.zh-CN.md:21 与英文一致,可证这是翻译错误。实测 isToolCallConcurrencySafe 对同样的 mutating shell 命令在 source=code_mode 时返回 true、source=model 时返回 false。

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

`source: code_mode` 和 `parentCallId`。

嵌套 scheduler 更新会合并到所属 scheduler 的可见调用中,嵌套 id 的确认响应也会
委派给它。外层模型仍只接收完整的 `exec` 响应。

ACP 沿用其独立审计过的执行链。它的 invocation 边界会绑定相同的 runtime 接口,
嵌套分派通过 `Session.runTool` 重入。因此,ACP 顶层和嵌套调用使用相同的 ACP 权限、
审批、hook、遥测、持久化和取消机制,而不是借用 CLI scheduler 状态或直接执行工具。
ACP 会串行执行普通嵌套调用,与现有直接工具顺序一致;Core scheduler 则保留现有的
安全只读并行 batch。

## Provider 行为

所有 provider 继续使用 `ToolRegistry` 提供的 `FunctionDeclaration[]`。在 Direct
模式下,该数组与现有视图逐字节一致。在 CodeModeOnly 中,它是暴露策略生成的视图,
因此 Gemini/Qwen、OpenAI-compatible 和 Anthropic adapter 都会收到结构化的
`exec` declaration,不需要 provider 专用 prompt。

经过过滤的子智能体 declaration 使用相同策略。对于只读 teammate 或带执行
allowlist 的 fork,`exec` 是经过审计的 gateway,其 invocation context 中携带的嵌套
名称会在 Core 分派前再次校验。显式列出的普通工具会收窄该嵌套集合;继承或显式允许
的 `exec` 则代表所有仍可用的普通 code-mode-callable binding,而智能体自身的
`tools` 列表仍会收窄直接调用面。隐藏 bridge 始终不可用,`exec` 也不能调用自身。

## 失败与回滚

未知、冲突、隐藏、仅直接调用和递归请求的工具都会在调度前 fail closed。无效参数
继续在正常执行链中失败。sandbox 启动、协议、timeout、内存或 teardown 失败会转为
`exec` 工具错误。

回滚方式是将 `tools.mode` 设为 `direct`。无需迁移 session 或清理 registry,
因为 code mode 没有持久状态,且普通 registry 从未被替换。

## 验证

单元和集成测试必须覆盖:

- Direct 和 CodeModeOnly 暴露、延迟保留、冲突处理、确定性描述和显式子智能体过滤。
- 有效/无效 JavaScript、异步顺序、`Promise.all`、helper 输出、throw error、CPU
死循环、内存限制、import、不可用全局变量、隔离、输出限制、取消、未 await 工作和
递归调用。
- 嵌套权限、确认、Pre/Post hook、失败、输出预算、真实名称遥测/UI 更新、MCP 工具
和 scheduler 死锁回归。
- Gemini/Qwen、OpenAI-compatible、Anthropic、headless、interactive、subagent
和 ACP 调用面。

只有通过相关 package 测试、build、typecheck、E2E probe 和两轮干净的完整 diff
自审,才算实现完成。
Loading
Loading