fix(rebase): migrate the replayed stack onto current master APIs
The linear replay carried each commit's own lineage, so this checkpoint restores the master-owned surfaces the conflicted regions clobbered and migrates branch-owned code to master's post-rebase APIs: - rebuild subprocess-local spawn.ts on master's tree-exit-observer machinery, keeping the branch's win32 childEnv key semantics and the Linux zombie-quiescence probe; the zombie test reaps its survivor directly since a confirmed-absent verdict is a permanent no-more-signals boundary - migrate pty-local test stubs to the Inbox-model Agent interface, Session.create, runnerFailureRules, and the new turn/start payload - implement the seam's resolveExecutable/spawnTerminal abstracts in the new pwsh-local and tool-fs-search test fakes - restore code-runtime, atomic-write, pwsh-local, and app-boot to master's exact content (the net-zero code-runtime churn is pruned from this history) and drop rename-detection graft debris - re-apply the PR's architecture rows and execution-world paragraph, re-record bilingual pairings, regenerate catalogs, and reconcile the lockfile
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md
|
||||
2026-07-20-canonical-tool-output-contract.md: 2429e1c141ad8c8ee932c6d654a6ba74bc4f7618
|
||||
2026-07-20-canonical-tool-output-contract.zh.md: fd1e55eab38e5929a2dc104073b49f9de7a493e2
|
||||
2026-07-20-canonical-tool-output-contract.zh.md: 26653ea769b8a642b70c4d3dd2f5ab907a70bd85
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-15-code-mode.md
|
||||
2026-06-15-code-mode.md: b6a24ecd9700e32912b8112b59cbd8b6ab131eb5
|
||||
2026-06-15-code-mode.zh.md: 4d0a4cf8fa31cf9d9954e5bd95f823dfc0668444
|
||||
2026-06-15-code-mode.md: 99bbed3edab32512f88ece9694d6519a1f89c2dd
|
||||
2026-06-15-code-mode.zh.md: ca1bbe9ed3e412186763d1ed4fca9ed06669d4c3
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-06-15-code-mode.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` 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 the loop dispatches each call through `ctx.tools.execute()` **sequentially** (parallel tool execution is an explicit open TODO in `dsh-tools` and [docs/architecture.md](../../../../docs/architecture.md)), with **every** intermediate `tool-result` re-entering the model's context on the next request.
|
||||
In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` 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](2026-07-10-parallel-tool-call-execution.md), the rolling pool in [docs/architecture.md](../../../../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.
|
||||
|
||||
@@ -32,7 +32,7 @@ This note owns Code Mode's presentation, composition, isolation, and settlement
|
||||
|
||||
**Interaction with `toolOrder`, stated up front:** 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 TypeScript declarations plus fixed usage instructions for the scope's visible capabilities. It shares lookup and execution visibility, excludes `run_code`, and sorts tools lexicographically for byte-stable output.
|
||||
**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](2026-07-31-code-mode-language-dispatch.md) 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.
|
||||
|
||||
@@ -48,7 +48,7 @@ Under `'code'` and `'both'` the registry owns `run_code` as a reserved presentat
|
||||
|
||||
**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 serialized.** Each run owns a dispatch queue, so even `Promise.all` executes tool calls in submission order. Settlement abandons queued calls that have not started. Parallelism requires per-tool concurrency-safety metadata.
|
||||
**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](2026-07-26-code-mode-live-parallel-dispatch.md) owns the scheduler that replaced it.
|
||||
|
||||
**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `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](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md).
|
||||
|
||||
@@ -64,7 +64,7 @@ Each sub-dispatch appends a log-only `tool/code-dispatch-start` event at pool en
|
||||
- `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](../../../../docs/defensive-patterns.md); 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 shipped backend; a Python backend would say so, and pair with its own SDK generator on the presentation side) and `isolation` (`'worker-thread'` for the shipped backend; `'process'`, `'container'`, … for future ones). `dsh-tools` requires `language === 'typescript'` in the MVP — its codegen emits TS — 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).
|
||||
- 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](2026-07-31-code-mode-language-dispatch.md)) 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.
|
||||
|
||||
@@ -85,11 +85,11 @@ The worker runtime provides containment, not a security boundary: model code can
|
||||
|
||||
### What the model sees
|
||||
|
||||
The SDK instructs the model to write an async erasable-TypeScript body, 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. Calls remain sequential even under `Promise.all`. The declaration prefix can be as large as native schemas, especially in `'both'`, but remains stable for provider caching.
|
||||
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](2026-07-31-code-mode-language-dispatch.md)), 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.
|
||||
|
||||
## Consequences
|
||||
|
||||
Deployments switching to `'code'` must update any native-only `toolOrder`. Assembly listeners own the integrity of any rewritten protocol surface. Sub-dispatch remains serialized, while per-call contexts retain their source, envelope, and metadata through the outer result.
|
||||
Deployments switching to `'code'` must update any native-only `toolOrder`. Assembly listeners own the integrity of any rewritten protocol surface. 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
|
||||
|
||||
@@ -106,7 +106,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
|
||||
|
||||
**Result elision / summarization over native tool-calling.** Addresses only the context-bloat half of the problem: trimming old `tool-result`s 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; still valid future work (the open TODO), still blocked on concurrency-safety metadata, and still no composition — it parallelizes calls the model already decided on in one step. Code Mode's serialized-queue decision keeps the two compatible: when the metadata lands, both native parallel dispatch and per-tool binding parallelism unlock together.
|
||||
**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 that is how it played out: the metadata landed as `isConcurrencySafe` (the [parallel tool-call note](2026-07-10-parallel-tool-call-execution.md)), and native rolling-pool dispatch and per-tool binding parallelism unlocked 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.
|
||||
|
||||
@@ -128,6 +128,6 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
|
||||
|
||||
**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.
|
||||
|
||||
**Serialized-only sub-dispatch.** `Promise.all` gains no wall-clock parallelism yet, only fewer round-trips; models may over-expect. The instructions state it; lifting it is tied to the same concurrency-safety metadata the native parallel-dispatch TODO needs.
|
||||
**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](2026-07-26-code-mode-live-parallel-dispatch.md) 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.
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
在注册表的原生呈现方式下,agent loop(智能体循环)将每个可见能力以 JSON Schema 函数定义的形式通告给模型。`ToolRegistry` 将其 schema 贡献给系统提示词组装,组装结果中的 `tools` 落到协议格式(wire format)上(也记录在请求头日志中),模型每步调用一个 `tool-call` 块,循环通过 `ctx.tools.execute()` **逐个**分发每次调用(并行工具执行是 `dsh-tools` 和 [docs/architecture.md](../../../../docs/architecture.md) 中明确标注的 open TODO),且**每一个**中间 `tool-result` 都会在下一次请求时重新进入模型上下文。
|
||||
在注册表的原生呈现方式下,agent loop(智能体循环)将每个可见能力以 JSON Schema 函数定义的形式通告给模型。`ToolRegistry` 将其 schema 贡献给系统提示词组装,组装结果中的 `tools` 落到协议格式(wire format)上(也记录在请求头日志中),模型每步调用一个 `tool-call` 块,而在本 note 写作时,循环通过 `ctx.tools.execute()` **逐个**分发每次调用(并行工具执行当时还是 open TODO;此后有界的并行分发已经交付——见[并行工具调用 note](2026-07-10-parallel-tool-call-execution.md),以及 [docs/architecture.md](../../../../docs/architecture.md) 中的 rolling pool)——且**每一个**中间 `tool-result` 都会在下一次请求时重新进入模型上下文。
|
||||
|
||||
对于多步工具操作,这种方式 token 开销大且串行。模型无法组合工具——遍历结果集、根据中间值分支、扇出、后处理——每次调用都需要一次完整的模型往返,而每次往返都会把完整的中间结果拖回上下文,不管模型是否需要。
|
||||
|
||||
@@ -19,7 +19,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
三项决策,各自在下方独立小节中展开:
|
||||
|
||||
1. **Code Mode 是 `ToolRegistry`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头塑造其权威贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。
|
||||
2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含接口包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.md);消费方 = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个实现包,而非重新设计。
|
||||
2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含接口包`@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.md);消费方 = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个实现包,而非重新设计。
|
||||
3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-worker`**:每次运行 spawn 一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 `dsh-bash-local`,后者以严格*更高*的环境权限执行模型编写的任意 shell 命令。
|
||||
|
||||
本说明负责定义 Code Mode 的呈现、组合、隔离与结算基础。后续的[类型化工具返回值 Agent Note](2026-07-20-code-mode-typed-tool-returns.md)负责定义生成的输出映射、规范绑定值、`ToolCallError` 和无损外层输出边界。
|
||||
@@ -32,7 +32,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
|
||||
**与 `toolOrder` 的交互,预先说明:** 如果配置的 `systemPrompt.toolOrder` 引用了原生能力名称,在 `mode: 'code'` 下会拒绝所有组装,因为那些名称不在该模式的协议校验范围内。这是正确行为而非 bug:使用 Code Mode 的部署需要更新其 order 配置或移除它。
|
||||
|
||||
**SDK 提示词段。** 在 `'code'` 和 `'both'` 下,tool-guidance order band 中的惰性 `tools:sdk` 段为当前 scope 的可见能力渲染 TypeScript 声明加固定的使用说明。它共享查找和执行可见性,排除 `run_code`,并按字典序排列工具以获得字节稳定的输出。
|
||||
**SDK 提示词段。** 在 `'code'` 和 `'both'` 下,tool-guidance order band 中的惰性 `tools:sdk` 段为当前 scope 的可见能力渲染所加载运行时语言的声明加固定的使用说明(默认 TypeScript;[语言分发 note](2026-07-31-code-mode-language-dispatch.md) 加入了 Python 与按 `ctx.codeRuntime.language` 选择的渲染器表)。它共享查找和执行可见性,排除 `run_code`,并按字典序排列工具以获得字节稳定的输出。
|
||||
|
||||
**组装所有权。** `run_code` 和 `tools:sdk` 作为正常的组装输入进入受信任的 `system-prompt/assemble` waterfall。一个 scoped 的 `tools:sdk` 段可以在分发前遮蔽全局默认值,监听器也可以移除或替换任一贡献。waterfall 返回的组装结果是最终的,因此修改这些输入的人有责任在部署期望 Code Mode 可用时保持协议面的完整性;没有恢复 pass 会覆盖有意的组合。
|
||||
|
||||
@@ -40,7 +40,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
|
||||
### 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)`:
|
||||
在 `'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](2026-07-26-code-mode-live-parallel-dispatch.md) 负责),以确定性的 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 运行时内部的工作。
|
||||
@@ -48,7 +48,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
|
||||
**子调用上下文通过父调用延后。** 在 `run_code` 内部注入会破坏父调用/结果的相邻性,因此 `ToolRunContext.deferContext()` 按分发顺序收集每个子结果的 `additionalContexts` 条目。即使程序后来抛出异常,注册表仍携带该数组;循环只在外层结果与步骤中所有兄弟结果之后追加每个条目。外层 post-execute 阻止会丢弃工具延后的条目,只暴露阻止 decision 显式附加的上下文。
|
||||
|
||||
**并发被序列化。** 每次 run 拥有一个分发队列,因此即使 `Promise.all` 也按提交顺序执行工具调用。结算时放弃尚未开始的排队调用。并行化需要每个工具的并发安全元数据。
|
||||
**并发是有界的,而非被序列化。** 每次 run 拥有一个分发队列,严格按提交顺序启动调用,并通过 `registry.executionMode` 对每个调用分类——与原生循环所用的 fail-closed `isConcurrencySafe` 契约相同。连续的 parallel 类调用最多重叠 `maxParallelSubCalls` 个(默认 10;设为 `1` 恢复串行分发);exclusive 类调用会排空池并单独运行。结算时放弃尚未开始的排队调用。本 note 交付的是被序列化的占位实现;取代它的调度器由[实时并行 Agent Note](2026-07-26-code-mode-live-parallel-dispatch.md) 负责。
|
||||
|
||||
**呈现。** `run_code` 的 render intent 按[呈现意图 Agent Note](../architecture/2026-07-02-tool-render-intent-union.md)在此决定:`presentCall` 创建一个 `generic` 卡片,`kind: 'execute'`,以程序文本作为标题,并将同一程序文本作为 `rawInput`;`run_code` 有意不声明 `presentResult`,因此 TUI 和宿主/客户端运行时(Web)会通过通用原始内容回退机制,使用最终持久化的 `tool/result.content` 补全该卡片,其中包括捕获的日志,以及返回值、失败信息或 post-policy 输出落盘预览。这不是 `terminal` 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。参见[结果卡片完整性说明](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md)。
|
||||
|
||||
@@ -61,35 +61,35 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
`packages/code-runtime/code-runtime/`——`@deepseek-ai/dsh-code-runtime`,仅依赖 `cordis`。一个抽象的 `CodeRuntime extends Service`(`super(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 类型,因此绑定参数与解析值可以完整跨越实现的序列化边界。
|
||||
- `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 }`——按[防御性模式](../../../../docs/defensive-patterns.md)独立报告的正交结果;超时的 run 不是异常,abort 不是超时,有损完成值不是溢出,基底退出也与上述情况相互独立。
|
||||
- 两个只读的后端描述符,仅供信息参考而非门禁判定:`language`(程序必须使用的语言——交付的后端为 `'typescript'`;Python 后端会声明自己,并在呈现侧配对自己的 SDK 生成器)和 `isolation`(交付的后端为 `'worker-thread'`;未来可为 `'process'`、`'container'` 等)。`dsh-tools` 在 MVP 中要求 `language === 'typescript'`——其代码生成输出 TS——否则组装会大声失败,与 `toolOrder` 违规时的配置错误惯用法相同(如 `mode` 为非 native 但根本没有加载 `ctx.codeRuntime`)。
|
||||
- 两个只读的后端描述符,仅供信息参考而非门禁判定:`language`(程序必须使用的语言——首个后端为 `'typescript'`;Python 后端声明 `'python'`,并在呈现侧配对自己的 SDK 生成器)和 `isolation`(交付的后端为 `'worker-thread'`;未来可为 `'process'`、`'container'` 等)。`dsh-tools` 接受任何注册了 SDK 渲染器与 `run_code` flavor 的 `language`(TypeScript 与 Python 已交付;见[语言分发 note](2026-07-31-code-mode-language-dispatch.md)),否则组装会大声失败,与 `toolOrder` 违规时的配置错误惯用法相同(如 `mode` 为非 native 但根本没有加载 `ctx.codeRuntime`)。
|
||||
|
||||
请求包含所有运行时输入;实现方拥有经校验的超时和上限默认值。注册表仅在组装 Code Mode 时查找可选的运行时,因此 native 模式不依赖它。缺失或语言不兼容的运行时会大声失败。替代基底或语言可以在同一 seam 背后替换实现,配对相应的 SDK 生成器。
|
||||
|
||||
### worker-thread 运行时
|
||||
|
||||
`@deepseek-ai/dsh-code-runtime-worker`,`packages/code-runtime/` 组的第二个包(package)。每次 `run()`:
|
||||
`@deepseek-ai/dsh-code-runtime-worker`,`packages/code-runtime/` 组的第二个包。每次 `run()`:
|
||||
|
||||
1. **宿主侧 type-strip**,使用 Node 内置的 `stripTypeScriptTypes`(`node:module`;在本仓库的整个引擎范围 `^22.19.0 || >=24.0.0` 内可用,且保持位置不变,因此运行时错误行号与模型源码一致)。仅剥离模式拒绝不可擦除的语法(`enum`、namespaces)——该拒绝以 `error.kind: 'exception'` 加 Node 的消息返回,SDK 说明写明「仅限可擦除 TypeScript」,模型像处理其他程序错误一样自我修正。语法级失败不会 spawn worker。
|
||||
1. **宿主侧 type-strip**,使用 Node 内置的 `stripTypeScriptTypes`(`node: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,因此顶层 `await` 和 `return` 可用。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__`、`constructor` 或 `toString` 的绑定是普通自有属性,而非原型链碰撞。未知名称、重复 id 和结算后消息被拒绝或忽略——端口协议假设对端是恶意的,因为对端运行的是模型代码。
|
||||
5. **强制独立预算。** `computeMs` 计量 worker 忙碌时间,允许慢速的 awaited 工具而不放过热循环。`maxWallMs` 约束总经过时间,包括未解析的等待。`maxOutputBytes` 只约束序列化后的外层日志、完成值或诊断的组合;中间绑定值没有字节数上限。到期、取消和完成都终止 worker,堆退出或外层溢出会作为显式失败报告。
|
||||
6. **dispose 至完全停稳**:服务自身的 dispose(资源释放)终止进行中的 worker 并*等待*其退出后再 resolve,遵循[防御性模式](../../../../docs/defensive-patterns.md)。
|
||||
6. **dispose(资源释放)至完全停稳**:服务自身的 dispose 终止进行中的 worker 并*等待*其退出后再 resolve,遵循[防御性模式](../../../../docs/defensive-patterns.md)。
|
||||
|
||||
### 信任姿态
|
||||
|
||||
worker 运行时提供的是隔离,而非安全边界:模型代码可以访问 Node API,权限与 bash 工具相当。`worker.terminate()` 停止线程但不停止它 spawn 的 OS 进程。Code Mode 使用与 bash 相同的 `tools/pre-execute` 策略门禁,并额外提供空环境、堆限制、独立 isolate 和对程序本身的硬终止。需要硬多租户边界的部署需要为代码和 bash 都使用容器级后端;运行时的 isolation 描述符让它们能区分该后端。
|
||||
worker 运行时只能约束程序的运行,而不构成安全边界:模型代码可以访问 Node API,权限与 bash 工具相当。`worker.terminate()` 停止线程但不停止它 spawn 的 OS 进程。Code Mode 使用与 bash 相同的 `tools/pre-execute` 策略门禁,并额外提供空环境、堆限制、独立 isolate 和对程序本身的硬终止。需要硬多租户边界的部署需要为代码和 bash 都使用容器级后端;运行时的 isolation 描述符让它们能区分该后端。
|
||||
|
||||
### 模型看到的内容
|
||||
|
||||
SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。即使在 `Promise.all` 下调用仍保持顺序。声明前缀可能与原生 schema 一样大,尤其在 `'both'` 下,但对提供方缓存保持稳定。
|
||||
SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 Python `async` 函数体——见[语言分发 note](2026-07-31-code-mode-language-dispatch.md)),通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一契约:相互独立的只读调用可以在 `Promise.all`(TypeScript)或 `asyncio.gather`(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 `await` 排序。声明前缀可能与原生 schema 一样大,尤其在 `'both'` 下,但对提供方缓存保持稳定。
|
||||
|
||||
## 后果
|
||||
|
||||
切换到 `'code'` 的部署必须更新任何仅限 native 的 `toolOrder`。组装监听器有责任维护任何被重写的协议面的完整性。子分发保持序列化,而每次调用的上下文会通过外层结果保留其 source、信封与元数据。
|
||||
切换到 `'code'` 的部署必须更新任何仅限 native 的 `toolOrder`。组装监听器有责任维护任何被重写的协议面的完整性。子分发在有界的重叠池下按提交顺序启动,而每次调用的上下文会通过外层结果保留其 source、信封与元数据。
|
||||
|
||||
## 测试
|
||||
|
||||
@@ -104,9 +104,9 @@ SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `aw
|
||||
|
||||
**`node:vm` 作为参考运行时,加固推迟。** 否决:`node:vm` 不是隔离(原型链逃逸可达宿主 realm)且无法中断热循环。worker 线程提供独立 isolate、空环境、`resourceLimits` 和可靠的 `terminate()`,信任等级等同于 bash,因此参考实现和生产实现是同一个包,无需 unsafe-acknowledgement 仪式。
|
||||
|
||||
**在原生工具调用上做结果省略/摘要。** 仅解决问题的上下文膨胀一半:裁剪旧 `tool-result` 作为可重建请求下的日志化表面替换成本低,但仍需每次调用一次模型往返,且无法表达循环、分支或汇合。互补而非竞争;它可以在 Code Mode 下为残余的原生调用分层。
|
||||
**在原生工具调用上做结果省略/摘要。** 仅解决问题中上下文膨胀这一半:裁剪旧 `tool-result` 作为可重建请求下的日志化表面替换成本低,但仍需每次调用一次模型往返,且无法表达循环、分支或汇合。互补而非竞争;它可以在 Code Mode 下为残余的原生调用分层。
|
||||
|
||||
**循环中的并行原生分发。** 往返成本的另一个答案;仍是有效的未来工作(open TODO),仍被并发安全元数据阻塞,且仍无组合能力——它并行化的是模型在一步中已经决定的调用。Code Mode 的序列化队列决策保持两者兼容:当元数据就绪时,原生并行分发和每工具绑定并行化一起解锁。
|
||||
**循环中的并行原生分发。** 决策当时对往返成本的另一个答案;它被并发安全元数据阻塞,且无论如何都不提供组合能力——它并行化的是模型在一步中已经决定的调用。Code Mode 的队列决策保持了两者兼容,后续也正是这样落地的:元数据以 `isConcurrencySafe` 的形式就绪(见[并行工具调用 note](2026-07-10-parallel-tool-call-execution.md)),原生 rolling-pool 分发与每工具绑定并行化基于同一个分类器一起解锁。
|
||||
|
||||
**始终排他(忠于 Cloudflare,无模式)。** 否决,因为本 SDK 的主要消费方是编码 agent:其日常的单次调用(`bash`、`read`、`edit`)作为原生调用已经是最优的,强制每次编辑都通过程序会给常见场景增加负担。mode 配置让忠实形式(`'code'`)只需一行配置即可启用,而不强加于人。
|
||||
|
||||
@@ -114,20 +114,20 @@ SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `aw
|
||||
|
||||
**SDK 中的清洁化标识符别名**(`my-tool` → `my_tool`,Cloudflare 的做法)。否决:`declare const` 上的带引号键使每个名称可达,零别名碰撞逻辑;模型能正常处理 `tools["my-tool"](…)`。
|
||||
|
||||
**REPL 风格的持久内核**(状态跨 `run_code` 调用存活)。在 MVP 中否决:跨调用状态对会话日志不可见,破坏了「每个请求是日志的纯函数」这一可重建性保证;每次 run 全新保持了这一点。内核风格的后端在未来仍可通过同一 seam 表达,配合自己的日志方案。
|
||||
**REPL 风格的持久内核**(状态跨 `run_code` 调用存活)。在 MVP 中否决:跨调用状态对会话日志不可见,破坏了「每个请求是日志的纯函数」这一可重建性保证;每次 run 均使用全新实例则维持了这一保证。内核风格的后端在未来仍可通过同一 seam 表达,配合自己的日志方案。
|
||||
|
||||
## 风险
|
||||
|
||||
**Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,隔离程度超过它,门禁使用相同的 seam。需要更强隔离的部署需要未来的 `isolation: 'container'` 后端——作为 seam 设计的扩展点跟踪,而非本设计的 TODO。
|
||||
**Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,约束能力强于它,门禁使用相同的 seam。需要更强隔离的部署需要未来的 `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.ts`、`code-mode.ts` 与 `schema.ts`/`json-schema.ts`/`presentation.ts` 并列)和 seam 约束:所有基底相关的内容都在 `ctx.codeRuntime` 之后。
|
||||
**注册表 scope 增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥和一个事件。包内的模块边界(`ts-types.ts`、`code-mode.ts` 与 `schema.ts`/`json-schema.ts`/`presentation.ts` 并列)和 seam 共同约束了这一增长:所有基底相关的内容都在 `ctx.codeRuntime` 之后。
|
||||
|
||||
**大型无损 JSON 值可能耗尽内存。** 工具绑定会在分发前对无损 JSON 创建快照,并完整返回规范 JSON 解析值。运行时会校验 worker 端口两侧,但不对单次绑定设置字节数上限;结构化克隆成本以及进程或 worker 内存构成实际边界。只有包含日志、完成值和失败诊断的组合外层输出账本受字节数上限约束。
|
||||
**大型无损 JSON 值可能耗尽内存。** 工具绑定会在分发前对无损 JSON 创建快照,并完整返回规范 JSON 返回值。运行时会校验 worker 端口两侧,但不对单次绑定设置字节数上限;结构化克隆成本以及进程或 worker 内存构成实际边界。只有包含日志、完成值和失败诊断的组合外层输出账本受字节数上限约束。
|
||||
|
||||
**仅序列化的子分发。** `Promise.all` 尚未获得挂钟并行性,仅减少往返次数;模型可能过度期望。说明中已声明;解除此限制与原生并行分发 TODO 所需的并发安全元数据绑定。
|
||||
**子分发的重叠由工具自身的安全声明限定,而非由调用方决定。** 程序里的 `Promise.all` 或 `asyncio.gather` 只在工具自己分类为并发安全的调用之间换来挂钟并行性;一串 exclusive 调用仍要按顺序付出各自的往返开销,模型可能过度期望。两种 flavor 的 SDK 说明都陈述了真实契约。本 note 交付的是使该风险绝对化的序列化占位实现;调度器及其重叠上限由[实时并行 Agent Note](2026-07-26-code-mode-live-parallel-dispatch.md) 负责。
|
||||
|
||||
**预算计量读取事件循环,而非 flag。** 忙碌时间轮询(`eventLoopUtilization()`)比精确 CPU 计量更粗糙——预算到期最多延迟一个轮询间隔——且其正确性声明(「pending 的分发不能暂停它」)对恶意程序是承重的。两侧都有单元测试(带 pending 诱饵分发的热循环在 `computeMs` 处死亡;在慢绑定上空闲的程序存活到 `maxWallMs`),轮询间隔是内部常量而非配置——部署无法将其误调为绕过手段。`maxWallMs` 是配置项,且会传入 `setTimeout`,后者会把超过 `MAX_TIMER_DELAY_MS`(2^31-1 ms)的延迟夹到 1 ms;因此仅有正数校验会放行一个 25 天的上限,它在第一个 tick 就到期,使每次运行都超时。worker 运行时正因如此在加载时对该字段做范围校验。`computeMs` 不需要上界,因为它对照的是实测占用率,而不是交给定时器。
|
||||
**预算计量读取事件循环,而非 flag。** 忙碌时间轮询(`eventLoopUtilization()`)比精确 CPU 计量更粗糙——预算到期最多延迟一个轮询间隔——且其正确性声明(「pending 的分发不能暂停它」)是抵御恶意程序的关键。两种情况均有单元测试(带 pending 诱饵分发的热循环会在耗尽 `computeMs` 预算时终止;等待慢速绑定的空闲程序则会持续运行至 `maxWallMs`),轮询间隔是内部常量而非配置——部署无法将其误调为绕过手段。`maxWallMs` 是配置项,且会传入 `setTimeout`,后者会把超过 `MAX_TIMER_DELAY_MS`(2^31-1 ms)的延迟夹到 1 ms;因此仅有正数校验会放行一个 25 天的上限,它在第一个 tick 就到期,使每次运行都超时。worker 运行时正因如此在加载时对该字段做范围校验。`computeMs` 不需要上界,因为它对照的是实测占用率,而不是交给定时器。
|
||||
|
||||
Reference in New Issue
Block a user