DSH / Atlas
2026-06-15implementedfeature

Code Mode — the model writes TypeScript against the tool registry

Code Mode——模型针对工具注册表编写 TypeScript

In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRuntime` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and at the time of this note the loop dispatched each call through `ctx.tools.execute()` **sequen

English

Problem

In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. ToolRuntime contributes its schemas to the system-prompt assembly, the assembly's tools land on the wire (and in the logged request header), the model invokes one tool-call block per step, and at the time of this note the loop dispatched each call through ctx.tools.execute() sequentially (parallel tool execution was an open TODO then; bounded parallel dispatch has since shipped — the parallel tool-call note, the rolling pool in docs/architecture.md) — with every intermediate tool-result re-entering the model's context on the next request.

For multi-step tool work this is token-heavy and serial. The model cannot compose tools — loop over a result set, branch on an intermediate value, fan out, post-process — without a full model round-trip per call, and each round-trip drags the entire intermediate result back into context whether the model needs it or not.

Cloudflare's Code Mode proposes an alternative grounded in a simple observation: LLMs are better at writing code than at emitting tool calls, because they have seen millions of lines of real code and comparatively few contrived tool-calling traces. Instead of one tool call per step, the model writes a TypeScript program against a generated API over the tools, the program executes in a sandboxed runtime, and the model curates what comes back — only what it prints or returns — instead of every intermediate result.

Tool presentation belongs to the registry that owns tool visibility: implementing a second presentation as an after-the-fact waterfall transform would make correctness depend on listener order and fight reconstructable requests. The execution substrate is also part of the foundation rather than a placeholder: Node worker_threads provides a separate isolate, an empty environment, heap caps, and termination of a hot synchronous loop, while fitting the harness's existing trust model (§Trust posture).

Decision

Three decisions, each elaborated in its own section below:

  1. Code Mode is a first-class presentation mode of ToolRuntime (dsh-tools), selected by a validated mode config: 'native' (the default, contributing the visible capability schemas), 'code' (the registry contributes only its reserved run_code transport plus a generated SDK .d.ts in the system prompt), or 'both' (native schemas and the transport + SDK). The registry constructs its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation.
  2. Code execution is a capability seampackages/code-runtime/ contains the Service Definition package @deepseek-ai/dsh-code-runtime, which owns ctx.codeRuntime (capability seams; Consumer = dsh-tools, with core-consumes-a-seam precedent in agent-loopdsh-llm). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports { value, logs, error? }. Language and substrate are backend properties, so a future Python or container backend is another Service Provider package, not a redesign.
  3. The shipped implementation is @deepseek-ai/dsh-code-runtime-worker-thread: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships dsh-bash-local, which executes arbitrary model-written shell commands with strictly more ambient authority.

This note owns Code Mode's presentation, composition, isolation, and settlement foundation. The later typed tool-return Agent Note owns the generated output map, canonical binding values, ToolCallError, and the lossless outer-output boundary.

The registry owns the mode

ToolRuntime gains a schemastery-validated config (static Config), its first: mode: 'native' | 'code' | 'both', default 'native'. A deployment flips it from cordis.yml (tools: { mode: code }) — no code edit, per the no-hardcoded-tunables convention.

Wire tool list. The registry contributes visible capabilities in 'native', only run_code in 'code', and both in 'both'. The final PromptAssembly.tools list is logged in the request header. run_code is a reserved presentation transport outside registration and restriction layers; direct prompt providers and the assembly waterfall remain responsible for their own contributions.

Interaction with toolOrder: a configured systemPrompt.toolOrder naming native capabilities rejects every assembly under mode: 'code', because those names are outside that mode's wire-validation universe. This is correct behavior, not a bug: a deployment using Code Mode updates its order config or drops it.

SDK prompt section. In 'code' and 'both', the lazy tools:sdk section in the tool-guidance order band renders the loaded runtime's language declarations plus fixed usage instructions for the scope's visible capabilities (TypeScript by default; the language-dispatch note added Python and the ctx.codeRuntime.language renderer table). It shares lookup and execution visibility, excludes run_code, and sorts tools lexicographically for byte-stable output.

Assembly ownership. run_code and tools:sdk enter the trusted system-prompt/assemble waterfall as normal assembly inputs. A scoped tools:sdk section may shadow the global default before dispatch, and a listener may remove or replace either contribution. The waterfall's returned assembly is final, so whoever changes these inputs owns preserving a viable Code Mode protocol when the deployment expects Code Mode to remain usable; no restoration pass overrides deliberate composition.

Codegen. jsonSchemaToTs() maps the defineTool JSON-Schema subset to TypeScript, carries schema descriptions into JSDoc, and degrades unsupported constructs to unknown. The SDK exposes tools as quoted object keys, supporting arbitrary names without aliases or collisions. Typing is advisory because the runtime strips types before execution.

The run_code tool and the dispatch bridge

Under 'code' and 'both' the registry owns run_code as a reserved presentation transport with two required parameters, { code: string; description: string } (the description labels the call in UIs, the bash precedent). It is represented by a normal ToolDefinition for dispatch but stays outside the filterable capability layers, so restrictions cannot accidentally remove Code Mode's only entry point. Calls traverse the complete tool pipeline — tools/pre-execute → monotonic guards → tools/execute around dispatch → tools/post-execute → optional definition-owned finalizeContent → immutable tools/result notification — exactly like native calls; a permission plugin can inspect the program text before it runs, and final-result observers see the normalized outer outcome. Its execute(args, exec):

  1. Build bindings. One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the live-parallel note owns the scheduling design), executes with a deterministic call id and the outer token as parent, defers returned contexts through the outer execution, and logs the tool/code-dispatch-start/tool/code-dispatch pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible ToolCallError. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline.
  2. Runs the program: ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal }). The runtime receives the run-scoped signal, not only the caller's outer signal, so any way the outer run settles also aborts work inside the runtime.
  3. Settle after quiescence. When the runtime settles, the bridge aborts outstanding work and drains the dispatch queue before returning. Success returns captured logs and the completion value as canonical output; the registry renders that value into durable tool/result.content, which the result card reads directly. A runtime failure becomes CodeRunFailedError; backend rejection uses the registry's normal error boundary. Both produce structured error results, and no sub-call can append after run_code settles.

Sub-call contexts are deferred through the parent. Injecting inside run_code would break parent call/result adjacency, so ToolRunContext.deferContext() collects every sub-result additionalContexts entry in dispatch order. The registry carries that array even when the program later throws, and the loop appends each entry only after the outer result and every sibling result in the step. An outer post-execute block discards tool-deferred entries and exposes only contexts explicitly attached by the blocking decision.

Concurrency is bounded, not serialized. Each run owns a dispatch queue that starts calls strictly in submission order and classifies each one through registry.executionMode, the same fail-closed isConcurrencySafe contract the native loop uses. Consecutive parallel-classified calls overlap up to maxParallelSubCalls (default 10; 1 restores serial dispatch); an exclusive call drains the pool and runs alone. Settlement abandons queued calls that have not started. This note shipped the serialized placeholder; the live-parallel Agent Note owns the scheduler that replaced it.

Presentation. run_code's render intent is decided here per the render-intent Agent Note: presentCall creates a generic card with kind: 'execute', the program text as its title, and the same program text as rawInput; run_code intentionally declares no presentResult, so the TUI and host/client runtime (Web) complete that card through their generic raw-content fallback using the final durable tool/result.content, including captured logs plus the returned value, failure, or post-policy spill preview. This is not a terminal card: that card's semantics are "a shell command in a working directory", which a program is not. See the result-card completeness note.

Observability: tool/code-dispatch

Each sub-dispatch appends a log-only tool/code-dispatch-start event at pool entry and a tool/code-dispatch settle event containing parent and child call ids, tool identity, normalized arguments, and the complete rendered content/isError outcome. It remains outside model history but available to persistence and UIs. Appends occur inside the open run_code turn. Direct executions without an agent still run but cannot log the event.

The code-runtime seam

packages/code-runtime/code-runtime/@deepseek-ai/dsh-code-runtime, depending only on cordis. An abstract CodeRuntime extends Service (super(ctx, 'codeRuntime')) plus the vocabulary:

  • CodeRunRequest = { program: string; bindings: CodeBindingNamespace[]; signal?: AbortSignal }
  • CodeBindingNamespace = { global: string; functions: Record<string, (args: unknown) => Promise<CodeJsonValue>>; errorClass?: { name: string; memberNameProperty: string } } — the runtime exposes each namespace as a global object of async functions inside the program; the optional descriptor asks the runtime to inject a real program-visible rejection class without teaching the seam consumer-specific names. CodeJsonValue is this dependency-light seam's structural lossless-JSON type, so binding arguments and resolutions cross the implementation's serialization boundary whole.
  • CodeRunResult = { value?: CodeJsonValue; logs: string[]; error?: CodeRunFailure } — program execution outcomes resolve as the error field. run() may reject only for caller/seam misuse (for example a duplicate binding namespace); consumers still contain a non-conforming backend rejection at their own error boundary.
  • CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'; message: string } — orthogonal outcomes reported independently per defensive patterns; a timed-out run is not an exception, an abort is not a timeout, a lossy completion is not an overflow, and a substrate exit is none of them.
  • Two readonly backend descriptors, informational not gating: language (what the program must be written in — 'typescript' for the first backend; a Python backend says 'python' and pairs with its own SDK generator on the presentation side) and isolation ('worker-thread' for the shipped backend; 'process', 'container', … for future ones). dsh-tools accepts any language with a registered SDK renderer and run_code flavor (TypeScript and Python ship; see the language-dispatch note) and fails the assembly loudly otherwise, the same misconfiguration idiom as toolOrder violations (as when mode is non-native with no ctx.codeRuntime loaded at all).

Requests contain every runtime input; implementations own validated timeout and cap defaults. The registry looks up the optional runtime only when Code Mode is assembled, so native mode does not depend on one. Missing or language-incompatible runtimes fail loudly. Alternate substrates or languages can replace the implementation behind the same seam, paired with the appropriate SDK generator.

The worker-thread runtime

@deepseek-ai/dsh-code-runtime-worker-thread, the second package of the packages/code-runtime/ group. Per run():

  1. Type-strip host-side with Node's built-in stripTypeScriptTypes (node:module; present across the repo's whole engines range, ^22.19.0 || >=24.0.0, and position-preserving, so runtime error line numbers match the model's source). Strip-only mode rejects non-erasable syntax (enum, namespaces) — that rejection returns as error.kind: 'exception' with Node's message, the SDK instructions say "erasable TypeScript only", and the model self-corrects like any other program error. A syntax-level failure never spawns a worker.
  2. Spawn one fresh Worker per run from the package's own bootstrap module: env: {} (truly empty — stronger than the scrubbed-env rule for spawned commands), resourceLimits from config, stdout/stderr captured into logs rather than inherited. No pooling and no cross-run state: a program's world dies with its worker, which keeps runs reconstructable from the log alone and makes state bleed unrepresentable.
  3. Execute in the bootstrap: the stripped program becomes the body of an AsyncFunction whose parameters are the binding globals, any consumer-declared rejection classes, and a capturing console shim, so top-level await and return work. Code Mode declares ToolCallError with member property toolName; the runtime materializes that real constructor without hardcoding tools. A lossless JSON completion crosses exactly; undefined remains absence, a lossy value is invalid-output, and an oversized outer result is output-limit rather than an inspected-string substitute.
  4. Bridge bindings over the message port: each binding function in the worker posts { id, global, name, args } and awaits the reply; the host validates the name against the request's bindings, invokes, and replies { id, ok, value } or { id, ok: false, message } (a host-side binding rejection becomes a program-side rejection). The worker-side namespace objects are built null-prototype via defineProperty, so a binding named __proto__, constructor, or toString is an ordinary own property, not a prototype collision. Unknown names, duplicate ids, and post-settlement messages are rejected or ignored — the port protocol assumes a hostile peer, because the peer runs model code.
  5. Enforce independent budgets. computeMs meters worker busy time, allowing slow awaited tools without excusing a hot loop. maxWallMs bounds total elapsed time, including unresolved waits. maxOutputBytes bounds only the combined serialized outer logs, completion, or diagnostic; intermediate binding values have no byte cap. Expiry, cancellation, and completion terminate the worker, and heap exits or outer overflow are explicit failures.
  6. Dispose to quiescence: the service's own disposal terminates in-flight workers and awaits their exits before resolving, per defensive patterns.

Trust posture

The worker runtime provides containment, not a security boundary: model code can reach Node APIs and has authority comparable to the bash tool. worker.terminate() stops the thread but not OS processes it spawned. Code Mode uses the same tools/pre-execute policy gate as bash and adds an empty environment, heap limits, a separate isolate, and hard termination of the program itself. Deployments that need a hard multi-tenant boundary need a container-class backend for both code and bash; the runtime's isolation descriptor lets them distinguish that backend.

What the model sees

The SDK instructs the model to write an async body in the loaded runtime's language (an erasable-TypeScript body by default; a Python async body under a Python runtime — see the language-dispatch note), call tools through await tools.name(args), catch rejected tool calls when needed, and return or log only the output that should re-enter context. Both flavors state the same contract in their own primitive: independent read-only calls MAY overlap under Promise.all (TypeScript) or asyncio.gather (Python), mutating calls run alone in submission order, and dependent work sequences with await. The declaration prefix can be as large as native schemas, especially in 'both', but remains stable for provider caching.

The transport's own description and both SDK instruction flavors open by naming code and description as the call's two required arguments. Prose that describes the call as passing a program leaves the second argument discoverable only through the parameter schema, and a model that emits {code} alone loses the whole written program to an INVALID_ARGS rejection.

Consequences

Deployments switching to 'code' must update any native-only toolOrder. Assembly listeners own the integrity of any rewritten protocol messages. Sub-dispatch starts in submission order under a bounded overlap pool, while per-call contexts retain their source, envelope, and metadata through the outer result.

Testing

  • Worker runtime: Real-worker tests cover typed binding values and failures, every lossless JSON completion root, invalid and over-limit output, exact combined ledger boundaries, compute and wall budgets, hostile binding traffic, empty environment, and disposal to quiescence. A built-package test runs the worker entry under plain Node.
  • Registry integration: Tests cover code generation, all presentation modes, reserved-name and restriction rules, scoped visibility, authoritative assembly rewrites, toolOrder, runtime compatibility failures, full-pipeline sub-dispatch, parent-token correlation, serialization, cancellation and queue drain, JSON normalization, error propagation, log events, ordered context deferral across successful and failed programs, outer-block suppression, and HMR cleanup.
  • With-key e2e: A real model composes two bash calls in one program; another discovers nested workspace instructions through a Code Mode fs dispatch. The tests verify collapsed request headers, correlated dispatch events, resulting files, deferred context, and model behavior.
  • Snapshot: The code-mode-turn, both-mode-turn, and code-mode-workspace-context fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards.

Alternatives considered

An add-on consumer plugin with zero core changes. Rejected because agent/request is call-config-only under reconstructable requests, while transforming an assembled tool list would have to undo toolOrder canonicalization without owning its config and would depend on listener order. Which tools the model is offered, and in which representation, is the registry's single concern: native schemas and the SDK are two projections of one visible store.

node:vm as the reference runtime, with hardening deferred. Rejected: node:vm is not isolation (prototype-chain escapes reach the host realm) and cannot interrupt a hot loop. A worker thread provides a separate isolate, empty environment, resourceLimits, and reliable terminate() at bash-equivalent trust, so the reference and production implementation are one package without an unsafe-acknowledgement ceremony.

Result elision / summarization over native tool-calling. Addresses only the context-bloat half of the problem: trimming old tool-results is cheap to add as a logged surface replacement under reconstructable requests, but still pays one model round-trip per call and cannot express loops, branches, or joins. Complementary, not competing; it can layer under Code Mode for residual native calls.

Parallel native dispatch in the loop. The other answer to round-trip cost at decision time; it was blocked on concurrency-safety metadata and offers no composition either way — it parallelizes calls the model already decided on in one step. Code Mode's queue decision kept the two compatible, and both later shipped: the metadata as isConcurrencySafe (the parallel tool-call note), and native rolling-pool dispatch plus per-tool binding parallelism on the same classifier.

Always-exclusive (Cloudflare-faithful, no mode). Rejected for this SDK's primary consumer: a coding agent's bread-and-butter single calls (bash, read, edit) are already ideal as native calls, and forcing every edit through a program taxes the common case. The mode config keeps the faithful form ('code') one line away without imposing it.

Per-tool visibility tiers (this tool native, that tool code-only). Deferred: it needs per-tool metadata and a presentation split that 'native' | 'code' | 'both' does not, and its design depends on evidence about how models split usage under 'both'.

Sanitized identifier aliases in the SDK (my-toolmy_tool, Cloudflare's approach). Rejected: quoted keys on a declare const make every name reachable with zero alias-collision logic; models handle tools["my-tool"](…) fine.

A REPL-style persistent kernel (state survives across run_code calls). Rejected for the MVP: cross-call state would be invisible to the session log, breaking the reconstructability guarantee that every request is a pure function of the log; fresh-per-run keeps it. A kernel-style backend remains expressible behind the seam later, with its own logging story.

Risks

The worker is not a hard security boundary. Deliberate and documented (§Trust posture): posture equals the existing bash tool, containment exceeds it, gating uses the same approval and sandbox policies. Deployments needing more need a future isolation: 'container' backend — tracked as the seam's designed extension, not a TODO on this design.

stripTypeScriptTypes is marked experimental. It is the same engine (amaro/swc) behind Node's own native .ts execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite checks position preservation and the required parts of the erasable-only rejection message, the call sits behind one private function, and amaro/sucrase are direct replacements if the API shifts. The erasable-only subset is a model-facing input restriction, and the error tells the model how to correct the program.

Prompt cost of the SDK, especially under 'both'. The .d.ts can rival the native schemas it complements; 'both' carries two representations. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the Agent Note makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning.

Registry scope growth. dsh-tools absorbs codegen, a tool, a bridge, and an event. Package modules separate these responsibilities (ts-types.ts and code-mode.ts beside schema.ts, json-schema.ts, and presentation.ts), while ctx.codeRuntime owns all code-runtime-specific implementation.

Large lossless JSON values can exhaust memory. Tool bindings snapshot lossless JSON before dispatch and return canonical JSON resolutions whole. The runtime validates both sides of the worker port and applies no per-binding byte cap; structured-clone cost and process or worker memory are the practical bounds. The combined outer-output ledger for logs, the completion value, and a failure diagnostic is the only byte-capped boundary.

Sub-dispatch overlap is bounded by tool safety claims, not by the caller. A program's Promise.all or asyncio.gather buys wall-clock parallelism only across calls the tool itself classifies concurrency-safe; a run of exclusive calls still costs its round-trips in sequence, and models may over-expect. Both flavors' SDK instructions state the real contract. This note shipped the serialized placeholder that made the risk absolute; the live-parallel Agent Note owns the scheduler and its overlap cap.

Budget metering reads the event loop, not a flag. Busy-time polling (eventLoopUtilization()) is coarser than an exact CPU meter — a budget expires up to one poll interval late — and its correctness claim ("a pending dispatch cannot pause it") is load-bearing against a hostile program. Both sides are unit-tested (hot loop with a pending decoy dispatch dies at computeMs; idle-on-slow-binding survives to maxWallMs), and the poll interval is an internal constant, not config — nothing a deployment could mis-tune into a bypass. maxWallMs is config, and it reaches setTimeout, which clamps a delay above MAX_TIMER_DELAY_MS (2^31-1 ms) to 1 ms; a positivity check alone therefore accepts a 25-day ceiling that expires on the first tick and times out every run. The worker runtime range-checks the field at load for that reason. computeMs needs no upper bound because it is compared against measured utilization instead of being handed to a timer.

中文

问题

在注册表的原生呈现方式下,agent loop(智能体循环)将每个可见能力以 JSON Schema 函数定义的形式通告给模型。ToolRuntime 将其 schema 贡献给系统提示词组装,组装结果中的 tools 落到协议格式(wire format)上(也记录在请求头日志中),模型每步调用一个 tool-call 块,而在本 note 写作时,循环通过 ctx.tools.execute() 逐个分发每次调用(并行工具执行当时还是 open TODO;此后有界的并行分发已经交付——见并行工具调用 note,以及 docs/architecture.md 中的 rolling pool)——且每一个中间 tool-result 都会在下一次请求时重新进入模型上下文。

对于多步工具操作,这种方式 token 开销大且串行。模型无法组合工具——遍历结果集、根据中间值分支、扇出、后处理——每次调用都需要一次完整的模型往返,而每次往返都会把完整的中间结果拖回上下文,不管模型是否需要。

Cloudflare 的 Code Mode 提出了一种替代方案,基于一个简单的观察:LLM(大语言模型)编写代码的能力优于发出工具调用,因为它们见过数百万行真实代码,而人为构造的工具调用 trace 相对很少。模型不再每步发出一次工具调用,而是针对工具生成的 API 编写一段 TypeScript 程序,程序在沙箱运行时中执行,模型只筛选返回的内容——仅限它 print 或 return 的部分——而非所有中间结果。

工具呈现属于掌管工具可见性的注册表:如果把第二种呈现方式实现为事后的 waterfall(瀑布式事件)变换,正确性将依赖监听器顺序,并与可重建请求冲突。执行基底同样属于基础设施而非占位实现:Node worker_threads 提供独立 isolate、空环境、堆上限以及对热同步循环的终止能力,同时契合 harness 既有的信任模型(§信任姿态)。

决策

三项决策,各自在下方独立小节中展开:

  1. Code Mode 是 ToolRuntimedsh-tools)的一等呈现模式,通过经校验的 mode 配置选择:'native'(默认,贡献可见能力 schema)、'code'(注册表仅贡献其保留的 run_code 传输通道加一份生成的 SDK .d.ts 到系统提示词中)或 'both'(原生 schema 加传输通道 + SDK)。注册表在源头构建其规范贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。
  2. 代码执行是一个能力 seam——packages/code-runtime/ 包含 Service Definition 包 @deepseek-ai/dsh-code-runtime,拥有 ctx.codeRuntime能力 seam;消费方 = dsh-tools,core 消费 seam 的先例见 agent-loopdsh-llm)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 { value, logs, error? }。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service Provider 包,而非重新设计。
  3. 交付的实现是 @deepseek-ai/dsh-code-runtime-worker-thread:每次运行 spawn 一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 dsh-bash-local,后者以严格更高的环境权限执行模型编写的任意 shell 命令。

本说明负责定义 Code Mode 的呈现、组合、隔离与结算基础。后续的类型化工具返回值 Agent Note负责定义生成的输出映射、规范绑定值、ToolCallError 和无损外层输出边界。

注册表拥有模式

ToolRuntime 获得一个经 schemastery 校验的配置(static Config),这是它的第一个配置:mode: 'native' | 'code' | 'both',默认 'native'。部署通过 cordis.yml 翻转模式(tools: { mode: code }),无需改代码,遵循 no-hardcoded-tunables 约定。

协议工具列表。 注册表在 'native' 下贡献可见能力,在 'code' 下仅贡献 run_code,在 'both' 下两者都贡献。最终的 PromptAssembly.tools 列表记录在请求头中。run_code 是一个保留的呈现传输通道,位于注册和限制层之外;直接提示词提供方和组装 waterfall 仍各自负责自己的贡献。

toolOrder 的交互: 如果配置的 systemPrompt.toolOrder 引用了原生能力名称,在 mode: 'code' 下会拒绝所有组装,因为那些名称不在该模式的协议校验范围内。这是正确行为而非 bug:使用 Code Mode 的部署需要更新其 order 配置或移除它。

SDK 提示词段。'code''both' 下,tool-guidance order band 中的惰性 tools:sdk 段为当前 scope 的可见能力渲染所加载运行时语言的声明加固定的使用说明(默认 TypeScript;语言分发 note 加入了 Python 与按 ctx.codeRuntime.language 选择的渲染器表)。它共享查找和执行可见性,排除 run_code,并按字典序排列工具以获得字节稳定的输出。

组装所有权。 run_codetools:sdk 作为正常的组装输入进入受信任的 system-prompt/assemble waterfall。一个 scoped 的 tools:sdk 段可以在分发前遮蔽全局默认值,监听器也可以移除或替换任一贡献。waterfall 返回的组装结果是最终的,因此修改这些输入的人有责任在部署期望 Code Mode 可用时保持协议面的完整性;没有恢复 pass 会覆盖有意的组合。

代码生成。 jsonSchemaToTs()defineTool 的 JSON Schema 子集映射为 TypeScript,将 schema 描述带入 JSDoc,不支持的构造降级为 unknown。SDK 将工具暴露为带引号的对象键,支持任意名称而无需别名或冲突处理。类型是建议性的,因为运行时在执行前会剥离类型。

run_code 工具与分发桥

'code''both' 下,注册表拥有 run_code 作为保留的呈现传输通道,带两个必需参数 { code: string; description: string }(description 为 UI 标注该调用,沿用 bash 的先例)。它由一个正常的 ToolDefinition 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 Code Mode 的唯一入口。调用遍历完整的工具流水线——tools/pre-execute → 单调性守卫 → tools/execute 包裹分发 → tools/post-execute → 由定义拥有的可选 finalizeContent → 不可变的 tools/result 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 execute(args, exec)

  1. 构建绑定。 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由实时并行 Agent Note 负责),以确定性的 call id 和外层 token 作为 parent 执行,通过外层 execution 延后返回的上下文,并记录 tool/code-dispatch-start/tool/code-dispatch 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 ToolCallError。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。
  2. 运行程序ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })。运行时接收的是 run 级别的 signal 而非仅调用方的外层 signal,因此外层 run 以任何方式结算都会同时 abort 运行时内部的工作。
  3. 完全停稳后结算。 运行时结算后,桥 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的日志和完成值,将其作为规范输出;注册表再把该值渲染为持久化的 tool/result.content,供结果卡片直接读取。运行时失败变为 CodeRunFailedError;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 run_code 结算后不允许子调用追加。

子调用上下文通过父调用延后。run_code 内部注入会破坏父调用/结果的相邻性,因此 ToolRunContext.deferContext() 按分发顺序收集每个子结果的 additionalContexts 条目。即使程序后来抛出异常,注册表仍携带该数组;循环只在外层结果与步骤中所有兄弟结果之后追加每个条目。外层 post-execute 阻止会丢弃工具延后的条目,只暴露阻止 decision 显式附加的上下文。

并发是有界的,而非被序列化。 每次 run 拥有一个分发队列,严格按提交顺序启动调用,并通过 registry.executionMode 对每个调用分类——与原生循环所用的 fail-closed isConcurrencySafe 约定相同。连续的 parallel 类调用最多重叠 maxParallelSubCalls 个(默认 10;设为 1 恢复串行分发);exclusive 类调用会排空池并单独运行。结算时放弃尚未开始的排队调用。本 note 交付的是被序列化的占位实现;取代它的调度器由实时并行 Agent Note 负责。

呈现。 run_code 的 render intent 按呈现意图 Agent Note在此决定:presentCall 创建一个 generic 卡片,kind: 'execute',以程序文本作为标题,并将同一程序文本作为 rawInputrun_code 有意不声明 presentResult,因此 TUI 和宿主/客户端运行时(Web)会通过通用原始内容回退机制,使用最终持久化的 tool/result.content 补全该卡片,其中包括捕获的日志,以及返回值、失败信息或 post-policy spill 预览。这不是 terminal 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。参见结果卡片完整性说明

可观测性:tool/code-dispatch

每次子分发在进入分发池时追加一个仅日志的 tool/code-dispatch-start 事件,并以一个 tool/code-dispatch 结算事件收尾,后者包含父子 call id、工具标识、规范化参数以及完整渲染后的 content/isError 结果。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 run_code 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。

code-runtime seam

packages/code-runtime/code-runtime/——@deepseek-ai/dsh-code-runtime,仅依赖 cordis。一个抽象的 CodeRuntime extends Servicesuper(ctx, 'codeRuntime'))加上词汇:

  • CodeRunRequest = { program: string; bindings: CodeBindingNamespace[]; signal?: AbortSignal }
  • CodeBindingNamespace = { global: string; functions: Record<string, (args: unknown) => Promise<CodeJsonValue>>; errorClass?: { name: string; memberNameProperty: string } }——运行时将每个命名空间作为程序内部的全局异步函数对象暴露;可选描述符要求运行时注入真正的、程序可见的 reject 类,而无需让 seam 获知消费方专用名称。CodeJsonValue 是这个低依赖 seam 的结构化无损 JSON 类型,因此绑定参数与返回值可以完整跨越实现的序列化边界。
  • CodeRunResult = { value?: CodeJsonValue; logs: string[]; error?: CodeRunFailure }——程序执行失败时,执行 promise 仍会 fulfill,并通过 error 字段返回失败结果。只有调用方/seam 误用(例如重复的绑定命名空间)时,run() 才会 reject;消费方仍在自己的错误边界处理不合规后端的拒绝。
  • CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'; message: string }——按防御性模式独立报告的正交结果;超时的 run 不是异常,abort 不是超时,有损完成值不是溢出,基底退出也与上述情况相互独立。
  • 两个只读的后端描述符,仅供信息参考而非门禁判定:language(程序必须使用的语言——首个后端为 'typescript';Python 后端声明 'python',并在呈现侧配对自己的 SDK 生成器)和 isolation(交付的后端为 'worker-thread';未来可为 'process''container' 等)。dsh-tools 接受任何注册了 SDK 渲染器与 run_code flavor 的 language(TypeScript 与 Python 已交付;见语言分发 note),否则组装会显式失败,与 toolOrder 违规时的配置错误惯用法相同(如 mode 为非 native 但根本没有加载 ctx.codeRuntime)。

请求包含所有运行时输入;实现方拥有经校验的超时和上限默认值。注册表仅在组装 Code Mode 时查找可选的运行时,因此 native 模式不依赖它。缺失或语言不兼容的运行时会显式失败。替代基底或语言可以在同一 seam 背后替换实现,配对相应的 SDK 生成器。

worker-thread 运行时

@deepseek-ai/dsh-code-runtime-worker-threadpackages/code-runtime/ 组的第二个包。每次 run()

  1. 宿主侧 type-strip,使用 Node 内置的 stripTypeScriptTypesnode:module;在本仓库的整个引擎范围 ^22.19.0 || >=24.0.0 内可用,且会保留源码位置,因此运行时错误行号与模型源码一致)。仅剥离模式拒绝不可擦除的语法(enum、namespaces)——该拒绝以 error.kind: 'exception' 加 Node 的消息返回,SDK 说明写明「仅限可擦除 TypeScript」,模型像处理其他程序错误一样自我修正。语法级失败不会 spawn worker。
  2. 每次 run spawn 一个全新 Worker,来自包自身的 bootstrap 模块:env: {}(真正为空——比 spawn 命令的 scrubbed-env 规则更严格),resourceLimits 来自配置,stdout/stderr 捕获到 logs 而非继承。不做池化,不跨 run 保留状态:程序的世界随 worker 消亡,这使得 run 仅从日志即可重建,状态泄漏不可表达。
  3. 在 bootstrap 中执行:剥离后的程序成为一个 AsyncFunction 的函数体,其参数是绑定全局变量、消费方声明的 reject 类和一个捕获式 console shim,因此顶层 awaitreturn 可用。Code Mode 声明 ToolCallError,成员属性为 toolName;运行时无需硬编码工具即可实体化真正的构造函数。无损 JSON 完成值会精确跨越边界;undefined 仍表示缺席,有损值产生 invalid-output,过大的外层结果产生 output-limit,而不会退化为检查格式化后的字符串替代品。
  4. 通过消息端口桥接绑定:worker 中的每个绑定函数发送 { id, global, name, args } 并等待回复;宿主根据请求的绑定校验名称、调用、并回复 { id, ok, value }{ id, ok: false, message }(宿主侧绑定拒绝变为程序侧 rejection)。worker 侧的命名空间对象通过 defineProperty 构建为 null-prototype,因此名为 __proto__constructortoString 的绑定是普通自有属性,而非原型链碰撞。未知名称、重复 id 和结算后消息被拒绝或忽略——端口协议假设对端是恶意的,因为对端运行的是模型代码。
  5. 强制独立预算。 computeMs 计量 worker 忙碌时间,允许慢速的 awaited 工具而不放过热循环。maxWallMs 约束总经过时间,包括未解析的等待。maxOutputBytes 只约束序列化后的外层日志、完成值或诊断的组合;中间绑定值没有字节数上限。到期、取消和完成都终止 worker,堆退出或外层溢出会作为显式失败报告。
  6. dispose(资源释放)至完全停稳:服务自身的 dispose 终止进行中的 worker 并等待其退出后再 resolve,遵循防御性模式

信任姿态

worker 运行时只能约束程序的运行,而不构成安全边界:模型代码可以访问 Node API,权限与 bash 工具相当。worker.terminate() 停止线程但不停止它 spawn 的 OS 进程。Code Mode 使用与 bash 相同的 tools/pre-execute 策略门禁,并额外提供空环境、堆限制、独立 isolate 和对程序本身的硬终止。需要硬多租户边界的部署需要为代码和 bash 都使用容器级后端;运行时的 isolation 描述符让它们能区分该后端。

模型看到的内容

SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 Python async 函数体——见语言分发 note),通过 await tools.name(args) 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一约定:相互独立的只读调用可以(MAY)在 Promise.all(TypeScript)或 asyncio.gather(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 await 排序。声明前缀可能与原生 schema 一样大,尤其在 'both' 下,但对提供方缓存保持稳定。

传输自身的 description 与两种 flavor 的 SDK 说明都以点名 codedescription 这两个必填参数开头。把该调用描述成「传入一个程序」的散文会让第二个参数只能从参数 schema 中发现,而只发出 {code} 的模型会因 INVALID_ARGS 被拒,连同已写好的整个程序一起丢失。

后果

切换到 'code' 的部署必须更新任何仅限 native 的 toolOrder。组装监听器有责任维护任何被重写的协议消息的完整性。子分发在有界的重叠池下按提交顺序启动,而每次调用的上下文会通过外层结果保留其 source、信封与元数据。

测试

  • Worker 运行时: 真实 worker 测试覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界、compute 和 wall 预算、恶意绑定流量、空环境以及 dispose 至完全停稳。一个构建后包测试在纯 Node 下运行 worker 入口。
  • 注册表集成: 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、toolOrder、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、成功与失败程序中的有序上下文延后、外层阻止抑制以及 HMR(热模块替换)清理。
  • 带密钥 e2e: 真实模型在一个程序中组合两次 bash 调用;另一个模型通过 Code Mode fs 分发发现嵌套的工作区指令。测试验证折叠的请求头、关联的分发事件、结果文件、延后上下文和模型行为。
  • 快照: code-mode-turnboth-mode-turncode-mode-workspace-context fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。

曾考虑的替代方案

一个零核心改动的附加消费方插件。 否决,因为 agent/request可重建请求下仅限 call-config,而变换已组装的工具列表需要在不拥有其配置的情况下撤销 toolOrder 规范化,并依赖监听器顺序。向模型提供哪些工具、以何种表示形式提供,是注册表的单一关注点:原生 schema 和 SDK 是同一个可见存储的两种投影。

node:vm 作为参考运行时,加固推迟。 否决:node:vm 不是隔离(原型链逃逸可达宿主 realm)且无法中断热循环。worker 线程提供独立 isolate、空环境、resourceLimits 和可靠的 terminate(),信任等级等同于 bash,因此参考实现和生产实现是同一个包,无需 unsafe-acknowledgement 仪式。

在原生工具调用上做结果省略/摘要。 仅解决问题中上下文膨胀这一半:裁剪旧 tool-result 作为可重建请求下的日志化表面替换成本低,但仍需每次调用一次模型往返,且无法表达循环、分支或汇合。互补而非竞争;它可以在 Code Mode 下为残余的原生调用分层。

循环中的并行原生分发。 决策当时对往返成本的另一个答案;它被并发安全元数据阻塞,且无论如何都不提供组合能力——它并行化的是模型在一步中已经决定的调用。Code Mode 的队列决策保持了两者兼容,两者后来都已交付:元数据即 isConcurrencySafe(见并行工具调用 note),原生 rolling-pool 分发加每工具绑定并行化则基于同一个分类器。

始终排他(忠于 Cloudflare,无模式)。 否决,因为本 SDK 的主要消费方是编码 agent:其日常的单次调用(bashreadedit)作为原生调用已经是最优的,强制每次编辑都通过程序会给常见场景增加负担。mode 配置让忠实形式('code')只需一行配置即可启用,而不强加于人。

每工具可见性分层(此工具 native,彼工具 code-only)。 推迟:它需要每工具元数据和 'native' | 'code' | 'both' 不提供的呈现拆分,且其设计取决于模型在 'both' 下如何分配使用的证据。

SDK 中的清洁化标识符别名my-toolmy_tool,Cloudflare 的做法)。否决:declare const 上的带引号键使每个名称可达,零别名碰撞逻辑;模型能正常处理 tools["my-tool"](…)

REPL 风格的持久内核(状态跨 run_code 调用存活)。在 MVP 中否决:跨调用状态对会话日志不可见,破坏了「每个请求是日志的纯函数」这一可重建性保证;每次 run 均使用全新实例则维持了这一保证。内核风格的后端在未来仍可通过同一 seam 表达,配合自己的日志方案。

风险

Worker 不是硬安全边界。 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,约束能力强于它,门禁使用相同的审批与沙箱策略。需要更强隔离的部署需要未来的 isolation: 'container' 后端——作为 seam 设计中预留的扩展进行跟踪,而非本设计的 TODO。

stripTypeScriptTypes 标记为 experimental。 它与 Node 自身原生 .ts 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件会检查位置保持和可擦除限制拒绝消息中的必需部分;调用位于一个私有函数之后,且 amaro/sucrase 可在 API 变化时直接替换它。仅可擦除子集是面向模型的输入限制,错误消息会告诉模型如何修正程序。

SDK 的提示词成本,尤其在 'both' 下。 .d.ts 可能与它补充的原生 schema 体量相当;'both' 携带两种表示。前缀稳定性 + 提供方缓存摊销了每会话成本;mode 按部署配置;本 Agent Note 不做无条件节省的声明。何时优先使用哪种模式的量化指导明确属于上线后学习。

注册表 scope 增长。 dsh-tools 吸收了代码生成、一个工具、一个桥和一个事件。包内模块把这些职责分开(ts-types.tscode-mode.tsschema.tsjson-schema.tspresentation.ts 并列),所有 code-runtime 专用实现都由 ctx.codeRuntime 提供。

大型无损 JSON 值可能耗尽内存。 工具绑定会在分发前对无损 JSON 创建快照,并完整返回规范 JSON 返回值。运行时会校验 worker 端口两侧,但不对单次绑定设置字节数上限;结构化克隆成本以及进程或 worker 内存构成实际边界。只有包含日志、完成值和失败诊断的组合外层输出账本受字节数上限约束。

子分发的重叠由工具自身的安全声明限定,而非由调用方决定。 程序里的 Promise.allasyncio.gather 只在工具自己分类为并发安全的调用之间换来挂钟并行性;一串 exclusive 调用仍要按顺序付出各自的往返开销,模型可能过度期望。两种 flavor 的 SDK 说明都陈述了真实约定。本 note 交付的是使该风险绝对化的序列化占位实现;调度器及其重叠上限由实时并行 Agent Note 负责。

预算计量读取事件循环,而非 flag。 忙碌时间轮询(eventLoopUtilization())比精确 CPU 计量更粗糙——预算到期最多延迟一个轮询间隔——且其正确性声明(「pending 的分发不能暂停它」)是抵御恶意程序的关键。两种情况均有单元测试(带 pending 诱饵分发的热循环会在耗尽 computeMs 预算时终止;等待慢速绑定的空闲程序则会持续运行至 maxWallMs),轮询间隔是内部常量而非配置——部署无法将其误调为绕过手段。maxWallMs 是配置项,且会传入 setTimeout,后者会把超过 MAX_TIMER_DELAY_MS(2^31-1 ms)的延迟夹到 1 ms;因此仅有正数校验会放行一个 25 天的上限,它在第一个 tick 就到期,使每次运行都超时。worker 运行时正因如此在加载时对该字段做范围校验。computeMs 不需要上界,因为它对照的是实测占用率,而不是交给定时器。