Files
tanzhenxinandShaojin Wen 9eb6bc7044 feat(core): lazy-load deferred tools in Code Mode (#12898)
* feat(core): lazy-load deferred tools in Code Mode

* refactor(core): drop unreachable Code Mode scope check in tool_search

In Code Mode, returnSchemas resolves names only against the binding keys,
so a resolved name is always a binding and the follow-up bindings.has()
check could never fail. Note where the scope is actually enforced instead.

* docs: align older Code Mode docs and comments with lazy discovery

Mark the CodeModeOnly MVP and context-tools designs as partly superseded
now that tool_search is a top-level direct control, scope the sub-agent
bridge paragraph to Direct mode, and correct two test comments that still
said both bridge tools are hidden in Code Mode.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-09-29 03:00:24 +00:00

6.7 KiB

Lazy Code Mode

English | 简体中文

Problem and scope

CodeModeOnly currently puts every callable tool's signature and description in the initial exec declaration, including deferred tools. This removes the prompt-size benefit of deferral. The existing tool_call bridge also describes target arguments as a generic object; issue #12889 reports repeated empty arguments with Responses providers. Code Mode lets the model express these arguments as JavaScript, with ordinary runtime validation still enforced.

This change makes the existing experimental tools.codeModeOnly mode load deferred descriptions and schemas through tool_search. Direct mode keeps its current protocol. This extends the original CodeModeOnly MVP's scope.

Design

Declarations and execution

Expose tool_search at the top level alongside exec and existing direct controls. Keep tool_call hidden. When search is available in the current tool scope, include only eager or explicitly visible bindings in the exec description, including its embedded metadata. Describe how to discover other tools without listing their names or descriptions. Keep the complete binding plan and runtime ALL_TOOLS: schema visibility does not grant execution rights.

When search is disabled or excluded from an agent's scope, retain full signatures in that scope's exec declaration. This preserves access for existing configurations. Use the existing deferred state, tools.visible, and tools.eager; add no setting. Deferred preloading and startup tool-list reminders stay disabled in CodeModeOnly.

Discovery

Reuse ToolSearch keyword scoring, exact lookup, result limits, and JSON escaping. In CodeModeOnly, search only tools in the current callable binding plan. Return the original description and JSON Schema together with the actual normalized JavaScript name and signature, and instructions to call it through exec in a later turn. The raw schema remains authoritative for constraints that the TypeScript-like signature cannot express. Direct-only and collided tools are excluded from Code Mode discovery.

Search does not reveal tools in the registry or rewrite declarations. Its results enter the conversation as tool output. With otherwise unchanged configuration, searching and invoking deferred tools leave the provider tool array unchanged. Provider cache hits remain subject to provider behavior.

Invocation guidance and context compression

Describe tool_search as a separate top-level call, outside the JavaScript runtime. Nested calls use the returned jsName exactly. Reuse schemas already present in the current context; if a schema is missing after compression, discover it again before constructing arguments.

In Code Mode, an exact-name miss explains that select: requires the registered name, including the mcp__<server>__<tool> prefix for MCP tools, and suggests keyword discovery without select:. Keep exact lookup semantics and scoped visibility unchanged; the hint does not enumerate other tools. Direct mode retains its current response.

Compression can replace earlier discovery results with a summary. Verify rediscovery, execution and subsequent reuse after an actual history rewrite. The declarations must remain stable across compression; history replacement can reduce cache reuse, so observe the retained prefix and cache rebuilding separately from ordinary append-only turns. Use the existing compression pipeline without adding schema persistence or another recovery mechanism.

Agent scopes

Treat search as a discovery gateway for a scoped agent, honoring explicit disallowed-tool rules. Carry the same effective nested-tool allowlist used by exec on search requests through the existing tool-call runtime context. Filter both keyword and exact lookup against that scoped binding plan. Permission-deferred tools remain eligible for an agent that is allowed to use them. Search cannot broaden execution permissions.

The Core scheduler already owns scoped agent execution. The ACP session adapter uses the shared exposure policy and unrestricted top-level registry; its nested execution path remains unchanged. Forks inherit tool names and resolve current declarations before running. Their execution allowlists also constrain discovery; disallowing search produces full signatures for their allowed nested tools.

Affected components

  • packages/core/src/tools/code-mode.ts: exposure, signature formatting, and conditional description generation.
  • packages/core/src/tools/tool-registry.ts: scoped discovery availability.
  • packages/core/src/tools/tool-search.ts: Code Mode description and results.
  • packages/core/src/core/coreToolScheduler.ts and packages/core/src/agents/runtime/agent-core.ts: scoped search context.
  • Collocated tests and Code Mode runtime tests; settings documentation and its source schema descriptions.

Constraints and validation

No new sandbox, provider adapter, persistent guest state, or alternate execution path is introduced. Parameter validation, permissions, hooks, cancellation, and output limits continue through the existing scheduler. Generated JavaScript can still contain invalid arguments; this change requires real-model validation for the reported provider combination.

Manual compression probes verified rediscovery and continued cache reuse, but also exposed an existing summary-cleanup defect: a literal <analysis> quoted inside the summary can cause its remaining text to be discarded while compression is reported as successful. This can lose task state and requires a separate compression fix. Stable tool declarations and cache hits alone do not establish that the summary preserved the information needed to continue.

Acceptance criteria:

  • Initial requests omit deferred names, descriptions, and signatures from exec when discovery is available; eager and visible bindings remain documented.
  • Search returns complete tool information and a usable JavaScript name, then nested execution succeeds. Invalid arguments still fail validation.
  • Search and nested calls do not change provider tool declarations.
  • Exact and keyword discovery respect scoped allowlists, exclusions, unavailable tools, and normalized-name collisions. Disabled search falls back to full signatures for allowed tools only.
  • Direct mode retains existing behavior. Build, typecheck, focused unit tests, deterministic CLI checks, and Responses model probes verify the change.
  • Exact-name misses offer scoped-safe recovery guidance. After compression, missing schemas can be rediscovered and tools invoked and reused; actual request history and provider usage establish the cache behavior.

The E2E plan and results are kept in .qwen/e2e-tests/lazy-code-mode.md. There are no open design questions for this first version.