mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-10-21 05:40:14 +00:00
* feat(core): allow concurrent Bash calls in Code Mode * fix(acp): preserve AUTO denial state across concurrent calls
201 lines
9.7 KiB
Markdown
201 lines
9.7 KiB
Markdown
# CodeModeOnly MVP
|
|
|
|
## Status
|
|
|
|
Implemented for [#10377](https://github.com/QwenLM/qwen-code/issues/10377).
|
|
The feature is opt-in and defaults off.
|
|
|
|
Partly superseded by [Lazy Code Mode](lazy-code-mode.md): `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.
|
|
|
|
## 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
|
|
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
|
|
registration, deferred-tool behavior, provider requests, and execution remain
|
|
unchanged.
|
|
|
|
## Non-goals
|
|
|
|
- Hybrid direct/code exposure.
|
|
- Persistent cells, globals, or values between `exec` calls.
|
|
- Background jobs, `wait`, `yield`, `store`, or `load`.
|
|
- Raw/freeform provider calls.
|
|
- Making `tool_search` or a `tool_call` bridge available inside code mode.
|
|
- A Node.js-compatible sandbox.
|
|
|
|
## Configuration
|
|
|
|
```json
|
|
{
|
|
"tools": {
|
|
"codeModeOnly": true
|
|
}
|
|
}
|
|
```
|
|
|
|
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.
|
|
|
|
## 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 |
|
|
|
|
The direct-control allowlist is centralized and deliberately small. It covers
|
|
user interaction (`ask_user_question`), delegation (`agent`), terminal output
|
|
contracts, plan/goal/task/team/worktree/session controls, and ACP host controls
|
|
whose lifecycle cannot safely be hidden behind an interpreted program. New
|
|
tools default to code-mode-callable; adding a direct-only or hidden tool
|
|
requires an explicit policy edit.
|
|
|
|
Deferred tools remain registered and callable from `exec`, and keep their
|
|
deferred registry state. The generated `exec` description nonetheless carries
|
|
their full schemas, alongside their names and descriptions in `ALL_TOOLS`:
|
|
CodeModeOnly hides `tool_search`, and a nested call never appears in history as
|
|
a `functionCall`, so no later reveal can supply a schema the description
|
|
omitted. CodeModeOnly skips deferred preload and ToolSearch reminders because
|
|
neither mechanism is part of its model-facing protocol.
|
|
|
|
## Deterministic JavaScript interface
|
|
|
|
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
|
|
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()`;
|
|
- TypeScript-like signatures generated deterministically from JSON Schema;
|
|
- the absence of Node.js, imports, network APIs, timers, and persistent state.
|
|
|
|
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
|
|
the guest promise with the scheduler/ACP error.
|
|
|
|
## Sandbox and transport
|
|
|
|
`exec` runs QuickJS compiled to WebAssembly in a dedicated child process. Each
|
|
call creates a fresh QuickJS runtime and context. The child process receives a
|
|
small framed JSON protocol over stdio; it has no tool implementations or Qwen
|
|
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
|
|
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
|
|
scheduler/ACP timeouts remain authoritative, and resume before guest jobs run
|
|
again. This lets long builds keep their declared tool timeout without allowing
|
|
guest CPU loops to escape the fixed budget. Source, protocol frames, helper
|
|
output, and the final result are bounded.
|
|
|
|
Cancellation aborts every nested call and terminates the child. Settling the
|
|
top-level promise also cancels unawaited nested calls before teardown. No child,
|
|
timer, promise handle, or guest global survives the call.
|
|
|
|
The child receives a minimal sanitized environment and cannot inspect it from
|
|
the guest. The separate process is defense in depth for interpreter failures;
|
|
QuickJS/WASM is the guest capability boundary.
|
|
|
|
## Reentrant dispatch
|
|
|
|
`CoreToolScheduler.schedule()` cannot be called recursively: a child call would
|
|
queue behind its still-running parent and deadlock. Scheduler execution
|
|
therefore binds an async-local `ToolCallRuntime` context at the invocation
|
|
boundary. `exec` only talks to that context.
|
|
|
|
The scheduler runtime batches nested calls received in one event-loop turn and
|
|
runs them through a sibling scheduler configured from the same `Config` and
|
|
observers. This preserves the existing build/validation, permission,
|
|
confirmation, hook, execution, truncation, telemetry, and concurrency path
|
|
without direct `tool.execute()` calls. Sequential guest awaits produce
|
|
sequential batches; calls submitted together enter one batch. 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. Nested request ids include the parent id and carry
|
|
`source: code_mode` plus `parentCallId`.
|
|
|
|
Nested scheduler updates are merged into the owning scheduler's visible calls,
|
|
and confirmation responses for nested ids are delegated to it. The outer model
|
|
still receives only the completed `exec` response.
|
|
|
|
ACP keeps its existing independently audited execution chain. Its invocation
|
|
boundary binds the same runtime interface, with nested dispatch re-entering
|
|
`Session.runTool`. Thus ACP top-level and nested calls use the same ACP
|
|
permissions, approvals, hooks, telemetry, persistence, and cancellation rather
|
|
than borrowing CLI scheduler state or executing tools directly. ACP serializes
|
|
ordinary nested calls, matching its existing direct-tool ordering; the Core
|
|
scheduler retains its existing safe-read parallel batches.
|
|
|
|
## Provider behavior
|
|
|
|
All providers continue to consume `FunctionDeclaration[]` from
|
|
`ToolRegistry`. In Direct mode this array is byte-for-byte the existing view.
|
|
In CodeModeOnly it is the exposure-policy view, so Gemini/Qwen,
|
|
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.
|
|
|
|
## Failure and rollback
|
|
|
|
Unknown, collided, hidden, direct-only, and recursively requested tools fail
|
|
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.
|
|
|
|
## Verification
|
|
|
|
Unit and integration coverage must include:
|
|
|
|
- Direct and CodeModeOnly exposure, deferred retention, collision handling,
|
|
deterministic descriptions, and explicit subagent filtering.
|
|
- Valid/invalid JavaScript, async sequencing, `Promise.all`, helper output,
|
|
thrown errors, CPU loops, memory limits, imports, unavailable globals,
|
|
isolation, output limits, cancellation, unawaited work, and recursive calls.
|
|
- Nested permissions, confirmation, Pre/Post hooks, failures, output budgets,
|
|
true-name telemetry/UI updates, MCP tools, and the scheduler deadlock
|
|
regression.
|
|
- Gemini/Qwen, OpenAI-compatible, Anthropic, headless, interactive, subagent,
|
|
and ACP surfaces.
|
|
|
|
The implementation is complete only after focused package tests, build,
|
|
typecheck, E2E probes, and two clean full-diff audit passes.
|