Merge remote-tracking branch 'origin/master' into feat/todo-multi-in-progress

# Conflicts:
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl
This commit is contained in:
Chinesezjc
2026-07-26 21:45:47 +08:00
105 changed files with 3665 additions and 1736 deletions

View File

@@ -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
README.md: 14b7896e413a56fcee5a7db4cd92813f3e91c286
README.zh.md: c89b03ff12bd346a2c0a8848a0e61b8fd738c318
README.md: 893ba3afef71ea0fb6b4bca267d929b220e3d506
README.zh.md: cefb35bbc3556a1aca97d9e2fc5f8e6e06391e2e

View File

@@ -114,16 +114,16 @@ Returning `undefined` selects generic fallback. Presenters depend only on their
### Code Mode
Under `code` or `both`, the registry exposes the reserved `run_code` transport and a deterministic TypeScript SDK for the current scope; only the program's outer logs and return value re-enter model context. The SDK declares exact `ToolArgsMap` and `ToolOutputMap` entries for every visible tool, and each binding resolves to the tool's canonical JSON value. Each lossless-JSON binding call re-enters the complete tool pipeline sequentially with logged correlation to the outer call. Denials and other failed results reject with the real program-visible `ToolCallError` carrying only `toolName` and `message`; Native content and internal error codes stay outside the Code contract. Ordinary side effects are not rolled back, and sub-call `additionalContexts` are deferred through the parent result to preserve call/result adjacency. Run settlement aborts and drains outstanding bindings; runtime failures surface as `CodeRunFailedError`. See the [Code Mode foundation](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md), [typed-return contract](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md), and [code-runtime seam](../../code-runtime/README.md). Try `pnpm run demo:code-mode`.
Under `code` or `both`, the registry exposes the reserved `run_code` transport and a deterministic TypeScript SDK for the current scope; only the program's outer logs and return value re-enter model context. The SDK declares exact `ToolArgsMap` and `ToolOutputMap` entries for every visible tool, and each binding resolves to the tool's canonical JSON value. Each lossless-JSON binding call re-enters the complete tool pipeline under the native scheduling contract (concurrency-safe calls may overlap up to `maxParallelSubCalls`; exclusive calls run alone as ordering barriers) with logged correlation to the outer call. Denials and other failed results reject with the real program-visible `ToolCallError` carrying only `toolName` and `message`; Native content and internal error codes stay outside the Code contract. Ordinary side effects are not rolled back, and sub-call `additionalContexts` are deferred through the parent result to preserve call/result adjacency. Run settlement aborts and drains outstanding bindings; runtime failures surface as `CodeRunFailedError`. See the [Code Mode foundation](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md), [typed-return contract](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md), and [code-runtime seam](../../code-runtime/README.md). Try `pnpm run demo:code-mode`.
- **The SDK section** (`tools:sdk`, order 150): a lazy prompt section regenerating, at each assembly, `JsonValue`, exact `ToolArgsMap` / `ToolOutputMap`, `ToolName`, the `ToolCallError` declaration, and a mapped `tools` namespace for the calling scope's visible end capabilities (exotic names via quoted keys), plus fixed usage instructions. Deterministic — lexicographic tool order, byte-identical text for an unchanged tool set (prefix-cache-friendly). The codegen (`jsonSchemaToTs`, exported) handles every unified schema construct and degrades unsupported raw constructs to `unknown`, never throwing during prompt assembly.
- **The dispatch bridge** (`run_code`'s execute): every binding call is snapshotted as lossless JSON before dispatch (`undefined`, `BigInt`, cycles, sparse arrays, `-0`, and exotic objects reject that one call), serialized through a per-run queue (even `Promise.all` executes underlying calls one at a time in submission order), given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A success returns the final canonical value after policy; a failure reaches the worker as one message and becomes `ToolCallError(toolName, message)`. Each sub-call is logged as a `tool/code-dispatch` session event with deterministic id `<parent>:code:<n>` and a bounded Native-content summary; `deriveMessages()` does not surface that event or persist the value. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and retaining each source/meta even when the program later fails.
- **The dispatch bridge** (`run_code`'s execute): every binding call is snapshotted as lossless JSON before dispatch (`undefined`, `BigInt`, cycles, sparse arrays, `-0`, and exotic objects reject that one call), scheduled through a per-run pool that reuses the native concurrency contract — calls start strictly in submission order, consecutive `isConcurrencySafe` calls overlap up to the validated `maxParallelSubCalls` config (default 10; `1` restores serial dispatch), and an exclusive-classified call drains the pool, runs alone, and bars later calls — given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A success returns the final canonical value after policy; a failure reaches the worker as one message and becomes `ToolCallError(toolName, message)`. Each started sub-call logs a `tool/code-dispatch-start` event (deterministic id `<parent>:code:<n>`, numbered by submission) at pipeline entry and settles with one `tool/code-dispatch` event carrying the complete model-facing `content`/`isError` outcome (the `tool/result` vocabulary, so UIs render sub-calls through the native path — the pair's `time` fields carry per-sub-call timing); a queued call abandoned by run settlement logs neither. `deriveMessages()` surfaces neither event nor persists the canonical value. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and retaining each source/meta even when the program later fails.
- **Settlement discipline**: the bridge owns a run-scoped abort that follows the outer signal in and fires when the run settles for any reason, so a budget expiry aborts an in-flight sub-tool instead of orphaning it; the bridge then drains its queue BEFORE returning, so every `tool/code-dispatch` lands inside the open turn. A failed run throws `CodeRunFailedError` (`code: 'CODE_RUN_FAILED'`, message = the failure kind + captured logs), which the pipeline converts to a structured `isError` the model self-corrects from.
- **Result boundary**: intermediate binding values cross the worker boundary whole and have no per-binding byte cap. `run_code` returns canonical `{ logs: string[], result?: JsonValue }`; strings render raw, every other present JSON root renders through a stack-safe pretty JSON traversal whose total indentation is capped at ten characters (deeper subtrees stay compact), `null` remains explicit, and absent `result` means the program returned `undefined`. The worker's configurable `maxOutputBytes` (default 64 MiB) applies only to the combined serialized outer log-array, completion-value, or failure-message payloads; fixed result-envelope syntax and presentation whitespace are outside that ledger. Invalid and over-limit completions fail explicitly, and only this outer result is eligible for ordinary spill.
### Parallel execution
The agent loop groups consecutive `parallel` calls into a bounded rolling pool and treats each `exclusive` call as an ordering barrier. Only dispatch/body overlaps; policy, durable results, and context retain model order. Code Mode bindings remain serial. The [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) owns the shipped declarations and rationale.
The agent loop groups consecutive `parallel` calls into a bounded rolling pool and treats each `exclusive` call as an ordering barrier. Only dispatch/body overlaps; policy, durable results, and context retain model order. Code Mode bindings reuse the same classification through the bridge's own pool. The [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) owns the shipped declarations and rationale.
## Model Experience
@@ -156,7 +156,7 @@ Pass `run_code` the body of an async TypeScript function (erasable syntax only
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
- Calls execute sequentially, even under `Promise.all`.
- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.
- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.
The available tools:
@@ -191,5 +191,5 @@ Append-only; newly visible content follows the reusable request prefix and does
- **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper.
- **Code Mode is TypeScript-only and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`; scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only.
- **Code Mode intermediate values are execution-local and unbounded by bytes** — they cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap.
- **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The rendered `content` of every sub-call IS logged verbatim on `tool/code-dispatch`, uncapped and outside spill policy, so programs that read huge files grow the session log by the same bytes (spill integration for the logged copy is deferred work).
- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md).

View File

@@ -114,16 +114,16 @@ ctx.tools.register(defineTool({
### Code Mode
`code``both` 模式下,注册表为当前作用域公开保留的 `run_code` 传输和确定性的 TypeScript SDK只有程序的外层日志与返回值会重新进入模型上下文。SDK 为每个可见工具声明精确的 `ToolArgsMap``ToolOutputMap` 条目,每个绑定都会解析为该工具的规范 JSON 值。每个无损 JSON 绑定调用都会按顺序重新进入完整工具流水线,并在日志中与外层调用建立关联。拒绝及其他失败结果会以程序实际可见的 `ToolCallError` 进行 reject且只携带 `toolName``message`Native 内容和内部错误码留在 Code 契约之外。普通副作用不会回滚,子调用的 `additionalContexts` 会通过父结果延迟,以保持调用/结果相邻。运行结算会中止并 drain 尚未完成的绑定;运行时失败以 `CodeRunFailedError` 形式出现。参见 [Code Mode 基础](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)、[类型化返回契约](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)和[代码运行时 seam](../../code-runtime/README.md)。可以运行 `pnpm run demo:code-mode` 试用。
`code``both` 模式下,注册表为当前作用域公开保留的 `run_code` 传输和确定性的 TypeScript SDK只有程序的外层日志与返回值会重新进入模型上下文。SDK 为每个可见工具声明精确的 `ToolArgsMap``ToolOutputMap` 条目,每个绑定都会解析为该工具的规范 JSON 值。每个无损 JSON 绑定调用都会在原生调度契约下重新进入完整工具流水线(并发安全的调用最多可重叠 `maxParallelSubCalls` 个;独占调用单独运行并构成排序屏障),并在日志中与外层调用建立关联。拒绝及其他失败结果会以程序实际可见的 `ToolCallError` 进行 reject且只携带 `toolName``message`Native 内容和内部错误码留在 Code 契约之外。普通副作用不会回滚,子调用的 `additionalContexts` 会通过父结果延迟,以保持调用/结果相邻。运行结算会中止并 drain 尚未完成的绑定;运行时失败以 `CodeRunFailedError` 形式出现。参见 [Code Mode 基础](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)、[类型化返回契约](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)和[代码运行时 seam](../../code-runtime/README.md)。可以运行 `pnpm run demo:code-mode` 试用。
- **SDK 段**`tools:sdk`,顺序 150一个惰性提示词段每次组装时都会重新生成 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap``ToolName``ToolCallError` 声明、面向调用作用域可见最终能力的映射 `tools` 命名空间(特殊名称使用带引号的键),以及固定用法说明。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache。导出的代码生成器 `jsonSchemaToTs` 会处理统一 schema 的每种构造,并将不受支持的原始构造降级为 `unknown`,绝不会在提示词组装期间抛出。
- **分发桥接层**`run_code` 的 execute每个绑定调用都会在分发前快照为无损 JSON`undefined``BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),通过每次运行独有的队列串行化(即使使用 `Promise.all`,底层调用也会按提交顺序逐个执行),以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker并成为 `ToolCallError(toolName, message)`。每个子调用都会记录为 `tool/code-dispatch` 会话事件,其确定性 id `<parent>:code:<n>`并附带有界的 Native 内容摘要;`deriveMessages()` 不会公开该事件或持久化值。token 关联让以提交为语义的观察器能够把内部成功延迟到最终 `run_code` 结果,而无需公开实时外层执行;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系,并且即使程序后来失败,也会保留各自的来源/元数据。
- **分发桥接层**`run_code` 的 execute每个绑定调用都会在分发前快照为无损 JSON`undefined``BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发契约的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `<parent>:code:<n>`按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件,也不持久化规范值。token 关联让以提交为语义的观察器能够把内部成功延迟到最终 `run_code` 结果,而无需公开实时外层执行;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系,并且即使程序后来失败,也会保留各自的来源/元数据。
- **结算纪律**:桥接层拥有一次运行作用域的中止;该中止会跟随传入的外层信号,并在运行因任何原因结算时触发,因此预算耗尽会中止正在运行的子工具,而不会将其遗留。桥接层随后会在返回之前 drain 队列,使每个 `tool/code-dispatch` 都落在仍打开的轮次内。失败的运行会抛出 `CodeRunFailedError``code: 'CODE_RUN_FAILED'`message = 失败类型 + 已捕获日志),流水线会将其转换为模型可据以自我修正的结构化 `isError`
- **结果边界**:中间绑定值会完整跨越 worker 边界,且没有逐绑定字节上限。`run_code` 返回规范的 `{ logs: string[], result?: JsonValue }`;字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),`null` 保持显式,而缺少 `result` 表示程序返回 `undefined`。worker 可配置的 `maxOutputBytes`(默认 64 MiB只应用于组合序列化后的外层日志数组、完成值或失败消息载荷固定的结果 envelope 语法和呈现空白不计入该账本。无效和超限的完成会明确失败,只有此外层结果可以使用普通 spill。
### 并行执行
agent loop 将连续的 `parallel` 调用归入有界滚动池,并把每个 `exclusive` 调用视为顺序屏障。只有分发主体会重叠策略、持久结果和上下文仍保持模型顺序。Code Mode 绑定仍按串行执行。[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) 规定已交付声明及其原理。
agent loop 将连续的 `parallel` 调用归入有界滚动池,并把每个 `exclusive` 调用视为顺序屏障。只有分发主体会重叠策略、持久结果和上下文仍保持模型顺序。Code Mode 绑定通过桥接层自己的池复用同一套分类。[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) 规定已交付声明及其原理。
## 模型体验
@@ -156,7 +156,7 @@ Pass `run_code` the body of an async TypeScript function (erasable syntax only
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
- Calls execute sequentially, even under `Promise.all`.
- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.
- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.
The available tools:
@@ -191,5 +191,5 @@ The available tools:
- **调用方定义的 subagent 与工作流结构化输出仍要求对象根**:这是消费方层面的守卫;共享 schema 词汇和工具输出支持任意 JSON 根。
- **定义上的 `timeoutMs` 仅为声明**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-timeout-policy` 包装层。
- **Code Mode 只支持 TypeScript且呈现模式在服务内统一**`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language === 'typescript'`;作用域限制/遮蔽仍会选择每个 agent 的可见绑定,但不能让一个工具仅使用 Native而另一个仅使用 Code。
- **Code Mode 中间值只存在于执行局部,且没有字节上限**:无法从会话回放重建这些值,它们可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。
- **Code Mode 中间值只存在于执行局部,且没有字节上限**这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。每个子调用渲染后的 `content` 确实会原样记录在 `tool/code-dispatch` 中,不受字节上限约束,也不在 spill 策略范围内。因此,读取超大文件的程序会使会话日志增加等量字节(日志中的副本尚未接入 spill相关工作留待后续完成
- **每次运行都会获得全新的 `run_code` 状态**MVP 不采用持久 REPL 风格内核(跨调用状态不会出现在日志中);参见 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。

View File

@@ -1,37 +1,52 @@
/**
* Code Mode `run_code` transport. Programs call the registry's agent-visible
* tools through nested, sequential executions; each sub-dispatch is logged for
* reconstruction, while only the outer curated result enters model history.
* tools through nested executions scheduled under the native concurrency
* contract; each sub-dispatch is logged for reconstruction, while only the
* outer curated result enters model history.
* @module @deepseek-ai/dsh-tools/src/code-mode
*/
import { parse } from 'node:path'
import { CallId, HarnessError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import { defineTool } from './schema.ts'
import type { ToolDefinition, ToolRegistry } from './index.ts'
import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
import type { ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
declare module '@deepseek-ai/dsh-session' {
interface SessionEventMap {
/**
* One bridged sub-dispatch from a `run_code` program: the parent
* `run_code` call id, the deterministic sub-call id
* (`<parent>:code:<n>`), the tool `name` with its JSON-normalized
* `arguments` — the exact value dispatched, normalized BEFORE dispatch,
* so this append can never fail on payload shape — whether the sub-call
* errored, and a bounded `resultSummary` of its model-facing text. Before
* bounding, occurrences of a non-root session workspace path are
* normalized to `.` so host-specific absolute path lengths cannot change
* the summary.
* One sub-dispatch STARTING inside a `run_code` program: the parent
* `run_code` call id, the deterministic sub-call id (`<parent>:code:<n>`,
* numbered in submission order), and the tool `name` with its
* JSON-normalized `arguments` — the exact value dispatched, normalized
* BEFORE dispatch, so this append can never fail on payload shape.
* Appended when the scheduler actually starts the call (not at
* submission), so a start means the tool body pipeline was entered; a
* call abandoned in the queue logs nothing. Log-only: `deriveMessages()`
* ignores it; UIs use it for live per-sub-call running state and pair it
* with `tool/code-dispatch` by `subCallId` (timing = the two events'
* `time` fields).
*/
'tool/code-dispatch-start': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown }
/**
* One bridged sub-dispatch SETTLING: the pairing ids (matching the
* `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
* with the same JSON-normalized `arguments`, and the sub-call's complete
* model-facing outcome in `tool/result`'s own vocabulary
* (`content` + `isError`), so UIs render a sub-call through the exact
* code path that renders a native call. Every started sub-call settles
* with exactly one of these (abort included: the aborted pipeline result
* is an `isError` outcome).
* Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
* model context; persistence and UIs get every call. Appended inside the
* parent `run_code`'s execution (the bridge drains its queue before
* returning), so the turn-enclosure invariant holds by construction.
* parent `run_code`'s execution (the bridge drains in-flight dispatches
* before returning), so the turn-enclosure invariant holds by
* construction.
*/
'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; resultSummary: string }
'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; content: ContentBlock[] }
}
}
@@ -55,35 +70,6 @@ export class CodeRunFailedError extends HarnessError {
}
}
/**
* Cap for a `tool/code-dispatch` event's `resultSummary`. A log-ergonomics
* constant, not config: the full result already flows to the program; the
* summary exists so log readers see what a sub-call returned at a glance.
*/
const SUMMARY_MAX_CHARS = 200
/** Join Native content for the bounded durable sub-dispatch summary; non-text blocks become diagnostic placeholders. */
function textOf(content: ContentBlock[]): string {
return content
.map((block) => {
switch (block.type) {
case 'text': return block.text
// ContentBlockMap is merge-extensible — future block kinds land here
// deliberately (no assertNever on merge-extensible unions).
default: return `[${block.type} content]`
}
})
.join('\n')
}
/** Normalize workspace paths, then bound a sub-call's model-facing text for its durable log summary. */
function summarize(text: string, cwd: string | undefined): string {
const stableText = cwd === undefined || cwd === parse(cwd).root
? text
: text.replaceAll(cwd, '.')
return stableText.length > SUMMARY_MAX_CHARS ? `${stableText.slice(0, SUMMARY_MAX_CHARS)}` : stableText
}
/**
* Snapshot one binding call's argument as lossless JSON, then snapshot that
* detached value again so dispatch and logging stay independent without
@@ -201,17 +187,20 @@ function renderValue(value: JsonValue): string {
type RunCodeOutput = { logs: string[]; result?: JsonValue }
/**
* Build the `run_code` {@link ToolDefinition}: one required `code` parameter,
* executed through the dispatch bridge described above. The
* Build the `run_code` {@link ToolDefinition}: required `code` and
* `description` parameters, executed through the dispatch bridge described
* above. The
* registry reserves it as presentation infrastructure under non-native modes,
* outside the filterable global/scoped capability layers.
* @param registry - the owning registry (sub-calls go through its `execute`,
* bindings cover its registered tools).
* @param requireRuntime - resolves `ctx.codeRuntime` or throws the loud
* misconfiguration error (shared with the registry's assembly-time checks).
* @param maxParallel - the run's overlap cap for parallel-classified
* sub-calls (the registry passes its validated `maxParallelSubCalls`).
* @returns the registry-ready definition.
*/
export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => CodeRuntime): ToolDefinition {
export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => CodeRuntime, maxParallel: number): ToolDefinition {
return defineTool({
name: RUN_CODE_NAME,
description:
@@ -221,6 +210,13 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
+ 'Only what you print or return comes back — curate it.',
parameters: {
code: { type: 'string', required: true, description: 'The program: the body of an async TypeScript function.' },
description: {
type: 'string',
required: true,
description: 'Clear, concise description of what this program does in active voice, '
+ '5-10 words (shown in the UI). Examples: "Count TODO markers across packages"; '
+ '"Read failing test and its fixture"; "Rename config key in every cordis.yml".',
},
},
output: {
schema: {
@@ -238,6 +234,9 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
},
},
async execute(args, exec): Promise<RunCodeOutput> {
if (args.description.trim().length === 0) {
throw new Error('invalid description: expected a non-empty string')
}
const runtime = requireRuntime()
// The run-scoped abort: follows the outer signal in, and fires when the
@@ -249,19 +248,115 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
exec.signal.addEventListener('abort', onOuterAbort, { once: true })
let dispatches = 0
// The per-run serialization queue: every binding call chains onto the tail, so even
// `Promise.all` executes the underlying tool calls one at a time in submission order (the
// tool contract carries no concurrency-safety metadata yet).
let queue: Promise<void> = Promise.resolve()
const enqueue = <T>(task: () => Promise<T>): Promise<T> => {
const turn = queue.then(() => {
if (runController.signal.aborted) {
throw new Error(`run_code run is over (${String(runController.signal.reason)}); tool call abandoned`)
// The per-run scheduler, reusing the NATIVE concurrency contract through
// the registry's staged view (the loop scheduler's own seam) — and the
// native loop's SEQUENCING: every ordered stage (the dispatch-start
// append, prepare = pre-execute/guards, finalize/finish = post-execute,
// context deferral, the settle append) runs inside ONE driver lane, so
// ordered policy stages never overlap each other and only the
// around-dispatch/body stage runs concurrently. Starts are strictly
// submission-ordered; results commit in submission order through the
// head-of-line cursor. Consecutive parallel-classified calls overlap up
// to maxParallel; an exclusive call waits for the pool to drain, runs
// alone, and holds its barrier until its COMMIT (post-execute included)
// completes, exactly like a native exclusive group. Classification is
// re-read via executionMode() immediately before each start (a registry
// mutation while queued can flip a call exclusive), matching the native
// scheduler's lazy reclassification.
interface PendingDispatch {
/** Ordered stage: append the start event, await prepare (pre-execute/guards), launch the body into `flight`. */
start(): Promise<void>
classify(): 'parallel' | 'exclusive'
abandon(): void
/** Ordered stage: post-execute + context deferral + settle event, in submission order. */
commit(): Promise<void>
/** The launched around-dispatch/body stage; resolved until start() replaces it. */
flight: Promise<void>
/** True once the dispatch stage parked its outcome; the commit cursor waits on it. */
settled: boolean
/** The classification this entry started under; an exclusive holds its barrier through commit(). */
mode?: 'parallel' | 'exclusive'
}
const pendingQueue: PendingDispatch[] = []
const inFlight = new Set<Promise<void>>()
const commitQueue: PendingDispatch[] = []
let exclusiveActive = false
let driving = false
let driverRun: Promise<void> = Promise.resolve()
let wake: (() => void) | undefined
const wakeup = (): void => {
const release = wake
wake = undefined
release?.()
}
/**
* The single ordered lane. Each pass commits the head-of-line settled
* dispatch (ordered post-execute), then starts the next queued entry if
* its slot is free (ordered pre-execute), and otherwise sleeps until a
* body settles or a new submission arrives. One run reaching the
* empty-queues/empty-pool state is quiescence.
*/
const drive = (): Promise<void> => {
if (driving) return driverRun
driving = true
driverRun = (async () => {
try {
for (;;) {
// Arm before inspecting state so a settle or submission landing
// between the checks and the await below cannot be lost.
const signal = new Promise<void>((resolve) => { wake = resolve })
const commitHead = commitQueue[0]
if (commitHead !== undefined && commitHead.settled) {
commitQueue.shift()
await commitHead.commit()
// The barrier covers post-execute: later starts wait for the
// exclusive call's full pipeline, as under the native loop.
if (commitHead.mode === 'exclusive') exclusiveActive = false
continue
}
const head = pendingQueue[0]
if (head !== undefined) {
if (runController.signal.aborted) {
pendingQueue.shift()
head.abandon()
continue
}
// Reclassify at start time (fail-closed on registry changes).
const mode = head.classify()
const capacity = !exclusiveActive
&& (mode === 'exclusive' ? inFlight.size === 0 : inFlight.size < maxParallel)
if (capacity) {
if (mode === 'exclusive') exclusiveActive = true
head.mode = mode
pendingQueue.shift()
// Joined before start() so the commit cursor sees submission
// order; nothing commits it until `settled` flips.
commitQueue.push(head)
await head.start()
const flight: Promise<void> = head.flight.finally(() => {
inFlight.delete(flight)
wakeup()
})
inFlight.add(flight)
continue
}
}
if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0) return
await signal
}
} finally {
driving = false
wake = undefined
}
return task()
})
queue = turn.then(() => undefined, () => undefined)
return turn
})()
return driverRun
}
/** Every dispatch settled AND committed; nothing can start (the run is aborted at call time). */
const drainDispatches = async (): Promise<void> => {
// The abort already fired: the driver abandons queued-unstarted
// entries, awaits the live pool, and drains the ordered commit lane —
// including a commit already in progress when the program returned.
await drive()
}
// Read through a call, not a bare property: the abort state genuinely
@@ -274,35 +369,85 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`)
}
const normalized = jsonNormalizeArgs(rawArgs)
const outcome = await enqueue(async () => {
const n = ++dispatches
const subCallId = CallId(`${String(exec.callId)}:code:${n}`)
const result = await registry.execute({
callId: subCallId,
name,
arguments: normalized.dispatched,
...exec.agent ? { agent: exec.agent } : {},
parent: exec.token,
signal: runController.signal,
})
for (const context of result.additionalContexts ?? []) {
exec.deferContext(context)
const n = ++dispatches
const subCallId = CallId(`${String(exec.callId)}:code:${n}`)
const input = {
callId: subCallId,
name,
arguments: normalized.dispatched,
...exec.agent ? { agent: exec.agent } : {},
parent: exec.token,
signal: runController.signal,
}
type DispatchOutcome = { isError: true; message: string } | { isError: false; value: JsonValue }
const scheduler = registry[TOOL_REGISTRY_SCHEDULER]
const outcome = await new Promise<DispatchOutcome>((resolve, reject) => {
// Set by the dispatch stage (or start() for a pre-settled result): what commit() finalizes in submission order.
let parked:
| { kind: 'post-result' | 'final-result'; exec: ToolRunContext; result: ToolExecutionResult }
| undefined
const settle = (result: ToolExecutionResult): void => {
exec.agent?.session.append('tool/code-dispatch', {
parentCallId: exec.callId,
subCallId,
name,
// The SIBLING parse of the dispatched value: byte-identical JSON,
// but a separate object — a tool mutating its args cannot desync
// this record from what it actually received.
arguments: normalized.logged,
isError: result.isError,
// The registry deep-froze this projection at result finalization;
// append snapshots it again, so the log copy stays detached.
content: result.content,
})
resolve(result.isError
? { isError: true, message: result.error.message }
: { isError: false, value: result.value })
}
const text = textOf(result.content)
exec.agent?.session.append('tool/code-dispatch', {
parentCallId: exec.callId,
subCallId,
name,
// The SIBLING parse of the dispatched value: byte-identical JSON,
// but a separate object — a tool mutating its args cannot desync
// this record from what it actually received.
arguments: normalized.logged,
isError: result.isError,
resultSummary: summarize(text, exec.agent.session.header.cwd),
pendingQueue.push({
flight: Promise.resolve(),
settled: false,
// Re-read per driver pass against the same agent view the SDK
// declared; fail-closed exclusive when undeclared/invalid.
classify: () => registry.executionMode(input).kind,
abandon: () => {
reject(new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`))
},
async start(): Promise<void> {
exec.agent?.session.append('tool/code-dispatch-start', {
parentCallId: exec.callId,
subCallId,
name,
arguments: normalized.logged,
})
// Ordered prepare runs INSIDE the driver lane: the next entry's
// pre-execute waits for this resolution, as under the native
// scheduler. Only the launched body below overlaps.
const prepared = await scheduler.prepare(input)
if (prepared.kind === 'dispatch') {
this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
parked = { kind: dispatchOutcome.kind, exec: prepared.exec, result: dispatchOutcome.result }
this.settled = true
})
return
}
parked = { kind: prepared.kind, exec: prepared.exec, result: prepared.result }
this.settled = true
},
async commit(): Promise<void> {
/* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */
if (parked === undefined) return
const result = parked.kind === 'post-result'
? await scheduler.finalize(parked.exec, parked.result)
: scheduler.finish(parked.exec, parked.result)
for (const context of result.additionalContexts ?? []) {
exec.deferContext(context)
}
settle(result)
},
})
return result.isError
? { isError: true as const, message: result.error.message }
: { isError: false as const, value: result.value }
wakeup()
void drive()
})
// A budget expiry or outer cancel that lands while this call was in
// flight already aborted the dispatch; stop the program now rather
@@ -345,10 +490,11 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
signal: runController.signal,
})
} finally {
// Abort sub-dispatches and drain the folded queue before closing the turn.
// Abort sub-dispatches and drain every in-flight dispatch before
// closing the turn (queued-unstarted ones are abandoned unlogged).
// Binding failures remain observable through their individual promises.
runController.abort('run_code settled')
await queue
await drainDispatches()
}
if (result.error) {
@@ -363,10 +509,11 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
exec.signal.removeEventListener('abort', onOuterAbort)
}
},
// The program is the call's always-visible UI label.
// The model-authored description is the call's always-visible UI label
// (the bash `description` precedent); the program itself rides rawInput.
presentCall: args => ({
card: 'generic',
title: args.code,
title: args.description,
kind: 'execute',
rawInput: args.code,
}),

View File

@@ -534,6 +534,14 @@ export interface Config {
* absent or mismatched. Under `code`, native names in `toolOrder` are invalid.
*/
mode?: ToolPresentationMode
/**
* Concurrency cap for a `run_code` program's overlapping sub-calls
* (default 10, the loop scheduler's own default). Sub-calls follow the
* native scheduling contract — only calls whose tools classify
* concurrency-safe overlap; exclusive calls form barriers — so `1`
* restores strictly serial dispatch. Must be a positive integer.
*/
maxParallelSubCalls?: number
}
/**
@@ -627,6 +635,15 @@ interface FusedToolSignal {
dispose(): void
}
/** Resolve the run_code overlap cap at the owning config boundary (direct construction bypasses the Loader schema). */
function resolveMaxParallelSubCalls(value: number | undefined): number {
const maxParallelSubCalls = value ?? 10
if (!Number.isInteger(maxParallelSubCalls) || maxParallelSubCalls < 1) {
throw new Error('maxParallelSubCalls must be a positive integer')
}
return maxParallelSubCalls
}
/**
* Tool registry and execution pipeline. Scoped registrations shadow globals;
* one visibility resolver feeds presentation, lookup, and dispatch.
@@ -636,6 +653,7 @@ export class ToolRegistry extends Service {
static Config: z<Config> = z.object({
mode: z.union(['native', 'code', 'both'] as const).default('native'),
maxParallelSubCalls: z.natural().min(1).default(10),
})
/** Internal staged view consumed by `dsh-agent-loop`'s parallel scheduler. */
@@ -672,7 +690,7 @@ export class ToolRegistry extends Service {
// the filterable global/scoped capability layers.
this.codeTransport = this.mode === 'native'
? undefined
: createRunCodeTool(this, () => this.requireCodeRuntime())
: createRunCodeTool(this, () => this.requireCodeRuntime(), resolveMaxParallelSubCalls(config.maxParallelSubCalls))
ctx.systemPrompt.tools(context => this.wireSchemas(context.scope))
if (this.mode !== 'native') {
ctx.systemPrompt.section({

View File

@@ -253,7 +253,7 @@ Pass \`run_code\` the body of an async TypeScript function (erasable syntax only
- Call tools as \`await tools.name(args)\` — quoted access for exotic names: \`tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose \`message\` is human-readable — \`try/catch\` it to handle and continue.
- Calls execute sequentially, even under \`Promise.all\`.
- Independent read-only calls MAY overlap under \`Promise.all\` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with \`await\`.
- Emit results with \`return\` and/or \`console.log(...)\`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.
The available tools:`

View File

@@ -42,6 +42,7 @@ class FakeRuntime extends CodeRuntime {
interface SetupOptions {
mode?: Config['mode']
maxParallelSubCalls?: number
runtime?: false | { language?: string }
toolOrder?: string[]
}
@@ -49,7 +50,7 @@ interface SetupOptions {
async function setup(options: SetupOptions = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { ...options.toolOrder ? { toolOrder: options.toolOrder } : {} })
await ctx.plugin(ToolRegistry, { mode: options.mode ?? 'code' })
await ctx.plugin(ToolRegistry, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
let runtime: FakeRuntime | undefined
if (options.runtime !== false) {
await ctx.plugin(FakeRuntime, options.runtime ?? {})
@@ -87,11 +88,11 @@ function registerEcho(ctx: Context, name = 'echo'): unknown[] {
}
/** A structural fake of the owning agent: captures session appends. */
function fakeAgent(options: { cwd?: string } = { cwd: '/workspace' }): { agent: Agent; events: { type: string; data: unknown }[] } {
function fakeAgent(): { agent: Agent; events: { type: string; data: unknown }[] } {
const events: { type: string; data: unknown }[] = []
const agent = {
session: {
header: options.cwd === undefined ? {} : { cwd: options.cwd },
header: { cwd: '/workspace' },
append: (type: string, data: unknown) => { events.push({ type, data }) },
},
} as unknown as Agent
@@ -99,12 +100,16 @@ function fakeAgent(options: { cwd?: string } = { cwd: '/workspace' }): { agent:
}
/** Dispatch run_code through the registry pipeline, as the loop would. */
async function runCode(ctx: Context, code: string, extras: { agent?: Agent; signal?: AbortSignal } = {}): Promise<ToolExecutionResult> {
async function runCode(
ctx: Context,
code: string,
extras: { agent?: Agent; signal?: AbortSignal; description?: string } = {},
): Promise<ToolExecutionResult> {
return ctx.tools.execute({
signal: testToolSignal,
callId: CallId('call-1'),
name: RUN_CODE_NAME,
arguments: { code },
arguments: { code, description: extras.description ?? 'Run the test program' },
...extras.agent ? { agent: extras.agent } : {},
...extras.signal ? { signal: extras.signal } : {},
})
@@ -354,6 +359,331 @@ describe('mode-aware wire contribution', () => {
})
})
describe('the sub-dispatch scheduler (native concurrency contract)', () => {
/** Register a tool whose calls resolve only when the test releases them; returns live-call telemetry. */
function registerGated(ctx: Context, name: string, concurrencySafe: boolean) {
const gates: (() => void)[] = []
let live = 0
let peak = 0
const order: string[] = []
ctx.tools.register(defineTool({
name,
description: `Gated tool ${name}.`,
parameters: { id: { type: 'string', required: true } },
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
...concurrencySafe ? { isConcurrencySafe: () => true } : {},
async execute(args, exec) {
order.push(`start:${args.id}`)
live++
peak = Math.max(peak, live)
// Abort-observing like a real tool: the run-scoped abort releases the
// gate so the bridge's drain reaches quiescence.
await new Promise<void>((release) => {
gates.push(release)
exec.signal.addEventListener('abort', () => { release() }, { once: true })
})
live--
order.push(`end:${args.id}`)
return `${name}:${args.id}`
},
}))
const release = (): void => { gates.shift()?.() }
const releaseAll = (): void => { while (gates.length > 0) gates.shift()!() }
return { order, release, releaseAll, peakLive: () => peak, pending: () => gates.length }
}
it('overlaps concurrency-safe calls under Promise.all and logs a start event per dispatch', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const gated = registerGated(ctx, 'safe_read', true)
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const all = Promise.all([
tools.safe_read!({ id: 'a' }),
tools.safe_read!({ id: 'b' }),
tools.safe_read!({ id: 'c' }),
])
// All three must be START-able without any completion (overlap proof).
await expect.poll(() => gated.pending()).toBe(3)
gated.releaseAll()
return { logs: [], value: (await all).map(String).join(',') }
}
const result = await runCode(ctx, 'program', { agent })
expect(result.isError).toBe(false)
expect(gated.peakLive()).toBe(3)
if (result.isError) throw new Error('expected success')
expect(result.value).toMatchObject({ result: 'safe_read:a,safe_read:b,safe_read:c' })
// One start per dispatch, paired with its settle by subCallId, starts in submission order.
const starts = events.filter(event => event.type === 'tool/code-dispatch-start').map(event => event.data as { subCallId: string })
const settles = events.filter(event => event.type === 'tool/code-dispatch').map(event => event.data as { subCallId: string })
expect(starts.map(start => start.subCallId)).toEqual(['call-1:code:1', 'call-1:code:2', 'call-1:code:3'])
expect(new Set(settles.map(settle => settle.subCallId))).toEqual(new Set(starts.map(start => start.subCallId)))
})
it('an exclusive call bars overlap: safe calls drain first, it runs alone, later calls wait', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const safe = registerGated(ctx, 'safe_read', true)
const unsafe = registerGated(ctx, 'writer', false)
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const reads = [tools.safe_read!({ id: 'r1' }), tools.safe_read!({ id: 'r2' })]
const write = tools.writer!({ id: 'w' })
const tail = tools.safe_read!({ id: 'r3' })
await expect.poll(() => safe.pending()).toBe(2)
// The exclusive call must NOT have started while the pool is live.
expect(unsafe.pending()).toBe(0)
safe.releaseAll()
await expect.poll(() => unsafe.pending()).toBe(1)
// The trailing safe call must NOT start while the exclusive one runs.
expect(safe.pending()).toBe(0)
unsafe.release()
await expect.poll(() => safe.pending()).toBe(1)
safe.releaseAll()
await Promise.all([...reads, write, tail])
return { logs: [], value: 'ordered' }
}
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(false)
expect(safe.order.slice(0, 2)).toEqual(['start:r1', 'start:r2'])
expect(unsafe.order).toEqual(['start:w', 'end:w'])
// r3 started only after w ended.
expect(safe.order.indexOf('start:r3')).toBeGreaterThan(safe.order.indexOf('end:r1'))
})
it('maxParallelSubCalls caps the overlap window', async () => {
const { ctx, runtime } = await setup({ mode: 'code', maxParallelSubCalls: 2 })
const gated = registerGated(ctx, 'safe_read', true)
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const all = Promise.all([
tools.safe_read!({ id: 'a' }),
tools.safe_read!({ id: 'b' }),
tools.safe_read!({ id: 'c' }),
])
await expect.poll(() => gated.pending()).toBe(2)
// The third call waits for a slot.
expect(gated.pending()).toBe(2)
gated.release()
await expect.poll(() => gated.pending()).toBe(2)
gated.releaseAll()
await all
return { logs: [], value: 'capped' }
}
const result = await runCode(ctx, 'program')
if (result.isError) console.error('CAP-FAIL:', (result.content[0] as { text: string }).text)
expect(result.isError).toBe(false)
expect(gated.peakLive()).toBe(2)
})
it('a tool unregistered between binding enumeration and dispatch fails as unknown tool', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls: unknown[] = []
const dispose = ctx.tools.register(defineTool({
name: 'ephemeral',
description: 'Unregistered between binding enumeration and dispatch.',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute() {
calls.push('ran')
return Promise.resolve('ok')
},
}))
runtime.behavior = async (request) => {
// The binding exists (enumerated at run start); the registry mutation
// makes prepare resolve UNKNOWN_TOOL as a final-result, which commits
// through scheduler.finish (no post-execute).
dispose()
const message = await request.bindings[0]!.functions.ephemeral!({})
.then(() => 'resolved', (error: unknown) => error instanceof Error ? error.message : String(error))
return { logs: [], value: message }
}
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected success')
expect(result.value).toMatchObject({ result: 'unknown tool "ephemeral"' })
expect(calls).toEqual([])
})
it('ordered pre-execute never overlaps: a slow policy on one call delays the next start', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const gated = registerGated(ctx, 'safe_read', true)
const stages: string[] = []
let releaseGate: (() => void) | undefined
ctx.on('tools/pre-execute', async (preExec, next) => {
if (preExec.name !== 'safe_read') return next()
stages.push(`pre-enter:${String(preExec.callId)}`)
if (releaseGate === undefined) {
// The FIRST call's policy awaits an asynchronous decision.
await new Promise<void>((resolve) => { releaseGate = resolve })
}
stages.push(`pre-exit:${String(preExec.callId)}`)
return next()
})
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const all = Promise.all([tools.safe_read!({ id: 'a' }), tools.safe_read!({ id: 'b' })])
// Both submissions are in; the second pre-execute must NOT have entered
// while the first is still awaiting its policy decision.
await expect.poll(() => stages.length).toBeGreaterThanOrEqual(1)
expect(stages).toEqual(['pre-enter:call-1:code:1'])
releaseGate!()
await expect.poll(() => gated.pending()).toBe(2)
gated.releaseAll()
await all
return { logs: [], value: 'ordered-prepare' }
}
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(false)
expect(stages).toEqual([
'pre-enter:call-1:code:1', 'pre-exit:call-1:code:1',
'pre-enter:call-1:code:2', 'pre-exit:call-1:code:2',
])
})
it('an exclusive call holds its barrier through post-execute: the next start waits for the commit', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const writer = registerGated(ctx, 'writer', false)
const reader = registerGated(ctx, 'safe_read', true)
const stages: string[] = []
let releasePost: (() => void) | undefined
ctx.on('tools/post-execute', async (postExec, _result, next): Promise<PostToolDecision> => {
if (postExec.name === 'writer') {
stages.push('post-enter:writer')
await new Promise<void>((resolve) => { releasePost = resolve })
stages.push('post-exit:writer')
}
return next()
})
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const w = tools.writer!({ id: 'w' })
const r = tools.safe_read!({ id: 'r' })
await expect.poll(() => writer.pending()).toBe(1)
writer.release()
// The writer's body is done and its async post-execute is running; the
// parallel read must not have STARTED (no pre/body) while the exclusive
// call's pipeline is still open.
await expect.poll(() => stages).toContain('post-enter:writer')
expect(reader.pending()).toBe(0)
releasePost!()
await w
await expect.poll(() => reader.pending()).toBe(1)
reader.releaseAll()
await r
return { logs: [], value: 'barrier-through-commit' }
}
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(false)
expect(stages).toEqual(['post-enter:writer', 'post-exit:writer'])
})
it('run settlement drains a commit already in progress: the settle event lands inside the turn', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const gated = registerGated(ctx, 'safe_read', true)
const { agent, events } = fakeAgent()
let releasePost: (() => void) | undefined
ctx.on('tools/post-execute', async (postExec, _result, next): Promise<PostToolDecision> => {
if (postExec.name === 'safe_read') {
await new Promise<void>((resolve) => { releasePost = resolve })
}
return next()
})
runtime.behavior = async (request) => {
// Fire-and-forget: the program returns while the sub-call's async
// post-execute commit is mid-flight.
request.bindings[0]!.functions.safe_read!({ id: 'a' }).catch(() => 'run-over')
await expect.poll(() => gated.pending()).toBe(1)
gated.release()
await expect.poll(() => releasePost !== undefined).toBe(true)
queueMicrotask(() => { releasePost!() })
return { logs: [], value: 'returned-early' }
}
const result = await runCode(ctx, 'program', { agent })
expect(result.isError).toBe(false)
// The drain awaited the in-progress commit: the settle event exists and
// preceded the run_code turn closing (all appends happen inside
// execute()). The run's settlement aborted the sub-call's signal while
// its post-execute was mid-flight, so the native cancellation contract
// replaces the successful outcome with the aborted result — the event is
// still durable and in-turn, which is the invariant under test.
const settles = events.filter(event => event.type === 'tool/code-dispatch')
expect(settles).toHaveLength(1)
expect(settles[0]?.data).toMatchObject({ name: 'safe_read', isError: true })
})
it('post-execute and context commitment stay in submission order under out-of-order completion', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const gated = registerGated(ctx, 'safe_read', true)
const postOrder: string[] = []
ctx.on('tools/post-execute', async (postExec, _result, next): Promise<PostToolDecision> => {
if (postExec.name === 'safe_read') {
postOrder.push(String(postExec.callId))
return {
kind: 'accept' as const,
additionalContexts: [{
content: [{ type: 'text' as const, text: `ctx:${String(postExec.callId)}` }],
source: { kind: 'plugin' as const, plugin: 'order-probe' },
}],
}
}
return next()
})
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const all = Promise.all([tools.safe_read!({ id: 'a' }), tools.safe_read!({ id: 'b' })])
await expect.poll(() => gated.pending()).toBe(2)
// Complete b FIRST (out of submission order), then a.
gated.release() // releases a (FIFO gate) — invert: release twice reversed is not possible;
gated.releaseAll()
await all
return { logs: [], value: 'ordered-commit' }
}
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(false)
// Post-execute observed submission order regardless of completion interleave.
expect(postOrder).toEqual(['call-1:code:1', 'call-1:code:2'])
// Deferred contexts reach the outer result in the same order.
expect(result.additionalContexts?.map(c => (c.content[0] as { text: string }).text))
.toEqual(['ctx:call-1:code:1', 'ctx:call-1:code:2'])
})
it('a queued-unstarted call abandoned by run settlement logs no start event', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const gated = registerGated(ctx, 'writer', false)
const { agent, events } = fakeAgent()
const abandoned: string[] = []
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
// First exclusive call occupies the pool; the second queues unstarted.
// Both rejections are captured (abandonment fires only at settlement,
// AFTER this program has already failed — awaiting it here would deadlock).
tools.writer!({ id: 'w1' }).catch(() => 'settled-under-abort')
tools.writer!({ id: 'w2' }).catch((error: unknown) => {
abandoned.push(error instanceof Error ? error.message : String(error))
})
await expect.poll(() => gated.pending()).toBe(1)
// Fail the program while w1 is in flight and w2 is queued unstarted.
throw new Error('program failed with a queued call')
}
const result = await runCode(ctx, 'program', { agent })
expect(result.isError).toBe(true)
const starts = events.filter(event => event.type === 'tool/code-dispatch-start').map(event => (event.data as { subCallId: string }).subCallId)
const settles = events.filter(event => event.type === 'tool/code-dispatch').map(event => (event.data as { subCallId: string }).subCallId)
// w1 started and settled under the abort; w2 never started and never
// settled — no start event, no settle event, binding rejected with the
// abandonment message at drain time.
expect(starts).toEqual(['call-1:code:1'])
expect(settles).toEqual(['call-1:code:1'])
expect(abandoned).toEqual(['run_code run is over (run_code settled); writer tool call abandoned'])
})
})
describe('the run_code dispatch bridge', () => {
it('bridges tool calls, returns only the curated output, and logs one event per dispatch', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
@@ -374,8 +704,14 @@ describe('the run_code dispatch bridge', () => {
expect(calls).toEqual([{ value: 'one' }, { value: 'two' }])
const dispatches = events.filter(event => event.type === 'tool/code-dispatch')
expect(dispatches.map(event => event.data)).toEqual([
{ parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo', arguments: { value: 'one' }, isError: false, resultSummary: 'echo:one' },
{ parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo', arguments: { value: 'two' }, isError: false, resultSummary: 'echo:two' },
{
parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo',
arguments: { value: 'one' }, isError: false, content: [{ type: 'text', text: 'echo:one' }],
},
{
parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo',
arguments: { value: 'two' }, isError: false, content: [{ type: 'text', text: 'echo:two' }],
},
])
expect(result.meta).toBeUndefined()
})
@@ -465,6 +801,37 @@ describe('the run_code dispatch bridge', () => {
expect(result.content[0]).toEqual({ type: 'text', text: 'caught: deliberate failure' })
})
it('a throwing tools/pre-execute listener settles the sub-call without post-execute', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)
const postExecuted: string[] = []
ctx.on('tools/pre-execute', (exec, next) => {
if (exec.name === 'echo') throw new Error('gate exploded')
return next()
})
ctx.on('tools/post-execute', (exec, _result, next): Promise<PostToolDecision> => {
if (exec.name === 'echo') postExecuted.push(exec.name)
return next()
})
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
const message = await request.bindings[0]!.functions.echo!({ value: 'x' })
.then(() => 'resolved', (error: unknown) => error instanceof Error ? error.message : String(error))
return { logs: [], value: message }
}
const result = await runCode(ctx, 'program', { agent })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected success')
expect(result.value).toMatchObject({ result: 'gate exploded' })
// The pipeline failure is final: the body never ran and post-execute was
// skipped, yet the settle event still carries the error outcome.
expect(calls).toEqual([])
expect(postExecuted).toEqual([])
const settles = events.filter(event => event.type === 'tool/code-dispatch')
expect(settles).toHaveLength(1)
expect(settles[0]?.data).toMatchObject({ name: 'echo', isError: true })
})
it('a tools/pre-execute deny reaches the program as a binding rejection', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
registerEcho(ctx)
@@ -693,19 +1060,26 @@ describe('the run_code dispatch bridge', () => {
expect((result.content[0] as { text: string }).text).toContain('requires a code runtime')
})
it('presents the program as the execute-card title', async () => {
it('presents the model-authored description as the execute-card title over the program input', async () => {
const { ctx } = await setup({ mode: 'code' })
const tool = ctx.tools.get(RUN_CODE_NAME)!
// The program is the title, mirroring how command tools label their cards
// with the command while retaining the same value in the expanded input.
expect(tool.presentCall?.({ code: 'return 1' })).toEqual({
// The description labels the card (the bash description precedent); the
// program itself remains the expanded raw input.
expect(tool.presentCall?.({ code: 'return 1', description: 'Return the constant one' })).toEqual({
card: 'generic',
title: 'return 1',
title: 'Return the constant one',
kind: 'execute',
rawInput: 'return 1',
})
})
it('rejects a whitespace-only description with a structured isError', async () => {
const { ctx } = await setup({ mode: 'code' })
const result = await runCode(ctx, 'return 1', { description: ' ' })
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('invalid description')
})
it.each([
['logs only', { logs: ['printed'] }, 'printed'],
['result only', { logs: [], value: 'returned' }, 'returned'],
@@ -759,7 +1133,7 @@ describe('the run_code dispatch bridge', () => {
expect('presentResult' in tool).toBe(false)
})
it('renders non-text sub-result blocks as placeholders and truncates long event summaries', async () => {
it('logs the complete sub-result content verbatim, non-text blocks and long text included', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const { agent, events } = fakeAgent()
const long = 'x'.repeat(300)
@@ -786,58 +1160,10 @@ describe('the run_code dispatch bridge', () => {
expect(result.isError).toBe(false)
expect((result.content[0] as { text: string }).text).toBe('mixed-value')
const dispatch = events.find(event => event.type === 'tool/code-dispatch')?.data as SessionEventMap['tool/code-dispatch']
expect(dispatch.resultSummary.length).toBe(201)
expect(dispatch.resultSummary.endsWith('…')).toBe(true)
})
it('normalizes the session workspace root before bounding durable result summaries', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
ctx.tools.register(defineTool({
name: 'workspace_path',
description: 'Return a path beneath the session workspace.',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute(_args, exec) {
const cwd = exec.agent?.session.header.cwd ?? ''
return Promise.resolve(`<path>${cwd}/nested/task.txt</path>\n${'x'.repeat(240)}`)
},
}))
runtime.behavior = async request => ({
logs: [],
value: await request.bindings[0]!.functions.workspace_path!({}),
})
const short = fakeAgent({ cwd: '/tmp/workspace' })
const long = fakeAgent({ cwd: `/tmp/${'long-segment/'.repeat(30)}workspace` })
const shortResult = await runCode(ctx, 'program', { agent: short.agent })
const longResult = await runCode(ctx, 'program', { agent: long.agent })
const shortDispatch = short.events[0]!.data as SessionEventMap['tool/code-dispatch']
const longDispatch = long.events[0]!.data as SessionEventMap['tool/code-dispatch']
expect(shortResult.content).not.toEqual(longResult.content)
expect(shortDispatch.resultSummary).toBe(longDispatch.resultSummary)
expect(shortDispatch.resultSummary).toHaveLength(201)
expect(shortDispatch.resultSummary).toMatch(/^<path>\.\/nested\/task\.txt<\/path>\n.+…$/)
})
it('leaves result summaries unchanged when a session cwd is absent or is the filesystem root', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
registerEcho(ctx)
runtime.behavior = async request => ({
logs: [],
value: await request.bindings[0]!.functions.echo!({ value: '/workspace/value' }),
})
const absent = fakeAgent({})
const root = fakeAgent({ cwd: '/' })
await runCode(ctx, 'program', { agent: absent.agent })
await runCode(ctx, 'program', { agent: root.agent })
expect((absent.events[0]!.data as SessionEventMap['tool/code-dispatch']).resultSummary).toBe('echo:/workspace/value')
expect((root.events[0]!.data as SessionEventMap['tool/code-dispatch']).resultSummary).toBe('echo:/workspace/value')
expect(dispatch.content).toEqual([
{ type: 'text', text: long },
{ type: 'reasoning', text: 'hidden' },
])
})
it('rejects undefined, getter-throwing, exotic, and unrepresentable binding arguments before dispatch', async () => {
@@ -1067,13 +1393,27 @@ describe('the run_code dispatch bridge', () => {
name: 'echo',
arguments: { value: 'x' },
isError: false,
resultSummary: 'echo:x',
content: [{ type: 'text', text: 'echo:x' }],
})
const derived = session.deriveMessages()
expect(derived).toHaveLength(1)
expect(derived[0]?.role).toBe('user')
})
it('direct construction rejects a non-positive parallel sub-call cap at load', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
expect(() => new ToolRegistry(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
.toThrow('maxParallelSubCalls must be a positive integer')
})
it('direct construction in code mode defaults the parallel sub-call cap', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx, { mode: 'code' })
expect(registry.get(RUN_CODE_NAME)).toBeDefined()
})
it('defaults to native mode under direct construction with no config', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})

View File

@@ -144,7 +144,7 @@ describe('renderToolsSdk', () => {
// The fixed instruction lines the model relies on.
expect(text).toContain('erasable syntax only')
expect(text).toContain('rejects with `ToolCallError`')
expect(text).toContain('sequentially, even under `Promise.all`')
expect(text).toContain('MAY overlap under `Promise.all`')
expect(text).toContain('lossless JSON')
})