`(exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发包名为 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名仍为 `deepseek_harness` / `deepseek_harness_runtime`。
## 工作线程插件
@@ -62,7 +62,7 @@ exe 内支持 `dsh-workflow-workerthread` 与 `dsh-code-runtime-worker`。两个
## 测试
-验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化以下各处的不透明消息 ID:SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。
+验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化以下各处的不透明消息 ID:SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。
手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,短命管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
index 91deb7153c..ce530420a5 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
@@ -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/architecture/2026-07-12-agent-scope-runtime-design.md
-2026-07-12-agent-scope-runtime-design.md: 8903a2fefa83dba042f8105b182e590783a9adde
+2026-07-12-agent-scope-runtime-design.md: 5bee5f3f79be903848f8ecdf1ea84fe6fca60f6f
2026-07-12-agent-scope-runtime-design.zh.md: d86ab3d8cb9a051d4664aedac812b10b53521f22
diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
index 8903a2fefa..5bee5f3f79 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
@@ -25,7 +25,7 @@ The design can be skimmed as seven choices:
| Coordinate create/resume | One `AgentCreationTransaction` |
| Protect durable, queued, model, or wire data | Materialize once at that boundary |
| Pass typed values inside one process | Readonly borrowed contract |
-| Compose the model-visible prompt and tool surface | One shared tool view plus the authoritative assembly-waterfall result |
+| Compose the model-visible prompt and tool set | One shared tool view plus the authoritative assembly-waterfall result |
| Coordinate subagent, worker, and process shutdown | One cancellation signal plus the independent terminal/quiescence facts of that boundary |
The rest of this Agent Note expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks.
diff --git a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.i18n.yaml
index d4886965ad..bbecaabbbf 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.i18n.yaml
@@ -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/architecture/2026-07-12-scoped-layers-store.md
-2026-07-12-scoped-layers-store.md: c5186d1652bca617eed62ec02937f2d055ea727c
+2026-07-12-scoped-layers-store.md: 91df72cf18c3f28d49534793bee85094b299c9a5
2026-07-12-scoped-layers-store.zh.md: f5084426beef836d71e55c0898ec1edbdc7725f3
diff --git a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
index c5186d1652..91df72cf18 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
@@ -111,11 +111,11 @@ All seven facades keep validation and diagnostics in their owning registry and c
## Consequences
- Scope-aware registries express one aggregate layer and reuse the same construction, ownership, rollback, notification, and reclamation choreography. Domain-specific validation, diagnostics, filtering, evaluation, and observer policy remain in each registry.
-- The public read surface stays narrow: direct table iteration preserves explicitly live behavior, while `merge()` is the one shared materialized shadowing operation. A heterogeneous `ScopeLayer` has no layer-wide `values()` contract.
+- The public read API stays narrow: direct table iteration preserves explicitly live behavior, while `merge()` is the one shared materialized shadowing operation. A heterogeneous `ScopeLayer` has no layer-wide `values()` contract.
- The helper is deliberately synchronous. A future registration that needs asynchronous setup or several independently owned undos must identify its ownership and settlement boundaries before widening this contract.
- An action must throw before retaining a contribution or return an undo for everything it retained; the helper cannot repair mutation outside that contract. The provided entry operations are atomic, and migrated registries perform fallible validation before insertion.
- A scoped layer remains allocated until every table in its aggregate is empty. Disposing one facade therefore cannot discard sibling contributions owned by the same scope.
-- The four public symbols become a reusable package contract. Keeping `EntryValues` internal and consumer policy outside the helper limits the compatibility surface.
+- The four public symbols become a reusable package contract. Keeping `EntryValues` internal and consumer policy outside the helper limits the compatibility API.
- The migration changes no public registry behavior and no model-, human-, wire-, persistence-, configuration-, or dependency-graph output.
## Verification
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
index dd7ff64291..b1ffadacb3 100644
--- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
@@ -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/architecture/2026-07-14-provider-routed-llm-adapters.md
-2026-07-14-provider-routed-llm-adapters.md: 8bd34126d05140e486cd170bfc9255f0962dc413
-2026-07-14-provider-routed-llm-adapters.zh.md: 09d9032da5845d3e416dcfa047fde8e6527025b4
+2026-07-14-provider-routed-llm-adapters.md: df152a4436226ac5cb1941ce060a35bd4a0d496d
+2026-07-14-provider-routed-llm-adapters.zh.md: 181323c136a4f953d64928a8698720e164dd5ad1
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
index 8bd34126d0..df152a4436 100644
--- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
@@ -48,7 +48,7 @@ This state is model-visible replay input and therefore follows the existing [rec
### Propagate the target through every request producer
-Every model-selection surface carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains `provider` beside `model`.
+Every model-selection path carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains `provider` beside `model`.
Compaction configuration gains `summarizationProvider` beside `summarizationModel`. Both are empty to inherit, or both are non-empty to select an explicit target; a half-configured pair fails load. Inheritance uses the last logged request target when one exists and falls back to the agent's creation options. `compact/summary` records both fields with the existing model-call envelope.
@@ -66,7 +66,7 @@ The on-disk session format remains the pre-release pinned version `0`, with no c
**Let `dsh-llm-pi-ai` automatically register every pi-ai provider.** This would claim ambient credentials and provider names the deployment never intended to expose, and would conflict with native adapters such as `dsh-llm-deepseek`. Explicit profiles make capability and credential scope reviewable.
-**Mount one pi-ai plugin instance per provider.** Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle surface.
+**Mount one pi-ai plugin instance per provider.** Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle API.
**Accept arbitrary inline pi-ai model descriptors.** This would support catalog-external private model ids, but it exposes pi-ai's model and compatibility schema as Harness configuration and makes the adapter responsible for validating protocol-specific combinations. The first version supports custom endpoints by overriding `baseURL` on catalog models; custom descriptors require a separate decision after a real catalog-external deployment is identified.
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
index 09d9032da5..181323c136 100644
--- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
@@ -48,7 +48,7 @@ pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包
### 在所有请求生产方中传播目标
-每个模型选择接口都同时携带 provider 与 model:声明式 agent、ACP(Agent Client Protocol)和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`。
+每条模型选择路径都同时携带 provider 与 model:声明式 agent、ACP(Agent Client Protocol)和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`。
压缩配置在 `summarizationModel` 之外增加 `summarizationProvider`。两个值均为空时继承,均非空时选择显式目标;只配置其中一个会导致加载失败。继承优先使用最近一次记录的请求目标,没有时回退到 agent 创建选项。`compact/summary` 使用现有模型调用 envelope 记录两个字段。
@@ -60,7 +60,7 @@ JSON-RPC 运行时显式接收 provider 与 model。仅当 `deepseek` 提供方
**继续以模型名称作为注册表键,并增加通配适配器。** 通配机制会在精确注册与兜底插件之间引入回退顺序,使重复所有权取决于监听器顺序;若不再增加其他约定,仍无法区分不同提供方中相同的模型 ID。
-**将提供方与模型编码到一个字符串中。** OpenRouter 的 `openai/gpt-*` 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择接口,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
+**将提供方与模型编码到一个字符串中。** OpenRouter 的 `openai/gpt-*` 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择器,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
**增加 `backend + provider + model`。** backend 键可以让 `dsh-llm-deepseek` 与 pi-ai 的 DeepSeek 实现共存,并按请求切换。最终采用的部署规则是一个提供方对应一个适配器所有者:同一上游的不同实现属于由插件组合选定的替代项。第三个路由维度会增加每个请求与配置的负担,却没有当前消费方。
diff --git a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml
index 91c9baf2a0..6fe5951d90 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml
@@ -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/architecture/2026-07-15-lsp-capability-seam.md
-2026-07-15-lsp-capability-seam.md: a461432fe5f218ea6c3ff3e031b52f7ebffbd7a5
+2026-07-15-lsp-capability-seam.md: c407de5275da0a8323b3c6186e178c8b2fafdc31
2026-07-15-lsp-capability-seam.zh.md: bfbd8d04f71b154d2833c86f2a6dc84d7e13fff4
diff --git a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
index a461432fe5..c407de5275 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
+++ b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
@@ -137,7 +137,7 @@ Navigation maps `Location` directly and `LocationLink` from `targetUri` plus `ta
Abort reaches every query phase and sends `$/cancelRequest` once an id exists. An unresponsive server is terminated and awaited without collateral active work because the instance is serialized. Disposal rejects and cancels work, attempts graceful shutdown, escalates through bounded termination, and awaits quiescence.
-## Deliberately deferred surface
+## Deliberately deferred API
Symbols are deferred because they need different schemas and overlap read/search; a future workspace-symbol tool must accept a search query. Call hierarchy is deferred because support is uneven, and `prepareCallHierarchy` remains an internal prerequisite rather than a model operation.
diff --git a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml
index 19f77344e1..08ddfd9b1c 100644
--- a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml
@@ -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/architecture/2026-07-16-explicit-turn-cancellation.md
-2026-07-16-explicit-turn-cancellation.md: 5d7b5856ceebd3dc49564c1800004188da162fd8
+2026-07-16-explicit-turn-cancellation.md: 86faad9929d3eb5b00e66bb1c46a2e35b135d954
2026-07-16-explicit-turn-cancellation.zh.md: ba20b80f3c280087d71688ef4596fb075fa5782b
diff --git a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
index 5d7b5856ce..86faad9929 100644
--- a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
+++ b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
@@ -40,7 +40,7 @@ Initiator-scope tests assert that every hook still observes the exact Agent and
**Persist a free-form string reason.** Strings admit spelling drift, prevent exhaustive switching, and encourage consumers to parse presentation text. The runtime uses a closed discriminated union, while the terminal record needs only the stable aborted outcome.
-**Persist the typed caller cause in `turn/end`.** No production replay, UI, ACP, telemetry, or workflow consumer distinguishes `user` from `parent`. Copying the request source into the terminal result would conflate two facts and add Session-specific validation without a consumer; a future audit surface can record a separate cancellation-request event.
+**Persist the typed caller cause in `turn/end`.** No production replay, UI, ACP, telemetry, or workflow consumer distinguishes `user` from `parent`. Copying the request source into the terminal result would conflate two facts and add Session-specific validation without a consumer; a future audit trail can record a separate cancellation-request event.
**Define speculative `superseded`, `timeout`, and `shutdown` variants now.** No current Agent cancellation producer implements those semantics. `shutdown` is already lifecycle disposal, and timeout or supersession should enter the union only with an owning policy and unique terminal meaning.
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
index c7be4bbdf1..d467b9c37b 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
@@ -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/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
-2026-07-19-gui-layering-and-rpc-protocol.md: f9c95176321496e965a95b6358d6feaa8466fe89
-2026-07-19-gui-layering-and-rpc-protocol.zh.md: 7d20c5a2662c9036382b30a96bc9973c8f0349bd
+2026-07-19-gui-layering-and-rpc-protocol.md: deba5e81c35e1572e2e6dc68b48234414ac9a7d5
+2026-07-19-gui-layering-and-rpc-protocol.zh.md: e506c6242794b1b065136c8190f5c46a46e0d069
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
index f9c9517632..deba5e81c3 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
@@ -10,7 +10,7 @@ English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)
We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product clients are coming — Web (server), Electron, and others. We call them Clients and want the following capabilities:
-- One `dsh` process supporting both `dsh web` (serve) and `dsh run` (headless) — one process, two modes (a design reservation)
+- One `dsh` process supporting both `dsh web` (serve) and `dsh --profile headless` (headless) — one process, two modes (a design reservation)
- Launching inside Electron with the same Web technologies as `dsh web`
That demands a stable layered responsibility model in the engineering codebase, so future clients plug in cleanly.
@@ -27,11 +27,11 @@ Directories layer as follows:
- the unified backend protocol (fetch, HTTP, streaming interfaces…) — definitions and support, see the "Message protocol" sections below
- `packages/client/*`: packages provide client-side capability only; every package stays single-sided. Three kinds live here (the axes are owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md)):
- **Pure libraries** (`ui-slots`, `web-react`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the first three are seeded into the module table.
- - **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dshClient` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
- - **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
+ - **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dsh.client` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
+ - **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
- - `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
- - `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
+ - `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell API exported by `dsh-client-web`.
+ - `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh --profile headless` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
- A future Electron application reuses the same web client packages over an IPC fetch carrier.
```
@@ -40,7 +40,7 @@ apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
▼
packages/host/* packages/client/*
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
- runtime assembly / host entity dshClient plugins ×8 (node half = empty apply,
+ runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
webserver Web HTTP carriage client half = src/client/)
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
▼ │ (type-only + the client base class)
@@ -63,7 +63,7 @@ On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Nod
| Layer | Package | Responsibility | Key discipline |
|---|---|---|---|
| Front layer | `dsh-host-apiproxy` | TS/zod definitions (api/) + the fetch abstraction (fetch/: handler + client base class) | Keep it simple — every consumer needs it; importable from Node and browser alike; protocol content in the "Message protocol" sections below; clients must not bypass api through ctx |
-| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dshClient packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
+| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dsh.client packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
| Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it |
| Client libraries | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | Slot registry core / ctx↔React glue / pure React atoms | Zero cordis runtime dependency in components; seeded into the loader module table by the shell |
| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | Browser-side cordis plugin tree (wire consumer, core services, theme, i18n, layout, sidebar, conversation, trajectory) — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); the consumption face goes exclusively through ApiProxy |
@@ -79,7 +79,7 @@ Packages under `packages/host/*` and `packages/client/*` **must carry the direct
2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the application's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports.
-The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
+The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh --profile headless` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
## Message protocol
@@ -116,7 +116,7 @@ Domain interface signatures perceive only the narrow forms: `RpcRequest = { r
### RpcReceipt: the carrier receipt
-The HTTP response body of a `ClientResponse` is `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }` — a carrier-layer receipt, **not** an RpcMessage (a response has no response); late/duplicate answers get `not-pending`, and the logical convergence surface is the `*/resolved` frames.
+The HTTP response body of a `ClientResponse` is `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }` — a carrier-layer receipt, **not** an RpcMessage (a response has no response); late/duplicate answers get `not-pending`, and the logical convergence point is the `*/resolved` frames.
## The type system: signatures are the source of truth
@@ -215,7 +215,7 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
| Subclass | Package | doFetch | Purpose |
|---|---|---|---|
-| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh run` drives core directly |
+| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh --profile headless` drives core directly |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser client; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) |
| IPC bridge subclass (hypothetical example — no such shell exists) | an Electron shell | IPC serialization round trip | would swap only doFetch; contract and base class unchanged |
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
index 7d20c5a266..e506c62427 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
@@ -9,7 +9,7 @@ Status: implemented
## Problem
需要提供 UI 对接层,除已有 ACP(Agent Client Protocol)/stdio 基线外,还需要 Web(server)、Electron 等其他产品客户端。我们把它们统一称为 Client。希望具备以下能力:
-- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh run`(headless),一个进程两种模式(设计预留)
+- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh --profile headless`(headless),一个进程两种模式(设计预留)
- 在 Electron 中使用与 `dsh web` 相同的 Web 技术启动
那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client。
@@ -25,11 +25,11 @@ Status: implemented
- 统一后端协议(fetch、HTTP、流式接口等)定义和支持,见本篇「消息协议」起各节
- `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有):
- **纯库**(`ui-slots`、`web-react`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。
- - **静态到达 entry 包**(`connection`、`runtime`、`ui-theme`、`i18n`、`hmr`):无 `dshClient` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
- - **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
+ - **静态到达 entry 包**(`connection`、`runtime`、`ui-theme`、`i18n`、`hmr`):无 `dsh.client` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
+ - **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dsh.client` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
- `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。
- - `apps/web`(`dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`。
- - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh run` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
+ - `apps/web`(`dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳 API 之上的一层薄 `main.ts`。
+ - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh --profile headless` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
- 将来的 Electron 应用经由 IPC fetch 载体复用同一套 web client 包。
```
@@ -38,7 +38,7 @@ apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
▼
packages/host/* packages/client/*
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
- runtime assembly / host entity dshClient plugins ×8 (node half = empty apply,
+ runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
webserver Web HTTP carriage client half = src/client/)
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
▼ │ (type-only + the client base class)
@@ -61,7 +61,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
| 层 | 包 | 职责 | 关键纪律 |
|---|---|---|---|
| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/:handler + 客户端基类) | 做简单、所有接入方都要;Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api |
-| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dshClient 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
+| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dsh.client 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
| 承载层 | `dsh-host-webserver` | Web HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 |
| client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 |
| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费者、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构笔记 | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy |
@@ -77,7 +77,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
2. **在 `apps/` 下写拼装模块**:`startHost()` + 客户端子类 + 该应用私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。
3. **需要 HTTP 承载才 import `dsh-host-webserver`**,否则零端口。
-现有两个应用保持这一区分:Web 应用挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。
+现有两个应用保持这一区分:Web 应用挂载 Host、载体与浏览器组合,而 `dsh --profile headless` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。
## 消息协议
@@ -114,7 +114,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
### RpcReceipt:载体回执
-`ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessage(response 不再有 response);迟到/重复应答收 `not-pending`,逻辑收敛面是 `*/resolved` 帧。
+`ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessage(response 不再有 response);迟到/重复应答收 `not-pending`,逻辑收敛点是 `*/resolved` 帧。
## 类型体系:函数签名即事实源
@@ -213,7 +213,7 @@ export type ResponseValue =
| 子类 | 所在包 | doFetch | 用途 |
|---|---|---|---|
-| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh run` 直接驱动 core |
+| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh --profile headless` 直接驱动 core |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器客户端;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) |
| IPC 桥子类(假想示例——尚无此形态) | Electron 壳 | IPC 序列化往返 | 只需换 doFetch,约定/基类零改 |
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
index 269933c1a7..8714bf9cf5 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
@@ -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/architecture/2026-07-19-gui-web-client-architecture.md
-2026-07-19-gui-web-client-architecture.md: 82b2f85708c423748954644d4991e2d54d42874a
-2026-07-19-gui-web-client-architecture.zh.md: c37252d1db291cae11db2a615c9e4005ece717da
+2026-07-19-gui-web-client-architecture.md: 070b857f14007f429826ab83b17b5c8fbd3d3d0b
+2026-07-19-gui-web-client-architecture.zh.md: 1f5bafe1dff878b5ca5ffcbdb9ed8ca38a863c9f
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
index 82b2f85708..070b857f14 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
@@ -30,7 +30,7 @@ Both ends run cordis. The host is a cordis plugin tree; the browser runs a secon
## The client cordis tree and the loading chain
-The loading chain — the two package kinds (plain vs dshClient plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dshClient` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); plugin CSS is inlined in the bundle and injected as `
-
\ No newline at end of file
+
diff --git a/apps/web/tests/agent-preset-authoring.e2e.ts b/apps/web/tests/agent-preset-authoring.e2e.ts
index e8ff5aa538..1a27f96c6e 100644
--- a/apps/web/tests/agent-preset-authoring.e2e.ts
+++ b/apps/web/tests/agent-preset-authoring.e2e.ts
@@ -161,7 +161,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8'))
const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8')
expect(metadata).toContain('name: 我的模式')
- expect(metadata).toContain('description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。')
+ expect(metadata).toContain('description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。')
expect(metadata).not.toContain('order:')
}, 60_000)
@@ -176,8 +176,10 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
await expect.poll(async () => dialog.getByText('我的模式').count(), { timeout: 10_000 }).toBe(0)
expect(existsSync(join(userRoot, 'my-agent'))).toBe(false)
- // Custom group gone with its only member; the shipped set stands.
- expect(await dialog.getByRole('heading', { name: '自定义' }).count()).toBe(0)
+ // The custom group outlives its only member: the heading stays with the
+ // creator entry so the place to author a preset never disappears.
+ expect(await dialog.getByRole('heading', { name: '自定义' }).count()).toBe(1)
+ expect(await dialog.getByRole('button', { name: '用「创造模式」创作自定义预设' }).count()).toBe(1)
expect(await dialog.getByText('标准模式').count()).toBeGreaterThan(0)
}, 60_000)
@@ -194,18 +196,18 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
const dialog = settingsDialog()
await dialog.getByRole('button', { name: '通用设置' }).click()
await dialog.getByRole('button', { name: 'Agent 预设' }).click()
- await dialog.getByText('已损坏').first().waitFor({ timeout: 10_000 })
+ await dialog.getByText('加载失败').first().waitFor({ timeout: 10_000 })
const snapshot = withPresetRoot(
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd))
await compareOrRefreshGolden(DAMAGED_EXPECTED, snapshot, MODE)
// Both damage shapes surface as marked, unselectable, uncopyable cards
// that still carry their metadata and the discovery-reported reason.
- expect(snapshot).toContain('已损坏: broken-yaml')
- expect(snapshot).toContain('已损坏: 幽灵预设')
+ expect(snapshot).toContain('加载失败: broken-yaml')
+ expect(snapshot).toContain('加载失败: 幽灵预设')
expect(snapshot).toContain('not valid YAML')
expect(snapshot).toContain('agent.cordis.yml is missing')
- expect(await dialog.getByRole('button', { name: '已损坏: broken-yaml' }).isDisabled()).toBe(true)
+ expect(await dialog.getByRole('button', { name: '加载失败: broken-yaml' }).isDisabled()).toBe(true)
expect(await dialog.getByRole('button', { name: '复制: 幽灵预设' }).isDisabled()).toBe(true)
// A broken card offers no "set default" affordance at all — the aria name
// IS the broken marking, so the picking name must not exist.
diff --git a/apps/web/tests/background-task-list.e2e.ts b/apps/web/tests/background-task-list.e2e.ts
new file mode 100644
index 0000000000..734d43131a
--- /dev/null
+++ b/apps/web/tests/background-task-list.e2e.ts
@@ -0,0 +1,133 @@
+// Web e2e scenario: the session-header background-task list over the real
+// host. No model call is involved — a genuine `run_in_background` bash call
+// registers with `ctx.tasks`, and the assertion chain is the whole delivery
+// path: registry change feed → api-proxy `session/tasks` frame → the client's
+// `tasksBySession` mirror → the header action.
+import { readFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import type { Agent } from '@deepseek-ai/dsh-agent'
+import { CallId } from '@deepseek-ai/dsh-llm'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import { TaskId } from '@deepseek-ai/dsh-tasks'
+import {
+ assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
+ launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { newEnglishPage, saveFailureShot } from './support.ts'
+
+const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url))
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/background-task-list', import.meta.url))
+const RUNNING_EXPECTED = join(SNAPSHOT_DIR, 'running.expected.md')
+const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
+const MODE = webSnapshotMode()
+const SEED_ID = 'background-task-list-web-e2e'
+// Long enough that the running assertions never race the process exiting on
+// their own; the test kills it explicitly to reach the settled state.
+const COMMAND = 'sleep 45'
+
+/**
+ * Wait for the Host to publish the live Agent that opening a session resumes.
+ * @param scaffold - the booted web scaffold.
+ * @param sessionId - the opened session's identity.
+ * @returns the registered Agent instance.
+ */
+async function liveAgent(scaffold: WebScaffold, sessionId: SessionId): Promise {
+ const deadline = Date.now() + 30_000
+ for (;;) {
+ const found = scaffold.ctx.agents.get(sessionId)
+ if (found !== undefined) return found
+ if (Date.now() > deadline) throw new Error(`opening session "${sessionId}" published no live Agent`)
+ await new Promise(resolve => setTimeout(resolve, 100))
+ }
+}
+
+describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+ let agent: Agent
+ let taskId: TaskId
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({})
+ await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), SEED_ID)
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+
+ const groupRow = page.locator('[role="treeitem"]').first()
+ await groupRow.waitFor({ timeout: 15_000 })
+ await groupRow.click()
+ const sessionRow = page.locator('[role="treeitem"]').nth(1)
+ await sessionRow.waitFor({ timeout: 10_000 })
+ await sessionRow.click()
+
+ // Opening the session drives the Host's ordinary Agent resolution; the
+ // task owner must be that exact live instance, never a second one.
+ // `expect.poll` is test-scoped, so this hook polls by hand.
+ agent = await liveAgent(scaffold, SessionId(SEED_ID))
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('shows a running background task in the session header without a refresh', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-running'))
+ // Point assertion, not a poll: `expect.poll` retries until a predicate
+ // holds, so polling for zero passes at t=0 and proves nothing. The
+ // "renders nothing without a task" branch is owned by the component suite.
+ const trigger = page.getByRole('button', { name: '1 background task running' })
+ expect(await trigger.count()).toBe(0)
+
+ const started = await scaffold.ctx.tools.execute({
+ signal: new AbortController().signal,
+ callId: CallId('background-task-list-e2e'),
+ name: 'bash',
+ arguments: { command: COMMAND, description: 'Hold a background slot open', run_in_background: true },
+ agent,
+ })
+ const reported = started.content.map(block => block.type === 'text' ? block.text : '').join('')
+ const matched = /\bbash-\d+\b/.exec(reported)
+ if (matched === null) throw new Error(`background bash reported no task id: ${reported}`)
+ taskId = TaskId(matched[0])
+
+ await trigger.waitFor({ timeout: 15_000 })
+ await trigger.click()
+ const row = page.getByRole('list', { name: 'Background tasks' }).getByRole('listitem').first()
+ await row.waitFor({ timeout: 10_000 })
+ await expect.poll(() => row.textContent()).toContain(COMMAND)
+
+ const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(RUNNING_EXPECTED, snapshot, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it('flips the open list to the cancelled outcome when the registry settles it', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-settled'))
+ expect(scaffold.ctx.tasks.kill(taskId, agent, 'web e2e cancellation')).toBe('requested')
+
+ // The trigger drops its live count once the task leaves running/stopping,
+ // which is also the proof that settlement reached the browser unprompted.
+ const idle = page.getByRole('button', { name: '1 background task' })
+ await idle.waitFor({ timeout: 20_000 })
+
+ const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(SETTLED_EXPECTED, snapshot, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it('keeps its snapshot inventory closed', async () => {
+ await assertFixtureInventory(SNAPSHOT_DIR, ['running.expected.md', 'settled.expected.md'])
+ })
+})
diff --git a/apps/web/tests/core-web-profile.snapshot.ts b/apps/web/tests/core-web-profile.snapshot.ts
deleted file mode 100644
index 1178390837..0000000000
--- a/apps/web/tests/core-web-profile.snapshot.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { writeFile } from 'node:fs/promises'
-import { join } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { afterAll, beforeAll, describe, expect, it } from 'vitest'
-import type { AgentHandle } from '@deepseek-ai/dsh-agent'
-import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
-import { SessionId } from '@deepseek-ai/dsh-session'
-import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
-
-const CORE_WEB_OVERLAY = fileURLToPath(new URL('../../cli/config/core-web.cordis.yml', import.meta.url))
-const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/core-web-profile', import.meta.url))
-const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
-const PROMPT = 'Reply exactly CORE_WEB_REQUEST_OK and stop.'
-
-describe('core Web profile', () => {
- let scaffold: WebScaffold
- let agentHandle: AgentHandle
-
- beforeAll(async () => {
- const systemPrompt = process.env.DSH_SYSTEM_PROMPT
- Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
- try {
- scaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
- } finally {
- if (systemPrompt !== undefined) process.env.DSH_SYSTEM_PROMPT = systemPrompt
- }
- agentHandle = await scaffold.ctx.agents.create({
- sessionId: SessionId('core-web-profile-smoke'),
- meta: { cwd: scaffold.workspaceCwd },
- agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
- })
- })
-
- afterAll(async () => {
- const failures: unknown[] = []
- await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
- await scaffold?.close().catch((error: unknown) => failures.push(error))
- if (failures.length === 1) throw failures[0]
- if (failures.length > 1) throw new AggregateError(failures, 'core Web profile smoke teardown failed')
- })
-
- it('sends the RL prompt and tool schemas through a real request, then executes both tools', async () => {
- agentHandle.agent.followup(createUserMessage({
- content: [{ type: 'text', text: PROMPT }],
- source: { kind: 'user' },
- }))
- await agentHandle.agent.whenIdle()
-
- const requestHeader = agentHandle.agent.session.requestHeader()
- if (requestHeader === undefined) throw new Error('the core Web agent issued no model request')
-
- const seedPath = join(scaffold.workspaceCwd, 'profile-smoke.txt')
- await writeFile(seedPath, 'CORE_WEB_EDITOR_OK\n')
- const signal = new AbortController().signal
- const bash = await scaffold.ctx.tools.execute({
- signal,
- callId: CallId('core-web-bash-smoke'),
- name: 'bash',
- arguments: { command: "printf 'CORE_WEB_BASH_OK\\n'" },
- agent: agentHandle.agent,
- })
- const editor = await scaffold.ctx.tools.execute({
- signal,
- callId: CallId('core-web-editor-smoke'),
- name: 'str_replace_editor',
- arguments: { command: 'view', path: seedPath },
- agent: agentHandle.agent,
- })
-
- const text = (result: typeof bash): string => result.content
- .filter(block => block.type === 'text')
- .map(block => block.text)
- .join('')
- .replaceAll(scaffold.workspaceCwd, '{{cwd}}')
- .trimEnd()
-
- expect({
- prompt: requestHeader.system,
- tools: requestHeader.tools?.map(tool => tool.name),
- bash: text(bash),
- editor: text(editor),
- }).toMatchInlineSnapshot(`
- {
- "bash": "CORE_WEB_BASH_OK",
- "editor": "Here's the content of {{cwd}}/profile-smoke.txt with line numbers (which has a total of 2 lines):
- 1 CORE_WEB_EDITOR_OK
- 2",
- "prompt": "You are a helpful software engineer assistant.",
- "tools": [
- "bash",
- "str_replace_editor",
- ],
- }
- `)
- expect(requestHeader.tools).toEqual(scaffold.ctx.tools.schemas(agentHandle.agent))
-
- const entries = [...scaffold.ctx.loader.entries()]
- expect(entries.find(entry => entry.options.id === 'persistent-bash')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'pty-local')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'str-replace-editor')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'web-runtime')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'workspace-context')?.fiber).toBeUndefined()
- await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
- })
-
- it('uses DSH_SYSTEM_PROMPT as the complete prompt when configured', async () => {
- const previous = process.env.DSH_SYSTEM_PROMPT
- process.env.DSH_SYSTEM_PROMPT = 'RL prompt override'
- let overrideScaffold: WebScaffold | undefined
- let overrideAgent: AgentHandle | undefined
- try {
- overrideScaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
- overrideAgent = await overrideScaffold.ctx.agents.create({
- sessionId: SessionId('core-web-profile-override'),
- meta: { cwd: overrideScaffold.workspaceCwd },
- agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
- })
- overrideAgent.agent.followup(createUserMessage({
- content: [{ type: 'text', text: PROMPT }],
- source: { kind: 'user' },
- }))
- await overrideAgent.agent.whenIdle()
- expect(overrideAgent.agent.session.requestHeader()?.system).toBe('RL prompt override')
- } finally {
- try {
- await overrideAgent?.dispose()
- } finally {
- try {
- await overrideScaffold?.close()
- } finally {
- if (previous === undefined) Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
- else process.env.DSH_SYSTEM_PROMPT = previous
- }
- }
- }
- })
-})
diff --git a/apps/web/tests/feedback-command.e2e.ts b/apps/web/tests/feedback-command.e2e.ts
new file mode 100644
index 0000000000..577e77a97a
--- /dev/null
+++ b/apps/web/tests/feedback-command.e2e.ts
@@ -0,0 +1,101 @@
+// Keyless assembled-browser coverage for the /feedback command over the
+// shipped Web bundles and the real host wire. The command plane settles
+// without a model turn: the host appends the log-only command/run +
+// feedback/record + command/done lifecycle, and the transcript renders the
+// acknowledgement — the recorded session id plus the session-sharing
+// disclosure — as a persistent command row. The scaffold mounts the shipped
+// telemetry row in FULL mode against a local dead endpoint (no record leaves
+// the process), so the golden pins the shipped default sentence
+// `Session sharing is enabled.`; the per-status sentences are pinned by the
+// package and OTel unit tests.
+import { readFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import {
+ assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
+ launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/feedback-command', import.meta.url))
+const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const ACK_EXPECTED = join(SNAPSHOT_DIR, 'ack.expected.md')
+const MODE = webSnapshotMode()
+// Discard port: loopback listener never binds, so FULL telemetry discloses
+// the shipped default policy without any record reaching a collector.
+const TELEMETRY_URL = 'http://127.0.0.1:9/v1/logs'
+
+const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
+
+describe('web e2e: /feedback command acknowledgement', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({
+ telemetryUrl: TELEMETRY_URL,
+ ...(MODE === 'record' ? {} : { replayFixture: FIXTURE }),
+ })
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ // Fresh world: connecting a workspace births the blank session whose
+ // live composer accepts the slash line.
+ await connectFreshWorkspace(page, scaffold.workspaceCwd)
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('drives the recorded prompt to a settled turn (all modes)', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-drive'))
+ if (MODE !== 'record') {
+ // Drift guard: the committed fixture must carry exactly the drive prompt.
+ expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
+ }
+ const input = page.locator('textarea').first()
+ await input.waitFor({ timeout: 10_000 })
+ // Arm the turn-boundary waiter BEFORE sending, so a burst replay cannot
+ // miss the turn/end that settles the recorded turn.
+ const settled = scaffold.whenTurnSettled()
+ await input.fill(PROMPT)
+ await input.press('Enter')
+ const sessionId = await settled
+ if (MODE === 'record') {
+ await recordFixture(scaffold, sessionId, FIXTURE)
+ }
+ }, 60_000)
+
+ it.skipIf(MODE === 'record')('records feedback and renders the acknowledgement with session id and sharing status', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-command'))
+ // The drive test settled the recorded turn: the transcript is active (a
+ // command row does not render while a fresh session is still blank) and
+ // the replayed reply is on screen.
+ await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
+ const input = page.locator('textarea').first()
+ await input.fill('/feedback the diff view is unreadable')
+ await input.press('Enter')
+ // The command plane settles without a model turn: the ack row names the
+ // recorded session and the mounted FULL backend's disclosure.
+ await page.getByText(/Feedback recorded for session/).waitFor({ timeout: 10_000 })
+ expect(await page.getByText(/Session sharing is enabled/).count()).toBe(1)
+ const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(ACK_EXPECTED, snapshot, MODE)
+
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
+ await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ack.expected.md'])
+ })
+})
diff --git a/apps/web/tests/hmr-live.e2e.ts b/apps/web/tests/hmr-live.e2e.ts
index cafd0fb474..57a3867187 100644
--- a/apps/web/tests/hmr-live.e2e.ts
+++ b/apps/web/tests/hmr-live.e2e.ts
@@ -6,8 +6,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { chromium } from 'playwright'
import { expect, it } from 'vitest'
-import { Context } from 'cordis'
-import type { Fiber } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
+import type { Fiber } from '@deepseek-ai/cordis'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { REPO_ROOT } from './support.ts'
diff --git a/apps/web/tests/math-rendering.e2e.ts b/apps/web/tests/math-rendering.e2e.ts
index b183fe7df5..de24c1ca76 100644
--- a/apps/web/tests/math-rendering.e2e.ts
+++ b/apps/web/tests/math-rendering.e2e.ts
@@ -119,6 +119,10 @@ describe('web e2e: settled Markdown math rendering', () => {
await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(6)
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2)
expect(await page.locator('.katex-error').count()).toBe(0)
+ await expect.poll(
+ () => page.getByText('Input 0 tok · Output 0 tok', { exact: false }).count(),
+ { timeout: 10_000 },
+ ).toBe(1)
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts
index 7555be866b..4284d3d71a 100644
--- a/apps/web/tests/message-actions.e2e.ts
+++ b/apps/web/tests/message-actions.e2e.ts
@@ -128,6 +128,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-actions-aria'))
await page.getByRole('button', { name: /^Select model, current/ })
.waitFor({ timeout: 10_000 })
+ await page.getByText(/Cache hit \d+%/u).first().waitFor({ timeout: 10_000 })
// Keep a footer focused so opacity-hidden actions stay in the a11y tree
// as an active/focused control during the capture.
await page.getByRole('button', { name: 'Copy' }).first().focus()
diff --git a/apps/web/tests/message-feedback-protocol.snapshot.ts b/apps/web/tests/message-feedback-protocol.snapshot.ts
new file mode 100644
index 0000000000..9fbc556f77
--- /dev/null
+++ b/apps/web/tests/message-feedback-protocol.snapshot.ts
@@ -0,0 +1,115 @@
+import { readFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import {
+ assertFixtureInventory,
+ compareOrRefreshGolden,
+ launchWebScaffold,
+ seedSession,
+ type WebScaffold,
+} from './scaffold.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-feedback-protocol', import.meta.url))
+const SESSION_FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const PROTOCOL_EXPECTED = join(SNAPSHOT_DIR, 'protocol.expected.json')
+const SESSION_ID = 'message-feedback-protocol'
+const MESSAGE_ID = '11111111-1111-4111-8111-111111111111'
+
+interface ProtocolExchange {
+ readonly endpoint: string
+ readonly request: unknown
+ readonly status: number
+ readonly response: unknown
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null
+}
+
+/** Extract the opaque item version while keeping every surrounding wire field snapshot-owned. */
+function createdVersion(response: unknown): string {
+ if (!isRecord(response) || !isRecord(response.result) || response.result.ok !== true
+ || !isRecord(response.result.value) || response.result.value.ok !== true
+ || !isRecord(response.result.value.value)
+ || typeof response.result.value.value.version !== 'string') {
+ throw new Error('messageFeedback.put did not return a successful versioned item')
+ }
+ return response.result.value.value.version
+}
+
+/** Replace only run-owned UUID/time values; all protocol names and business fields stay exact. */
+function normalizeProtocol(exchanges: readonly ProtocolExchange[], version: string): string {
+ return JSON.stringify(exchanges, (key, value: unknown) => {
+ if ((key === 'version' || key === 'ifVersion') && value === version) return '{{version}}'
+ if ((key === 'createdAt' || key === 'updatedAt') && typeof value === 'number') return '{{timestamp}}'
+ return value
+ }, 2)
+}
+
+describe('message feedback Host Remote protocol', () => {
+ let scaffold: WebScaffold
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold()
+ await seedSession(scaffold, await readFile(SESSION_FIXTURE, 'utf8'), SESSION_ID)
+ })
+
+ afterAll(async () => {
+ await scaffold?.close()
+ })
+
+ it('snapshots strict list, put, conflict, and delete calls through the shipped Web Host', async () => {
+ const exchanges: ProtocolExchange[] = []
+ const invoke = async (rpcId: string, endpoint: string, request: unknown): Promise => {
+ const payload = { args: { request } }
+ const response = await fetch(`${scaffold.baseUrl}/api/${endpoint}`, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ type: 'client-request',
+ rpcId,
+ method: endpoint,
+ payload,
+ }),
+ })
+ const body: unknown = await response.json()
+ exchanges.push({ endpoint: `/api/${endpoint}`, request: payload, status: response.status, response: body })
+ return body
+ }
+
+ await invoke('feedback-invalid', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'invalid-rating',
+ ifVersion: null,
+ })
+ await invoke('feedback-list-empty', 'messageFeedback/list', { sessionId: SESSION_ID })
+ const created = await invoke('feedback-put', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'positive',
+ note: 'Useful answer',
+ ifVersion: null,
+ })
+ const version = createdVersion(created)
+ expect(version).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
+ await invoke('feedback-list-created', 'messageFeedback/list', { sessionId: SESSION_ID })
+ await invoke('feedback-conflict', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'negative',
+ ifVersion: null,
+ })
+ await invoke('feedback-delete', 'messageFeedback/delete', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ ifVersion: version,
+ })
+ await invoke('feedback-list-deleted', 'messageFeedback/list', { sessionId: SESSION_ID })
+
+ expect(exchanges.every(exchange => exchange.status === 200)).toBe(true)
+ await compareOrRefreshGolden(PROTOCOL_EXPECTED, normalizeProtocol(exchanges, version), scaffold.mode)
+ await assertFixtureInventory(SNAPSHOT_DIR, ['protocol.expected.json', 'session.jsonl'])
+ })
+})
diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts
new file mode 100644
index 0000000000..0c8c6fa765
--- /dev/null
+++ b/apps/web/tests/minimal-preset.snapshot.ts
@@ -0,0 +1,115 @@
+import { mkdir, writeFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import type { AgentHandle } from '@deepseek-ai/dsh-agent'
+import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import type {} from '@deepseek-ai/dsh-agent-presets'
+import type {} from '@deepseek-ai/dsh-system-prompt'
+import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/minimal-preset', import.meta.url))
+const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const PROMPT = 'Reply exactly MINIMAL_PRESET_REQUEST_OK and stop.'
+
+describe('minimal agent preset', () => {
+ let scaffold: WebScaffold
+ let agentHandle: AgentHandle
+ let disposeInjectedPrompt: () => void
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({ replayFixture: FIXTURE })
+ disposeInjectedPrompt = scaffold.ctx.systemPrompt.section({
+ name: 'test:injected-prompt',
+ order: 999,
+ text: 'THIS TEXT MUST NOT REACH THE MODEL.',
+ })
+ agentHandle = await scaffold.ctx.agents.create({
+ sessionId: SessionId('minimal-preset-smoke'),
+ meta: { cwd: scaffold.workspaceCwd, agentPreset: 'minimal' },
+ agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
+ setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
+ })
+ })
+
+ afterAll(async () => {
+ const failures: unknown[] = []
+ await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
+ try {
+ disposeInjectedPrompt?.()
+ } catch (error: unknown) {
+ failures.push(error)
+ }
+ await scaffold?.close().catch((error: unknown) => failures.push(error))
+ if (failures.length === 1) throw failures[0]
+ if (failures.length > 1) throw new AggregateError(failures, 'minimal preset smoke teardown failed')
+ })
+
+ it('sends the exact RL prompt and schemas, then executes the persistent shell and editor', async () => {
+ agentHandle.agent.followup(createUserMessage({
+ content: [{ type: 'text', text: PROMPT }],
+ source: { kind: 'user' },
+ }))
+ await agentHandle.agent.whenIdle()
+
+ const requestHeader = agentHandle.agent.session.requestHeader()
+ if (requestHeader === undefined) throw new Error('the minimal agent issued no model request')
+
+ const stateDir = join(scaffold.workspaceCwd, 'persistent-state')
+ await mkdir(stateDir)
+ const signal = new AbortController().signal
+ await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-bash-state-setup'),
+ name: 'bash',
+ arguments: { command: `cd ${JSON.stringify(stateDir)} && export DSH_MINIMAL_STATE=PERSISTED` },
+ agent: agentHandle.agent,
+ })
+ const bash = await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-bash-state-read'),
+ name: 'bash',
+ arguments: { command: 'printf \'%s:%s\n\' "$DSH_MINIMAL_STATE" "$PWD"' },
+ agent: agentHandle.agent,
+ })
+ const seedPath = join(scaffold.workspaceCwd, 'preset-smoke.txt')
+ await writeFile(seedPath, 'MINIMAL_EDITOR_OK\n')
+ const editor = await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-editor-smoke'),
+ name: 'str_replace_editor',
+ arguments: { command: 'view', path: seedPath },
+ agent: agentHandle.agent,
+ })
+
+ const text = (result: typeof bash): string => result.content
+ .filter(block => block.type === 'text')
+ .map(block => block.text)
+ .join('')
+ .replaceAll(scaffold.workspaceCwd, '{{cwd}}')
+ .trimEnd()
+
+ expect({
+ prompt: requestHeader.system,
+ tools: requestHeader.tools?.map(tool => tool.name),
+ bash: text(bash),
+ editor: text(editor),
+ }).toMatchInlineSnapshot(`
+ {
+ "bash": "PERSISTED:{{cwd}}/persistent-state",
+ "editor": "Here's the content of {{cwd}}/preset-smoke.txt with line numbers (which has a total of 2 lines):
+ 1 MINIMAL_EDITOR_OK
+ 2",
+ "prompt": "You are a helpful software engineer assistant.",
+ "tools": [
+ "bash",
+ "str_replace_editor",
+ ],
+ }
+ `)
+ expect(requestHeader.tools?.toSorted((left, right) => left.name.localeCompare(right.name)))
+ .toEqual(scaffold.ctx.tools.schemas(agentHandle.agent).toSorted((left, right) => left.name.localeCompare(right.name)))
+ await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
+ })
+})
diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts
index 364b00f9e5..e07426cce1 100644
--- a/apps/web/tests/models-settings.e2e.ts
+++ b/apps/web/tests/models-settings.e2e.ts
@@ -4,8 +4,9 @@
// stores it write-only under the derived reference (`MINIMAX_CN_API_KEY`)
// while the settings document records only that reference. Each saved row
// appears after route topology invalidation without presenting liveness as
-// provider status. The customized-settings fold writes the curated
-// reasoning field as a merge patch. Zero model calls: configuration is pure
+// provider status. The customized-settings fold writes its curated fields —
+// the endpoint, and a declared route's own name and protocol — as merge
+// patches against the stored profile. Zero model calls: configuration is pure
// settings/credentials/llm-domain traffic, so there is no fixture and a
// stray stream would fail loud because the adapter registry is empty. The provider under test is
// minimax-cn so a developer's real ANTHROPIC/OPENAI environment keys can
@@ -28,6 +29,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/models-settings', import
const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md')
const DECLARED_EXPECTED = join(SNAPSHOT_DIR, 'declared.expected.md')
+const DECLARED_EDIT_EXPECTED = join(SNAPSHOT_DIR, 'declared-edit.expected.md')
const NATIVE_DELETE_EXPECTED = join(SNAPSHOT_DIR, 'native-delete.expected.md')
const DELETE_EXPECTED = join(SNAPSHOT_DIR, 'delete.expected.md')
const MODE = webSnapshotMode()
@@ -209,6 +211,40 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
+ it('reopens the name and protocol a declared route was created with', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-models-declared-identity'))
+ const dialog = page.getByRole('dialog', { name: '设置' })
+ await dialog.getByRole('button', { name: '编辑 Acme Gateway (acme-gateway)' }).click()
+ await dialog.getByText('自定义设置').click()
+ // The create card asked this route for a name and a protocol because
+ // nothing can default them; the editor reaches the same two fields rather
+ // than sending the user to settings.yaml for what only this route names.
+ const protocol = dialog.getByLabel('API 协议')
+ await protocol.waitFor({ timeout: 10_000 })
+ expect(await protocol.inputValue()).toBe('openai-completions')
+ const name = dialog.getByLabel('显示名称', { exact: true })
+ expect(await name.inputValue()).toBe('Acme Gateway')
+ const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(DECLARED_EDIT_EXPECTED, snapshot, MODE)
+
+ await protocol.selectOption('anthropic-messages')
+ await name.fill('Acme 网关')
+ await dialog.getByRole('button', { name: '保存', exact: true }).click()
+ await expect.poll(async () => dialog.getByLabel('API 协议').count(), { timeout: 10_000 }).toBe(0)
+ // The adapter re-resolved the route under the new protocol and re-registered
+ // it under the new name: an unserviceable profile would have been refused
+ // at the write instead, and a rename that did not re-register would leave
+ // the old label on the row.
+ await dialog.getByText('Acme 网关', { exact: true }).first().waitFor({ timeout: 10_000 })
+ // The status line names the route as the refreshed directory reports it;
+ // the target captured when the card opened still carries the old name.
+ await dialog.getByText('已保存 Acme 网关 (acme-gateway)。', { exact: true }).waitFor({ timeout: 10_000 })
+ const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ expect(document).toContain('api: anthropic-messages')
+ expect(document).toContain('displayName: Acme 网关')
+ expect(tripwire.pageErrors).toEqual([])
+ }, 60_000)
+
it('confirms an identified provider deletion before removing its profile and key', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-delete'))
const settingsDialog = page.getByRole('dialog', { name: '设置' })
@@ -243,8 +279,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
- 'configured.expected.md', 'declared.expected.md', 'delete.expected.md',
- 'empty.expected.md', 'native-delete.expected.md',
+ 'configured.expected.md', 'declared-edit.expected.md', 'declared.expected.md',
+ 'delete.expected.md', 'empty.expected.md', 'native-delete.expected.md',
])
})
})
diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts
index b96a1fa393..c8b6455296 100644
--- a/apps/web/tests/navigation-panes.e2e.ts
+++ b/apps/web/tests/navigation-panes.e2e.ts
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page, Response } from 'playwright'
import { chromium } from 'playwright'
+import { strFromU8, unzipSync } from 'fflate'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed } from 'vitest'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -274,6 +275,23 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await details.getByRole('button', { name: 'Close details' }).click()
}, 60_000)
+ it.skipIf(MODE === 'record')('downloads the session-log ZIP from the trajectory toolbar', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-export'))
+ await ensureSeedOpen(page)
+ await page.getByRole('tab', { name: 'Trajectory' }).click()
+ const downloadPromise = page.waitForEvent('download', { timeout: 30_000 })
+ await page.getByRole('button', { name: 'Export session log' }).click()
+ const download = await downloadPromise
+ expect(download.suggestedFilename()).toMatch(/^dsh-session-.+\.zip$/)
+ // The real host streamed the ZIP; its root entry is the persisted log
+ // text verbatim (the assembled seam: real route, real persistence read).
+ const files = unzipSync(await readFile(await download.path()))
+ expect(Object.keys(files)).toEqual(['session.jsonl'])
+ const content = strFromU8(files['session.jsonl'] as Uint8Array)
+ expect(content.split('\n')[0]).toContain(SEED_ID)
+ expect(content).toContain('FIRST_DONE')
+ }, 60_000)
+
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))
await ensureSeedOpen(page)
diff --git a/apps/web/tests/pwa-manifest.e2e.ts b/apps/web/tests/pwa-manifest.e2e.ts
index 696e1c7797..fe97e42da9 100644
--- a/apps/web/tests/pwa-manifest.e2e.ts
+++ b/apps/web/tests/pwa-manifest.e2e.ts
@@ -25,3 +25,11 @@ it('ships install metadata with the built web application', async () => {
}],
})
})
+
+it('ships a favicon that switches to a light mark under dark color scheme', async () => {
+ const favicon = await readFile(join(DIST_ROOT, 'favicon.svg'), 'utf8')
+ // The light fill must live inside the dark-scheme media query, so the icon
+ // stays black in light mode and only turns white under a dark scheme.
+ expect(favicon).toMatch(/@media \(prefers-color-scheme: dark\)\s*{\s*path\s*{[^}]*fill:\s*#fff/i)
+ expect(favicon).toContain('fill="#000"')
+})
diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts
index 945775678f..772bd4ae91 100644
--- a/apps/web/tests/scaffold.ts
+++ b/apps/web/tests/scaffold.ts
@@ -29,13 +29,12 @@ import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { Page } from 'playwright'
import { expect } from 'vitest'
-import { Context } from 'cordis'
-import Loader from '@cordisjs/plugin-loader'
-import Include, { type PatchOptions } from '@cordisjs/plugin-include'
-import Group from '@cordisjs/plugin-group'
+import { Context } from '@deepseek-ai/cordis'
+import Loader from '@deepseek-ai/cordis-plugin-loader'
+import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import Group from '@deepseek-ai/cordis-plugin-group'
import { scrubRequestHeaders, stabilizeFixtureMessageIds } from '@deepseek-ai/dsh-acp-snapshot'
import {
- addHarnessSourceSection,
assertEntriesLoaded,
composeEntries,
healProfilesModuleFallback,
@@ -65,6 +64,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
// Empty type imports carry the httpServer/agents/sessionPersistence Context merges.
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-agent'
+import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { REPO_ROOT, requireDist } from './support.ts'
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
@@ -245,6 +245,13 @@ export interface LaunchOptions {
}
/** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */
welcomeNoticePending?: boolean
+ /**
+ * Mount the shipped telemetry row in FULL mode against this exporter URL
+ * instead of disabling it. Used to pin a real backend disclosure in
+ * assembled coverage; point the URL at a local dead endpoint so no record
+ * leaves the process.
+ */
+ telemetryUrl?: string
/**
* Browse through a trusted non-loopback hostname that the browser resolves
* to loopback (for example `*.localhost`). The test server stays bound to
@@ -334,6 +341,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise failures.push(cleanupError))
+ restoreSkillRootEnvironment()
if (failures.length > 1) throw new AggregateError(failures, 'web scaffold temp-root setup failed')
throw error
}
@@ -395,8 +403,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise {
+ throw new Error(`web e2e scaffold: the web app requested exit ${String(code)} with no arguments to reject`)
+ },
+ })
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
// `cordis:group` beside it, exactly as `boot()` registers it: a group row is
// how a preset gives one `isolate` realm to a provider and its consumers,
// and a preset resolving package names from its own directory cannot reach
- // `@cordisjs/plugin-group` by name.
+ // `@deepseek-ai/cordis-plugin-group` by name.
ctx.loader.builtins.group = Group
// The shipped CLI deliberately has no dependency on this opt-in package.
// Keep the Loader row real without broadening the product installation.
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
- if (surfaceContext) {
- ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, REPO_ROOT) })
- }
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(rootConfig).href, patches },
diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts
index a112933f9c..9ec978fb97 100644
--- a/apps/web/tests/seeded-history.e2e.ts
+++ b/apps/web/tests/seeded-history.e2e.ts
@@ -18,7 +18,6 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'
import { deriveEventMessage, SessionId } from '@deepseek-ai/dsh-session'
-import type {} from '@deepseek-ai/dsh-agent-presets'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { TokenMeterService } from '@deepseek-ai/dsh-token-meter'
import { join } from 'node:path'
@@ -195,21 +194,11 @@ describe('web e2e: seeded history renders through cold resume', () => {
if (MODE !== 'record') {
const raw = await readFile(SEED, 'utf8')
expect(fixtureUserPrompts(raw), 'seed fixture must carry exactly the drive prompt').toEqual([PROMPT])
- // The meter belongs to an agent's preset, not to the process — token
- // accounting is per session. It is used here as a pure pricing function
- // over fixture content, so a throwaway composition is enough to reach one.
- const priced = await scaffold.ctx.agents.create({
- sessionId: SessionId('seeded-history-pricing'),
- setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx).then(() => undefined),
- })
- let realizedWithCompaction: string
- try {
- const meter = scaffold.ctx.agentPresets.serviceFor(priced.agent, 'tokenMeter')
- if (meter === undefined) throw new Error('seeded-history requires the composed token meter')
- realizedWithCompaction = withCompaction(realizeSeedFixture(scaffold, raw, SEED_ID), meter)
- } finally {
- await priced.dispose()
- }
+ // The meter is host-plane — it takes no configuration and keys every
+ // fold by Session — so pricing fixture content needs no agent at all.
+ const meter = scaffold.ctx.get('tokenMeter')
+ if (meter === undefined) throw new Error('seeded-history requires the host token meter')
+ const realizedWithCompaction = withCompaction(realizeSeedFixture(scaffold, raw, SEED_ID), meter)
await seedSession(scaffold, realizedWithCompaction, SEED_ID)
}
browser = await chromium.launch()
@@ -468,9 +457,9 @@ describe('web e2e: seeded history renders through cold resume', () => {
if (done?.type !== 'command/done') throw new Error('feedback command did not settle')
const [sessionLine, userLine, extraLine] = done.data.text?.split('\n') ?? []
expect(sessionLine).toBe(`Feedback recorded for session ${SEED_ID}`)
- expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
+ expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\./i)
expect(extraLine).toBeUndefined()
- const userId = userLine?.slice('User: '.length)
+ const userId = userLine?.match(/^User: ([0-9a-f-]+)/i)?.[1]
if (userId === undefined) throw new Error('feedback command omitted the user id')
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts
index 974181450a..a3b387b660 100644
--- a/apps/web/tests/settings-chrome.e2e.ts
+++ b/apps/web/tests/settings-chrome.e2e.ts
@@ -152,6 +152,67 @@ describe('web e2e: settings modal and General preferences', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
+ it('uses the persisted dark preference while plugins are still loading', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-boot-theme'))
+ await page.emulateMedia({ colorScheme: 'light' })
+ await page.getByRole('button', { name: '设置', exact: true }).click()
+ const initialDialog = page.getByRole('dialog', { name: '设置' })
+ const darkCube = initialDialog.getByRole('button', { name: '深色' })
+ await darkCube.click()
+ await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ .toMatch(/ui-theme:\n\s+preference: dark/)
+ await page.keyboard.press('Escape')
+
+ // Hold real plugin bundles so the shell-owned loading page remains observable.
+ const pluginPattern = '**/plugins/**'
+ let releaseBundles = (): void => {}
+ const bundlesReleased = new Promise((resolve) => { releaseBundles = resolve })
+ await page.route(pluginPattern, async (route) => {
+ await bundlesReleased
+ await route.continue()
+ })
+
+ const warningStart = tripwire.warnings.length
+ let reload: ReturnType | undefined
+ try {
+ reload = page.reload({ waitUntil: 'domcontentloaded' })
+ const loading = page.getByText('Loading plugins…', { exact: true })
+ await loading.waitFor({ timeout: 10_000 })
+ const state = await loading.evaluate((element) => {
+ const boot = element.parentElement?.parentElement
+ if (boot === undefined || boot === null) throw new Error('loading hint is detached from the boot page')
+ return {
+ attr: document.body.hasAttribute('data-ds-dark-theme'),
+ background: getComputedStyle(boot).backgroundColor,
+ colorScheme: document.documentElement.style.colorScheme,
+ }
+ })
+ expect(state).toEqual({
+ attr: true,
+ background: 'rgb(21, 21, 23)',
+ colorScheme: 'dark',
+ })
+ } finally {
+ releaseBundles()
+ await reload
+ await page.unroute(pluginPattern)
+ }
+
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ acknowledgeReloadConnectionLoss(tripwire, warningStart)
+ await page.getByRole('button', { name: '设置', exact: true }).click()
+ const restoredDialog = page.getByRole('dialog', { name: '设置' })
+ const systemCube = restoredDialog.getByRole('button', { name: '跟随系统' })
+ await systemCube.click()
+ await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
+ await expect.poll(() => page.evaluate(() => document.body.hasAttribute('data-ds-dark-theme')), {
+ timeout: 5_000,
+ }).toBe(false)
+ await page.keyboard.press('Escape')
+ expect(tripwire.pageErrors).toEqual([])
+ }, 90_000)
+
it('flips the theme through the Appearance cubes and persists across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-appearance'))
interface ThemeState {
diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts
index faf01ede62..ee00353ef8 100644
--- a/apps/web/tests/shipped-composition.e2e.ts
+++ b/apps/web/tests/shipped-composition.e2e.ts
@@ -36,9 +36,9 @@ const EXPECTED_TOOLS = [
'list_agents',
'ralph',
'read',
+ 'read_image',
'send_message',
'skill',
- 'str_replace_editor',
'subagent',
'subagent_fork',
'task_kill',
@@ -145,7 +145,7 @@ it('lets a preset producer reach the background-task registry', async () => {
content: [{ type: 'text', text: 'started background task bash-1' }],
})
- // The control surface reads what the producer started: same registry, one
+ // The controller reads what the producer started: same registry, one
// owner. A per-preset registry would list nothing here even on success.
const listed = await ctx.tools.execute({
signal,
diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts
index b0acf074b4..24a6f269c3 100644
--- a/apps/web/tests/smoke-real.e2e.ts
+++ b/apps/web/tests/smoke-real.e2e.ts
@@ -478,17 +478,20 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
requireDist()
sessionsDir = mkdtempSync(join(tmpdir(), 'dsh-web-w5-'))
const port = await probeFreePort()
- // tsx boot mirrors demo:web — lib/ may be unbuilt in this worktree. Isolate
+ // tsx boot mirrors the runtime half of the root dsh script. Isolate
// the host-level Harness and shared-agent homes inside the temp world; tsx
// also needs the repo's loader and tsconfig paths pointed at explicitly.
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
child = spawn(
process.execPath,
[
- '--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', String(port),
+ '--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web',
+ // Launcher flags come first: the first token the launcher does not own
+ // starts the web app's own arguments.
// Pin the in-browser picker: the shipped `-auto` row would resolve to
// the native OS chooser on this bind, and no page can drive that.
'--patch', fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
+ '--port', String(port),
],
{
cwd: sessionsDir,
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
index 395e517aa1..0ddc4dd77b 100644
--- a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
@@ -40,7 +40,7 @@
- text: 复制
- listitem:
- 'button "设为默认: 极简模式"':
- - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: minimal
- 'button "查看: 极简模式"':
- img
@@ -62,7 +62,7 @@
- list:
- listitem:
- 'button "设为默认: 我的模式"':
- - text: 我的模式 自定义 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 我的模式 自定义 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: my-agent
- 'button "查看路径: 我的模式"':
- img
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
index b40b848b5e..f853cacf24 100644
--- a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
@@ -40,7 +40,7 @@
- text: 复制
- listitem:
- 'button "设为默认: 极简模式"':
- - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: minimal
- 'button "查看: 极简模式"':
- img
@@ -61,8 +61,8 @@
- heading "自定义" [level=3]
- list:
- listitem:
- - 'button "已损坏: broken-yaml" [disabled]':
- - text: broken-yaml 已损坏 自定义 暂无描述。
+ - 'button "加载失败: broken-yaml" [disabled]':
+ - text: broken-yaml 加载失败 自定义 暂无描述。
- alert: "the composition is not valid YAML: unexpected end of the stream within a flow collection (3:1)"
- code: broken-yaml
- 'button "查看路径: broken-yaml"':
@@ -70,13 +70,13 @@
- text: 查看路径
- 'button "复制: broken-yaml" [disabled]':
- img
- - text: 预设已损坏,无法复制
+ - text: 预设加载失败,不能复制
- 'button "删除: broken-yaml"':
- img
- text: 删除
- listitem:
- - 'button "已损坏: 幽灵预设" [disabled]':
- - text: 幽灵预设 已损坏 自定义 composition 已被手动删除。
+ - 'button "加载失败: 幽灵预设" [disabled]':
+ - text: 幽灵预设 加载失败 自定义 composition 已被手动删除。
- alert: the composition file agent.cordis.yml is missing — the directory still occupies the id; delete it or restore the file
- code: ghost
- 'button "查看路径: 幽灵预设"':
@@ -84,7 +84,7 @@
- text: 查看路径
- 'button "复制: 幽灵预设" [disabled]':
- img
- - text: 预设已损坏,无法复制
+ - text: 预设加载失败,不能复制
- 'button "删除: 幽灵预设"':
- img
- text: 删除
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
index dcbe72641c..ad2bf86389 100644
--- a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
@@ -40,7 +40,7 @@
- text: 复制
- listitem:
- 'button "设为默认: 极简模式"':
- - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: minimal
- 'button "查看: 极简模式"':
- img
@@ -58,6 +58,7 @@
- 'button "复制: 创造模式"':
- img
- text: 复制
+ - heading "自定义" [level=3]
- button "用「创造模式」创作自定义预设":
- img
- text: 用「创造模式」创作自定义预设
diff --git a/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md b/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
index 78ec056f56..7e0d1ae032 100644
--- a/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
@@ -3,5 +3,5 @@
- text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
- img
- menuitem "Code mode All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program."
- - menuitem "Minimal mode Two-tool coding agent with only bash and str_replace_editor, for benchmarks and minimal reproductions."
+ - menuitem "Minimal mode Two-tool coding agent with persistent bash and str_replace_editor."
- menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
diff --git a/apps/web/tests/snapshots/background-task-list/running.expected.md b/apps/web/tests/snapshots/background-task-list/running.expected.md
new file mode 100644
index 0000000000..1adaa965e2
--- /dev/null
+++ b/apps/web/tests/snapshots/background-task-list/running.expected.md
@@ -0,0 +1,2 @@
+- list "Background tasks":
+ - listitem: bash sleep 45 running {{duration}}
diff --git a/apps/web/tests/snapshots/background-task-list/settled.expected.md b/apps/web/tests/snapshots/background-task-list/settled.expected.md
new file mode 100644
index 0000000000..c97ff5026c
--- /dev/null
+++ b/apps/web/tests/snapshots/background-task-list/settled.expected.md
@@ -0,0 +1,2 @@
+- list "Background tasks":
+ - listitem: "bash sleep 45 signal: SIGTERM {{duration}}"
diff --git a/apps/web/tests/snapshots/feedback-command/ack.expected.md b/apps/web/tests/snapshots/feedback-command/ack.expected.md
new file mode 100644
index 0000000000..89d40acb3b
--- /dev/null
+++ b/apps/web/tests/snapshots/feedback-command/ack.expected.md
@@ -0,0 +1,39 @@
+- banner:
+ - navigation "Session hierarchy":
+ - button "Reply with the single word" [disabled]
+ - img
+ - text: Standard mode
+ - tablist:
+ - tab "Chat" [selected]
+ - tab "Trajectory"
+- text: Reply with the single word LIGHTHOUSE and stop. {{clock}}
+- button "Copy":
+ - img
+- button "Context injection @deepseek-ai/dsh-system-prompt":
+ - img
+ - img
+ - text: Context injection @deepseek-ai/dsh-system-prompt
+- button "Think The user wants me to reply with a single word. Let me comply.":
+ - img
+ - img
+ - text: Think The user wants me to reply with a single word. Let me comply.
+- paragraph: LIGHTHOUSE
+- button "Copy":
+ - img
+- button "Branch into a new conversation":
+ - img
+- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
+- 'button "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."':
+ - img
+ - img
+ - text: "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."
+- textbox "Message the agent"
+- button "Commands":
+ - img
+- 'button "Access mode, current: Workspace Write"': Workspace Write
+- button "Select model, current DeepSeek-V4-Flash":
+ - text: DeepSeek-V4-Flash
+ - img
+- button "6% of context used"
+- button "Send message" [disabled]
+- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok
diff --git a/apps/web/tests/snapshots/feedback-command/session.jsonl b/apps/web/tests/snapshots/feedback-command/session.jsonl
new file mode 100644
index 0000000000..d528f36c0e
--- /dev/null
+++ b/apps/web/tests/snapshots/feedback-command/session.jsonl
@@ -0,0 +1,17 @@
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785015039278,"cwd":"{{cwd}}/workspace"}
+{"type":"turn/start","seq":0,"time":1785015039291,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
+{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
+{"type":"session/title","seq":2,"time":1785015039294,"data":{"title":"Reply with the single word","messageSeqs":[1],"source":{"kind":"fallback"}}}
+{"type":"step/start","seq":3,"time":1785015039362,"data":{"turn":1,"step":1}}
+{"type":"request/header","seq":4,"time":1785015039363,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
+{"type":"assistant/chunk","seq":5,"time":1785015039930,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":6,"time0":1785015039930,"data":{"turn":1,"step":1,"index":0,"dt":[162,28,1,0,0,46,1,0,0,0,11,0,0,30],"texts":["The"," user"," wants"," me"," to"," reply"," with"," a"," single"," word","."," Let"," me"," comply","."]}}
+{"type":"assistant/chunk","seq":21,"time":1785015040209,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":22,"time0":1785015040209,"data":{"turn":1,"step":1,"index":1,"dt":[1,0,30,1],"texts":["L","IGH","TH","O","USE"]}}
+{"type":"assistant/chunk","seq":27,"time":1785015040241,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."}}}}
+{"type":"assistant/chunk","seq":28,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"LIGHTHOUSE"}}}}
+{"type":"assistant/chunk","seq":29,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}}}}
+{"type":"assistant/chunk","seq":30,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"surfaceOp":"append"}
+{"type":"step/end","seq":32,"time":1785015040246,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":33,"time":1785015040247,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json
new file mode 100644
index 0000000000..302e4876df
--- /dev/null
+++ b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json
@@ -0,0 +1,203 @@
+[
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "invalid-rating",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-invalid",
+ "result": {
+ "ok": false,
+ "error": {
+ "code": "internal",
+ "message": "typert gateway: messageFeedback/put: wire field \"request\" failed boundary validation",
+ "details": {}
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-empty",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": []
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-put",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-created",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": [
+ {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "negative",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-conflict",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": false,
+ "error": {
+ "code": "version-conflict",
+ "current": {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/delete",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "ifVersion": "{{version}}"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-delete",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "absent": true
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-deleted",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": []
+ }
+ }
+ }
+ }
+ }
+]
diff --git a/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl
new file mode 100644
index 0000000000..d970fb8cb4
--- /dev/null
+++ b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl
@@ -0,0 +1,7 @@
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1786406400000,"cwd":"{{cwd}}"}
+{"type":"turn/start","seq":0,"time":1786406400001,"data":{"turn":1}}
+{"type":"user/message","seq":1,"time":1786406400002,"data":{"role":"user","content":[{"type":"text","text":"Give one useful answer."}],"source":{"kind":"user"},"id":"22222222-2222-4222-8222-222222222222"},"surfaceOp":"append"}
+{"type":"step/start","seq":2,"time":1786406400003,"data":{"turn":1,"step":1}}
+{"type":"assistant/message","seq":3,"time":1786406400004,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"A useful answer."}],"source":{"kind":"model","provider":"fixture","model":"fixture"},"id":"11111111-1111-4111-8111-111111111111"},"usage":{"inputTokens":4,"outputTokens":4}},"surfaceOp":"append"}
+{"type":"step/end","seq":4,"time":1786406400005,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":5,"time":1786406400006,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/apps/web/tests/snapshots/core-web-profile/session.jsonl b/apps/web/tests/snapshots/minimal-preset/session.jsonl
similarity index 73%
rename from apps/web/tests/snapshots/core-web-profile/session.jsonl
rename to apps/web/tests/snapshots/minimal-preset/session.jsonl
index 04f0d62d15..49977be802 100644
--- a/apps/web/tests/snapshots/core-web-profile/session.jsonl
+++ b/apps/web/tests/snapshots/minimal-preset/session.jsonl
@@ -1,7 +1,7 @@
-{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}"}
-{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly CORE_WEB_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}","agentPreset":"minimal"}
+{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":1,"time":1785974400002,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CORE_WEB_REQUEST_OK"}}}
-{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORE_WEB_REQUEST_OK"}}}}
+{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"MINIMAL_PRESET_REQUEST_OK"}}}
+{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}}}
{"type":"assistant/chunk","seq":4,"time":1785974400005,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
{"type":"assistant/chunk","seq":5,"time":1785974400006,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
diff --git a/apps/web/tests/snapshots/models-settings/declared-edit.expected.md b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md
new file mode 100644
index 0000000000..e36c7ff2e8
--- /dev/null
+++ b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md
@@ -0,0 +1,65 @@
+- dialog "设置":
+ - navigation:
+ - text: 设置
+ - button "通用设置":
+ - img
+ - text: 通用设置
+ - button "模型":
+ - img
+ - text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
+ - button "打开配置文件"
+ - button "关闭":
+ - img
+ - text: 关闭
+ - heading "模型" [level=2]
+ - paragraph: 填入各提供方的 API 密钥即可使用其模型。
+ - list:
+ - listitem:
+ - text: minimax-cn
+ - img "API 密钥已配置"
+ - button "编辑 minimax-cn": 编辑
+ - button "删除 minimax-cn": 删除
+ - listitem:
+ - text: Acme Gateway 自定义
+ - button "编辑 Acme Gateway (acme-gateway)": 编辑
+ - button "删除 Acme Gateway (acme-gateway)": 删除
+ - text: Acme Gateway acme-gateway API 密钥
+ - textbox "API 密钥":
+ - /placeholder: 输入 API 密钥,或留空使用环境认证
+ - group:
+ - text: 自定义设置 显示名称
+ - textbox "显示名称":
+ - /placeholder: acme-gateway
+ - text: Acme Gateway
+ - text: API 地址
+ - textbox "API 地址":
+ - /placeholder: https://gateway.acme.example/v1
+ - text: https://gateway.acme.example/v1
+ - text: API 协议
+ - combobox "API 协议":
+ - option "openai-completions" [selected]
+ - option "openai-responses"
+ - option "anthropic-messages"
+ - region "模型目录":
+ - text: 模型目录 已自定义模型目录
+ - button "恢复默认模型"
+ - button "获取可用模型"
+ - textbox "模型 ID 1":
+ - /placeholder: 模型 ID
+ - text: acme-large
+ - textbox "显示名称 1":
+ - /placeholder: 显示名称
+ - button "容量 1"
+ - button "删除模型 1"
+ - button "添加模型"
+ - button "取消"
+ - button "保存"
+ - button "添加提供方":
+ - img
+ - text: 添加提供方
+ - button "添加自定义提供方":
+ - img
+ - text: 添加自定义提供方
diff --git a/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md b/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
index a9b5dbb982..3476255bab 100644
--- a/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
+++ b/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
@@ -2,6 +2,7 @@
- button "Use actual duration": Duration
- button "Collapse turns": Turns
- button "Collapse calls": Calls
+ - button "Export session log": Export
- img
- searchbox "Search trajectory"
- region "Trajectory timeline":
diff --git a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
index 87b763d37c..6928b95777 100644
--- a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
+++ b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
@@ -38,10 +38,10 @@
- text: Context injection AGENTS.md
- img
- text: permission preset read-only
-- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}" [expanded]':
+- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured." [expanded]':
- img
- - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}"
-- text: "Feedback recorded for session {{seededId}} User: {{uuid}}"
+ - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
+- text: "Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
- textbox "Message the agent"
- button "Commands":
- img
diff --git a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
index cf22f5b566..15ceebba6b 100644
--- a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
+++ b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
@@ -43,7 +43,7 @@
- textbox "Message the agent"
- button "Commands":
- img
-- 'button "Access mode, current: Workspace Write"': Workspace Write
+- 'button "Access mode, current: Custom"': Custom
- button "6% of context used"
- button "Send message" [disabled]
- text: 2 turns · 2 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.6K tok · Output 158 tok
diff --git a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
index 7fe41a532d..8f4554e47b 100644
--- a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
+++ b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
@@ -20,6 +20,6 @@
- textbox "Parent session offline; sending is unavailable but you can still stop the run" [disabled]
- button "Commands" [disabled]:
- img
-- 'button "Access mode, current: Workspace Write" [disabled]': Workspace Write
+- 'button "Access mode, current: Custom" [disabled]': Custom
- button "Stop generating"
- button "Send message" [disabled]
diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts
index fa33e5cd3d..6756b093d2 100644
--- a/apps/web/tests/subagent-conversation.e2e.ts
+++ b/apps/web/tests/subagent-conversation.e2e.ts
@@ -395,7 +395,8 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
expect([
Math.round(clickAreaBox!.x - treeBox!.x),
Math.round(treeBox!.x + treeBox!.width - clickAreaBox!.x - clickAreaBox!.width),
- ]).toEqual([5, 5])
+ // Menu padding alone insets the rows now that the border is gone.
+ ]).toEqual([4, 4])
await compareOrRefreshGolden(
BRANCHLESS_EXPECTED,
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd),
diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts
index 40b9be39ca..ee2a1a1a62 100644
--- a/apps/web/tests/support.ts
+++ b/apps/web/tests/support.ts
@@ -57,9 +57,9 @@ export function probeFreePort(): Promise {
/**
* Drive the hero's workspace picker through the composed directory dialog
* until the live composer unlocks. A fresh world has no Workspace, so the boot
- * lands in the locked view state (startup auto-selection has nothing to
+ * lands in the Workspace-trigger view state (startup auto-selection has nothing to
* select); every scenario that types into the composer must connect one
- * first. With nothing to list, the chip gesture raises the dialog directly —
+ * first. With nothing to list, activating the textarea raises the dialog directly —
* adding a workspace is the picker's only entry. The directory is staged here
* and adopted through the path editor, which is idempotent across the repeated
* connects a scenario may make; creating a folder from inside the dialog (the
@@ -73,7 +73,7 @@ export function probeFreePort(): Promise {
*/
export async function connectFreshWorkspace(page: Page, root: string, name = 'workspace'): Promise {
mkdirSync(join(root, name), { recursive: true })
- await page.getByRole('button', { name: 'Choose workspace' }).click()
+ await page.getByRole('textbox', { name: 'Choose workspace' }).click()
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Edit path' }).click()
@@ -97,7 +97,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
*/
export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace'): Promise {
mkdirSync(join(root, name), { recursive: true })
- await page.getByRole('button', { name: '选择工作区' }).click()
+ await page.getByRole('textbox', { name: '选择工作区' }).click()
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '编辑路径' }).click()
diff --git a/apps/web/tests/workspace-management.e2e.ts b/apps/web/tests/workspace-management.e2e.ts
index c18a522d30..a435592da5 100644
--- a/apps/web/tests/workspace-management.e2e.ts
+++ b/apps/web/tests/workspace-management.e2e.ts
@@ -99,6 +99,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
}
}
+ /**
+ * Reveal and click a row action, re-hovering if a projection update replaces
+ * the row before its hover-only button becomes visible.
+ */
+ async function clickHoverAction(row: Locator, name: string): Promise {
+ const button = row.getByRole('button', { name })
+ await expect.poll(async () => {
+ await row.hover()
+ return await button.isVisible()
+ }, { timeout: 10_000 }).toBe(true)
+ await button.click()
+ }
+
beforeAll(async () => {
scaffold = await launchWebScaffold({})
// Seed one cold session (Ungrouped bucket) for the flat view + hover card.
@@ -137,10 +150,8 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
it('renames a workspace over the wire with a duplicate-name pre-check', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-rename'))
- // The actions button is display:none until its row hovers — hover the
- // group row first, then the revealed button becomes actionable.
- await page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first().hover()
- await page.getByRole('button', { name: 'Workspace actions for alpha-ws' }).click()
+ const alphaRow = page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first()
+ await clickHoverAction(alphaRow, 'Workspace actions for alpha-ws')
await page.getByRole('menuitem', { name: 'Rename' }).click()
const dialog = page.getByRole('dialog', { name: 'Rename workspace' })
await dialog.waitFor({ timeout: 10_000 })
@@ -214,17 +225,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
// The header row is wrapped by its HoverCard anchor span, so the section
// is the nearest groupSection ancestor, not the immediate parent.
const groupSection = groupRow.locator('xpath=ancestor::*[contains(@class, "groupSection")][1]')
- if (await groupSection.locator('[role="treeitem"]').count() < 2) await groupRow.click()
- await expect.poll(
- () => groupSection.locator('[role="treeitem"]').count(),
- { timeout: 10_000 },
- ).toBeGreaterThanOrEqual(2)
+ await expect.poll(async () => {
+ const count = await groupSection.locator('[role="treeitem"]').count()
+ if (count < 2 && await groupRow.getAttribute('aria-expanded') !== 'true') {
+ await groupRow.click()
+ await page.waitForTimeout(50)
+ }
+ return await groupSection.locator('[role="treeitem"]').count()
+ }, { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
const seededRow = groupSection.locator('[role="treeitem"]').nth(1)
await seededRow.click()
await expect.poll(() => seededRow.getAttribute('aria-selected'), { timeout: 10_000 }).toBe('true')
- await groupRow.hover()
- await page.getByRole('button', { name: `Workspace actions for ${workspace.title}` }).click()
+ await clickHoverAction(groupRow, `Workspace actions for ${workspace.title}`)
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
const dialog = page.getByRole('dialog', { name: 'Delete workspace' })
await dialog.waitFor({ timeout: 10_000 })
@@ -338,8 +351,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
if (oldWorkspace === undefined) throw new Error('old same-name Workspace was not registered')
const oldRow = page.locator('[role="treeitem"]').filter({ hasText: title }).first()
- await oldRow.hover()
- await page.getByRole('button', { name: `Workspace actions for ${title}` }).click()
+ await clickHoverAction(oldRow, `Workspace actions for ${title}`)
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
await page.getByRole('dialog', { name: 'Delete workspace' })
.getByRole('button', { name: 'Delete workspace' }).click()
@@ -509,9 +521,10 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-row-menu'))
const sessionRow = await seededSessionRow()
// The trigger is display:none until its row hovers.
- await sessionRow.hover()
const trigger = sessionRow.locator('button[aria-label^="Session actions for "]')
- await trigger.click()
+ const triggerName = await trigger.getAttribute('aria-label')
+ if (triggerName === null) throw new Error('seeded Session row has no actions label')
+ await clickHoverAction(sessionRow, triggerName)
const item = page.getByRole('menuitem', { name: 'Rename' })
await item.waitFor({ timeout: 5_000 })
// Into the list, then back up to the trigger across the 4px gap below it:
@@ -559,8 +572,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
const rowTitle = await sessionRow.locator('[class*="title"]').innerText()
// Row menu: hover reveals the actions button; Archive session commits
// without a confirmation dialog (non-destructive: log + accounting stay).
- await sessionRow.hover()
- await sessionRow.getByRole('button', { name: `Session actions for ${rowTitle}` }).click()
+ await clickHoverAction(sessionRow, `Session actions for ${rowTitle}`)
await page.getByRole('menuitem', { name: 'Archive session' }).click()
// The row disappears on the archive-set echo; with no other visible
// stray, the whole Ungrouped bucket withdraws.
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 9fe5a0575d..5ca8bb884d 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -24,7 +24,8 @@
"exclude": [
"tests/scaffold.ts",
"tests/scaffold-hermetic.e2e.ts",
- "tests/core-web-profile.snapshot.ts",
+ "tests/minimal-preset.snapshot.ts",
+ "tests/message-feedback-protocol.snapshot.ts",
"tests/live-interactions.e2e.ts",
"tests/question-composer.e2e.ts",
"tests/approval-composer.e2e.ts",
@@ -64,6 +65,7 @@
"tests/agent-preset-selection.e2e.ts",
"tests/agent-preset-authoring.e2e.ts",
"tests/shipped-composition.e2e.ts",
+ "tests/feedback-command.e2e.ts",
"tests/startup-auto-selection.e2e.ts",
"tests/produced-files.e2e.ts",
"tests/produced-file-mentions.e2e.ts",
@@ -72,6 +74,7 @@
"tests/subagent-interrupt.e2e.ts",
"tests/subagent-interrupt-ui.e2e.ts",
"tests/sidebar-subagent-activity.e2e.ts",
+ "tests/background-task-list.e2e.ts",
"tests/bash-abort-row.e2e.ts",
"tests/skill-tool-row.e2e.ts",
"tests/turn-tail-actions.e2e.ts",
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index a8323cdd78..66076c1171 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -5,8 +5,8 @@ import react from '@vitejs/plugin-react'
const src = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url))
const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. '
- + 'Build with `pnpm run build && pnpm run build:web`, then run `dsh web` (repository checkout: `pnpm run dsh -- web`). '
- + 'For client-plugin HMR, run `pnpm run dsh -- web --dev` together with `pnpm run dev:web`.'
+ + 'From a repository checkout, run `pnpm dsh web`; an installed package uses `dsh web`. '
+ + 'For client-plugin HMR, run `pnpm dsh web --dev` together with `pnpm run dev:web`.'
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
function rejectStandaloneServe(): Plugin {
@@ -130,7 +130,7 @@ export default defineConfig({
// Workspace packages resolve to SOURCE: package.json exports point at lib
// for Node/type consumers, but the browser bundle must compile src directly
// so CSS rides vite's pipeline instead of the CSS-externalized lib bundle.
- // Only the shell's normal-package surface is aliased — plugin packages are
+ // Only the shell's normal package entry is aliased — plugin packages are
// NEVER bundled here (shell self-sufficiency — see
// packages/client/web/README.md); they arrive as runtime
// bundles through the client module system. Order matters — subpath
diff --git a/bin/dsh b/bin/dsh
deleted file mode 100755
index c578d78e74..0000000000
--- a/bin/dsh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/sh
-# dsh launcher: runs the apps/cli `dsh` bin FROM SOURCE through the tsx ESM
-# hook, so a symlink from anywhere (e.g. ~/.local/bin/dsh) always executes the
-# current working tree without a build step.
-set -eu
-
-# Resolve symlink chains without readlink -f (not on every macOS).
-script=$0
-while [ -L "$script" ]; do
- target=$(readlink "$script")
- case $target in
- /*) script=$target ;;
- *) script=$(dirname "$script")/$target ;;
- esac
-done
-root=$(CDPATH='' cd -- "$(dirname -- "$script")/.." && pwd)
-
-# The ESM-only tsx hook transforms TypeScript and projects this checkout's
-# tsconfig paths into Node resolution (the CJS hook stays off: the graph is
-# ESM-only and the CJS resolver costs ~0.4s of startup). Absolute paths keep
-# both the hook and the tsconfig anchored to this checkout when the launcher
-# runs from any cwd, where bare `tsx/esm` would not resolve.
-NODE_USE_ENV_PROXY=1 \
- TSX_TSCONFIG_PATH="$root/tsconfig.json" \
- exec node --import "$root/node_modules/tsx/dist/esm/index.mjs" \
- "$root/apps/cli/src/bin.ts" "$@"
diff --git a/docs/agent-lifecycle.i18n.yaml b/docs/agent-lifecycle.i18n.yaml
index 97dd2649f0..f9ac3b6f30 100644
--- a/docs/agent-lifecycle.i18n.yaml
+++ b/docs/agent-lifecycle.i18n.yaml
@@ -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 docs/agent-lifecycle.md
-agent-lifecycle.md: 4d89e591626454af72de40426e66248f9b6fa404
+agent-lifecycle.md: 99b7823a3824b695d227abb85c3c773749154376
agent-lifecycle.zh.md: a54547f486c447fe83fbe6017d016102d66b8612
diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md
index 4d89e59162..99b7823a38 100644
--- a/docs/agent-lifecycle.md
+++ b/docs/agent-lifecycle.md
@@ -77,6 +77,6 @@ The `assistant/message` event records every successful provider call, including
The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.
-SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request construction, steering, continuation, and errors.
+SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination API for queue/status, prompt interception, request construction, steering, continuation, and errors.
Maintenance mode: curated Mermaid sequence; exact event signatures live in the generated Cordis catalog.
diff --git a/docs/api-gateway.i18n.yaml b/docs/api-gateway.i18n.yaml
index 29bfed3f19..a958569942 100644
--- a/docs/api-gateway.i18n.yaml
+++ b/docs/api-gateway.i18n.yaml
@@ -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 docs/api-gateway.md
-api-gateway.md: 4b904f24e5755460b6c629e3d2fd43ffc1aefaaf
-api-gateway.zh.md: 1e434cbb99450d241d6fde7e570ae1fadf5c0209
+api-gateway.md: 3065f3f10861965b327a4412049878dc1ba7faec
+api-gateway.zh.md: aa9b726c33fd9f51fc0b2d2ed95c4c9658662796
diff --git a/docs/api-gateway.md b/docs/api-gateway.md
index 4b904f24e5..3065f3f108 100644
--- a/docs/api-gateway.md
+++ b/docs/api-gateway.md
@@ -17,7 +17,7 @@ Services normally extend `GatewayService` so the constructor explicitly binds th
```ts
import type { Agent } from '@deepseek-ai/dsh-agent'
import { GatewayService, Remote, RemoteScope } from '@deepseek-ai/dsh-type-meta'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export interface CreateGoalRequest {
objective: string
@@ -60,7 +60,7 @@ The Client uses concrete functions on ordinary objects, not a JavaScript Proxy.
```ts ignore-check
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { AgentContext } from '@deepseek-ai/dsh-client-runtime/client'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
export const inject = ['remote', 'remote.goals']
@@ -138,20 +138,14 @@ SRC solves only dispatch for a Host process running from source. The Client does
## Development mode
-A complete build generates Host contracts before compiling the Host, Client, and Web, so it is the deterministic entry for creating or refreshing all artifacts:
+The repository `dsh` script completes the Host, Client, and Web build before starting the source Host. Web development runs that command and the Client plugin watcher in separate terminals:
```sh
-pnpm run build
-```
-
-Web development normally starts the source Host after one complete build and runs the Client plugin watcher in another terminal:
-
-```sh
-pnpm run dsh -- web --dev
+pnpm dsh web --dev
pnpm run dev:web
```
-`dsh` starts the Host source through tsx, so the Host can use the SRC fallback; `dev:web` watches only Client plugins with a `dshClient` declaration and rewrites their `lib/client.js`. It does not analyze Host decorators or generate Remote Client DTS.
+`dsh` starts the Host source through tsx, so the Host can use the SRC fallback; `dev:web` watches only Client plugins with a `dsh.client` declaration and rewrites their `lib/client.js`. It does not analyze Host decorators or generate Remote Client DTS.
Changing only a Remote method's implementation body without changing its contract does not require regenerating the TypeRT files. After adding or removing a decorator or changing an export name, namespace, parameter, return value, lookup, Context, or cancellation signature, rerun the ordered lib build so the Host generates the strict contract before the Client compiles and bundles the new contribution:
diff --git a/docs/api-gateway.zh.md b/docs/api-gateway.zh.md
index 1e434cbb99..aa9b726c33 100644
--- a/docs/api-gateway.zh.md
+++ b/docs/api-gateway.zh.md
@@ -17,7 +17,7 @@ Service 通常继承 `GatewayService`,让 Cordis service key 与默认 Remote
```ts
import type { Agent } from '@deepseek-ai/dsh-agent'
import { GatewayService, Remote, RemoteScope } from '@deepseek-ai/dsh-type-meta'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export interface CreateGoalRequest {
objective: string
@@ -60,7 +60,7 @@ Client 使用普通对象上的具体函数,不使用 JavaScript Proxy。直
```ts ignore-check
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { AgentContext } from '@deepseek-ai/dsh-client-runtime/client'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
export const inject = ['remote', 'remote.goals']
@@ -138,20 +138,14 @@ SRC 只解决 Host 源码进程的分发问题。Client 不会从运行中的 Ho
## 开发模式
-完整构建会先生成 Host 约定,再编译 Host、Client 与 Web,因此是建立或刷新所有产物的确定性入口:
+仓库的 `dsh` 脚本会先完成 Host、Client 与 Web 构建,再启动源码 Host。Web 开发需要在两个终端中分别运行该命令和 Client plugin watcher:
```sh
-pnpm run build
-```
-
-Web 开发通常在完成一次构建后启动源码 Host,并在另一个终端运行 Client plugin watcher:
-
-```sh
-pnpm run dsh -- web --dev
+pnpm dsh web --dev
pnpm run dev:web
```
-`dsh` 通过 tsx 启动 Host 源码,所以 Host 可以使用 SRC 回退;`dev:web` 只监听带 `dshClient` 声明的 Client plugin 并重写其 `lib/client.js`,它不会分析 Host decorator,也不会生成 Remote Client DTS。
+`dsh` 通过 tsx 启动 Host 源码,所以 Host 可以使用 SRC 回退;`dev:web` 只监听带 `dsh.client` 声明的 Client plugin 并重写其 `lib/client.js`,它不会分析 Host decorator,也不会生成 Remote Client DTS。
只修改 Remote 方法实现体而不改变约定时,无需重新生成 TypeRT 文件。新增或删除 decorator、修改导出名、namespace、参数、返回值、lookup、Context 或取消签名时,重新执行有序 lib 构建,让 Host 先生成严格约定,再让 Client 编译并打包新的贡献:
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index 344bac8145..ea8b934c79 100644
--- a/docs/architecture.i18n.yaml
+++ b/docs/architecture.i18n.yaml
@@ -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 docs/architecture.md
-architecture.md: 8d1c5a1be391e2455aefc69db89d96027aaf3efa
-architecture.zh.md: 53bf9f54a5503ae40aa62a348822f9d92162dda2
+architecture.md: aeda7f9674e75a1e97549f25c13d571b3b37ee8c
+architecture.zh.md: a25f20ba9babefeaab4636027e97d6f2d4ae8caf
diff --git a/docs/architecture.md b/docs/architecture.md
index 8d1c5a1be3..aeda7f9674 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -42,6 +42,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services,
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry, generic `task_*` controls |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration |
| `ctx.goals` | [`goal/`](../packages/goal/README.md) | persisted same-session goals |
+| `ctx.messageFeedback` | [`feedback/`](../packages/feedback/README.md) | lifecycle-bound editable feedback for individual assistant messages and its Host Remote contract |
| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | durable session-log storage |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | live-preferred exact/filter/trace queries over SQLite FTS, workspace-authorized model tools |
| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | log-backed fallbacks, one optional asynchronous provider |
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index 53bf9f54a5..a25f20ba9b 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -42,6 +42,7 @@
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | 后台任务注册表和通用 `task_*` 控制 |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | 脚本驱动的多 agent 编排 |
| `ctx.goals` | [`goal/`](../packages/goal/README.md) | 持久化的同会话目标 |
+| `ctx.messageFeedback` | [`feedback/`](../packages/feedback/README.md) | 绑定生命周期的单条 assistant 消息可编辑反馈及其 Host Remote 契约 |
| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | 会话日志的持久化存储 |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | 基于 SQLite 全文搜索的实时优先精确检索/过滤/追踪、经工作区授权的模型工具 |
| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | 基于日志的回退标题和单个可选异步提供方 |
diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml
index c5cc60a014..6d2f07e5f8 100644
--- a/docs/capability-seams.i18n.yaml
+++ b/docs/capability-seams.i18n.yaml
@@ -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 docs/capability-seams.md
-capability-seams.md: 85aee35af0e3de4d7cdbb715bc60c832c023d5ac
-capability-seams.zh.md: d3664b649f36b1788c6633497c95d5731ad4a401
+capability-seams.md: d56b8964a69d98b840749ff98b1be3605d95ec20
+capability-seams.zh.md: 8d2cf846a64c254f5ff0ab617eda25be3031f970
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index 85aee35af0..d56b8964a6 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -30,6 +30,7 @@ flowchart LR
pkg_session_query_sqlite["session-query-sqlite"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_invariants["invariants"]
+ pkg_message_feedback["message-feedback"]
svc_invariants["ctx.invariants
Package-owned invariant registry"]
pkg_scope["scope"]
pkg_typert_registry["typert-registry"]
@@ -60,6 +61,7 @@ flowchart LR
pkg_storage_domain["storage-domain"]
svc_storageDomain["ctx.storageDomain
Domain data facility"]
pkg_workspace["workspace"]
+ svc_messageFeedback["ctx.messageFeedback
Lifecycle-bound message feedback"]
svc_workspace["ctx.workspace
Workspace entity registry"]
svc_sessionQuery["ctx.sessionQuery
Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
@@ -218,6 +220,7 @@ flowchart LR
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
+ pkg_message_feedback --> svc_messageFeedback
pkg_modules --> svc_clientModuleHost
pkg_permission --> svc_permission
pkg_plan_mode --> svc_planMode
@@ -322,6 +325,7 @@ flowchart LR
svc_sessionPersistence --> pkg_agent_loop
svc_sessionPersistence --> pkg_hooks_claude
svc_sessionPersistence --> pkg_hooks_codex
+ svc_sessionPersistence --> pkg_message_feedback
svc_sessionPersistence --> pkg_session_query
svc_sessionPersistence --> pkg_session_query_sqlite
svc_sessionPersistence --> pkg_tool_bash
@@ -334,6 +338,7 @@ flowchart LR
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_invariants
+ svc_sessions --> pkg_message_feedback
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query
svc_sessions --> pkg_session_query_sqlite
@@ -344,6 +349,7 @@ flowchart LR
svc_skills --> pkg_tool_skill
svc_spillStore --> pkg_spill_policy
svc_storage --> pkg_storage_domain
+ svc_storageDomain --> pkg_message_feedback
svc_storageDomain --> pkg_workspace
svc_subagents --> pkg_tool_ralph
svc_subagents --> pkg_tool_subagent
@@ -392,16 +398,17 @@ flowchart LR
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
-| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
+| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
-| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
+| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer. |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage. |
| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. |
| `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json), [`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives. |
-| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. |
+| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace), [`message-feedback`](../packages/feedback/message-feedback) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. |
+| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry. |
| `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. |
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
@@ -423,7 +430,7 @@ flowchart LR
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`pty-local`](../packages/pty/pty-local), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`pwsh-local`](../packages/bash/pwsh-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. |
| `ctx.bashEnv` | `core` | [`bash-env`](../packages/bash/bash-env) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh) | - | Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace. |
-| `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface. |
+| `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model tools. |
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox), [`pty-local`](../packages/pty/pty-local) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
| `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/bash/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`pty-local`](../packages/pty/pty-local) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. |
| `ctx.approval` | `seam` | `approval` | [`acp`](../packages/acp/acp) | [`tools`](../packages/core/tools), [`tool-bash`](../packages/bash/tool-bash) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
@@ -432,12 +439,12 @@ flowchart LR
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. |
| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
-| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry. |
+| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing controller that reads, lists, and kills it; tasks-local is the process-local registry. |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
| `ctx.directoryPicker` | `seam` | `directory-picker` | `directory-picker-native`, `directory-picker-browse` | `apiproxy` | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). |
| `ctx.httpServer` | `core` | `webserver` | - | `connection`, `modules`, `hmr` | - | Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes. |
-| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | Composes the __DSH_BOOT__ entry graph from an incremental dshClient scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. |
+| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. |
| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. |
Maintenance mode: hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard.
diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md
index d3664b649f..8d2cf846a6 100644
--- a/docs/capability-seams.zh.md
+++ b/docs/capability-seams.zh.md
@@ -32,6 +32,7 @@ flowchart LR
pkg_session_query_sqlite["session-query-sqlite"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_invariants["invariants"]
+ pkg_message_feedback["message-feedback"]
svc_invariants["ctx.invariants
Package-owned invariant registry"]
pkg_scope["scope"]
pkg_typert_registry["typert-registry"]
@@ -62,6 +63,7 @@ flowchart LR
pkg_storage_domain["storage-domain"]
svc_storageDomain["ctx.storageDomain
Domain data facility"]
pkg_workspace["workspace"]
+ svc_messageFeedback["ctx.messageFeedback
Lifecycle-bound message feedback"]
svc_workspace["ctx.workspace
Workspace entity registry"]
svc_sessionQuery["ctx.sessionQuery
Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
@@ -220,6 +222,7 @@ flowchart LR
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
+ pkg_message_feedback --> svc_messageFeedback
pkg_modules --> svc_clientModuleHost
pkg_permission --> svc_permission
pkg_plan_mode --> svc_planMode
@@ -324,6 +327,7 @@ flowchart LR
svc_sessionPersistence --> pkg_agent_loop
svc_sessionPersistence --> pkg_hooks_claude
svc_sessionPersistence --> pkg_hooks_codex
+ svc_sessionPersistence --> pkg_message_feedback
svc_sessionPersistence --> pkg_session_query
svc_sessionPersistence --> pkg_session_query_sqlite
svc_sessionPersistence --> pkg_tool_bash
@@ -336,6 +340,7 @@ flowchart LR
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_invariants
+ svc_sessions --> pkg_message_feedback
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query
svc_sessions --> pkg_session_query_sqlite
@@ -346,6 +351,7 @@ flowchart LR
svc_skills --> pkg_tool_skill
svc_spillStore --> pkg_spill_policy
svc_storage --> pkg_storage_domain
+ svc_storageDomain --> pkg_message_feedback
svc_storageDomain --> pkg_workspace
svc_subagents --> pkg_tool_ralph
svc_subagents --> pkg_tool_subagent
@@ -394,16 +400,17 @@ flowchart LR
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、[`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop)、[`compact-basic`](../packages/compact/compact-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 |
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
-| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop)、[`agent`](../packages/core/agent)、[`session-persistence`](../packages/session/session-persistence)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess)、[`invariants`](../packages/support/invariants) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
+| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop)、[`agent`](../packages/core/agent)、[`session-persistence`](../packages/session/session-persistence)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess)、[`invariants`](../packages/support/invariants)、[`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session)、[`agent`](../packages/core/agent)、[`scope`](../packages/core/scope)、[`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader)、[`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |
-| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl)、[`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop)、[`tool-bash`](../packages/bash/tool-bash)、[`hooks-claude`](../packages/hooks/hooks-claude)、[`hooks-codex`](../packages/hooks/hooks-codex)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | 各后端持久化同一套 SessionEvent 词汇;应用在组合时选择后端。 |
+| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl)、[`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop)、[`tool-bash`](../packages/bash/tool-bash)、[`hooks-claude`](../packages/hooks/hooks-claude)、[`hooks-codex`](../packages/hooks/hooks-codex)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`message-feedback`](../packages/feedback/message-feedback) | - | 各后端持久化同一套 SessionEvent 词汇;应用在组合时选择后端。 |
| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、`apiproxy` | - | 插件注册命名空间 schema 并解析分层值;提供方存储原始文档。LLM(大语言模型)适配器在用户分区下将其入口配置注册为组合基础;Web 网关提供经过脱敏的分层描述符,并写入用户层。 |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、`apiproxy` | - | 配置携带对机密信息的引用;提供方拥有实际值。消费方按操作解析,因此轮换后的凭据会在紧接着的下一次请求中生效;Web 网关提供不含实际值的视图和只写存储。 |
| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | 该 seam 捕获会话记录、进行脱敏并交给一个后端;没有其他组件消费该服务,其输出会离开当前进程。 |
| `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json)、[`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | 各后端以不同名称并列注册;数据形态(领域优先)挂载到枢纽上,并将类型化操作转换为不透明的 KV 单元原语。 |
-| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | 等待所有已配置后端就绪,然后将领域形态发布为一个受生命周期约束的服务,用于类型化持久状态。 |
+| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace)、[`message-feedback`](../packages/feedback/message-feedback) | - | 等待所有已配置后端就绪,然后将领域形态发布为一个受生命周期约束的服务,用于类型化持久状态。 |
+| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有本地逐 assistant 消息反馈、生命周期与目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约,且不进入 Session 历史或遥测。 |
| `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | 通过领域设施拥有带 WorkspaceId 品牌类型的记录;稳定的 sessionIds 账户驱动 Host RPC 与 GUI 投影。 |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference)、[`tool-session-query`](../packages/session-query/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 |
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 |
@@ -434,12 +441,12 @@ flowchart LR
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local)、[`fs-sandbox`](../packages/fs/fs-sandbox)、[`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn)、[`subagent-fork`](../packages/subagent/subagent-fork)、[`subagent-acp`](../packages/subagent/subagent-acp)、[`subagent-codex`](../packages/subagent/subagent-codex)、[`subagent-claude-code`](../packages/subagent/subagent-claude-code)、[`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-subagent-control`](../packages/subagent/tool-subagent-control)、[`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 |
-| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-tasks`](../packages/tasks/tool-tasks) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-tasks 是面向模型的控制接口,用于读取、列出和终止这些工作;tasks-local 是进程本地注册表。 |
+| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-tasks`](../packages/tasks/tool-tasks) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-tasks 是面向模型的控制器,用于读取、列出和终止这些工作;tasks-local 是进程本地注册表。 |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa)、[`web-search-perplexity`](../packages/web/web-search-perplexity)、[`web-search-deepseek`](../packages/web/web-search-deepseek)、[`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |
| `ctx.directoryPicker` | `seam` | `directory-picker` | `directory-picker-native`、`directory-picker-browse` | `apiproxy` | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 |
| `ctx.httpServer` | `core` | `webserver` | - | `connection`、`modules`、`hmr` | - | 普通的 node:http 载体:具名路由注册表、索引转换 tap,以及静态 dist 回退;Web 传输插件注册自己的路由。 |
-| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | 通过增量 dshClient 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 |
+| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | 通过增量 `dsh.client` 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 |
| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | [`tool-workflow`](../packages/workflow/tool-workflow)、[`tool-ralph`](../packages/workflow/tool-ralph) | - | 每个上下文使用一个引擎,与 bash 相同,且没有具名提供方注册表;通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 |
维护模式:混合模式。服务从 Cordis 声明中发现;接口、实现和消费方角色在 `scripts/gen-doc-graphs.ts` 中分类,并设有完整性守卫。
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 60045c08ca..9535eb02a2 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.i18n.yaml
@@ -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 docs/config-catalog.md
-config-catalog.md: f2a5dd9cd9595323156c5f887944e06381ed62ac
-config-catalog.zh.md: dffcfcb6e41df2bfb0dde9a77ba2172b6c68a7e7
+config-catalog.md: efdc9ba6326b026b6d0d11b6c7b5df0deee611ee
+config-catalog.zh.md: bac39fbef04f8fd0d1d2fbc94f7ad96611273f7b
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index f2a5dd9cd9..efdc9ba632 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -3,7 +3,7 @@
# Plugin Config Catalog
-Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin's full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated `cordis-surface` region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.
+Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin's full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated Cordis API region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.
This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verified fresh by `pnpm run verify-config-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks use a `ts config-catalog` fence (skipped by doc-typecheck, since a lone declaration referencing imports is not standalone-compilable). The generator also cross-checks the runtime schemastery schema against the pasted declaration — every schema-validated key, nested keys included, must be locatable on the declared config type — so the paste cannot hide a loader-accepted field.
@@ -69,7 +69,7 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable
- /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
+ /** Generic background-task controls forwarded through agent-core; set false to omit their tools. */
toolTasks?: NonNullable
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
goals?: agentCore.GoalConfig | false
@@ -259,7 +259,7 @@ export interface Config {
Depends on: [`ToolPresentationMode`](subsystems/tools.md)
-Source: [`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
+Source: [`packages/core/agent-tool-mode/src/index.ts:38`](../packages/core/agent-tool-mode/src/index.ts)
## `@deepseek-ai/dsh-attachment-local`
@@ -533,7 +533,7 @@ export interface Config {
}
```
-Source: [`packages/fs/fs-local/src/index.ts:40`](../packages/fs/fs-local/src/index.ts)
+Source: [`packages/fs/fs-local/src/index.ts:41`](../packages/fs/fs-local/src/index.ts)
## `@deepseek-ai/dsh-fs-sandbox`
@@ -572,7 +572,7 @@ Source: [`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.
Requires: `agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
-/** Plugin config: the task, patched in by the launcher. */
+/** Plugin config: the task resolved from this app's injected provider service. */
export interface Config {
/** The prompt text for the single run. */
task: string
@@ -724,7 +724,7 @@ export interface JsonRpcConfig {
Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`)
-Source: [`packages/scaffold/server/src/index.ts:29`](../packages/scaffold/server/src/index.ts)
+Source: [`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts)
## `@deepseek-ai/dsh-llm-deepseek`
@@ -986,6 +986,8 @@ export interface ReplayModelConfig {
description?: string
/** Optional positive integer context capacity published by the replay adapter. */
contextWindow?: number
+ /** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
+ inputModalities?: readonly ModelModality[]
/**
* Optional per-request output cap the replay route materializes when callers
* omit one, so replay reconstructs the request header a live catalog produced.
@@ -1001,9 +1003,9 @@ export interface ReplayModelConfig {
}
```
-Depends on: [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
+Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
-Source: [`packages/support/llm-replay/src/index.ts:769`](../packages/support/llm-replay/src/index.ts)
+Source: [`packages/support/llm-replay/src/index.ts:776`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-llm-retry`
@@ -1086,6 +1088,8 @@ export interface StdioConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
}
/** Config for connecting to an MCP server over Streamable HTTP (SSE). */
@@ -1106,10 +1110,38 @@ export interface StreamableHttpConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
+}
+
+/** Automatic reconnect policy for one MCP server connection. */
+export interface ReconnectConfig {
+ /** Reconnect automatically after a lost connection (default true). */
+ enabled?: boolean
+ /** First reconnect delay in milliseconds; doubles per consecutive failed attempt (default 500). */
+ initialDelayMs?: number
+ /** Backoff ceiling in milliseconds; also the uptime after which the attempt budget resets (default 30000). */
+ maxDelayMs?: number
+ /** Consecutive failed attempts per outage before giving up for good (default 10). */
+ maxAttempts?: number
}
```
-Source: [`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
+Source: [`packages/mcp/mcp-client/src/index.ts:98`](../packages/mcp/mcp-client/src/index.ts)
+
+## `@deepseek-ai/dsh-message-feedback`
+
+Requires: `storageDomain` · `sessionPersistence` · `sessions`
+
+```ts config-catalog
+/** Required deployment policy for optional notes. */
+export interface Config {
+ /** Maximum UTF-8 byte length accepted for one note. */
+ readonly maxNoteBytes: number
+}
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1161,6 +1193,8 @@ export interface Config {
* variables. Empty text drops the section at render, matching the registry.
*/
text: string
+ /** Make this persona the complete system prompt, suppressing every other section. */
+ complete?: boolean
}
```
@@ -1306,22 +1340,6 @@ export interface Config {
Source: [`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
-## `@deepseek-ai/dsh-repository-plugin`
-
-Requires: `loader`
-
-```ts config-catalog
-/** Repository Plugin runtime and source-list configuration. */
-export interface Config {
- /** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
- repositories?: string[]
- /** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
- cacheDir?: string
-}
-```
-
-Source: [`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
-
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
@@ -1350,7 +1368,7 @@ export interface Config {
}
```
-Source: [`packages/sandbox/sandbox-local/src/index.ts:43`](../packages/sandbox/sandbox-local/src/index.ts)
+Source: [`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`
@@ -1412,7 +1430,7 @@ export interface Config {
export type JsonlCompression = 'zstd' | 'none'
```
-Source: [`packages/session/session-persistence-jsonl/src/index.ts:59`](../packages/session/session-persistence-jsonl/src/index.ts)
+Source: [`packages/session/session-persistence-jsonl/src/index.ts:60`](../packages/session/session-persistence-jsonl/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-sqlite`
@@ -1455,7 +1473,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-Source: [`packages/session/session-persistence-sqlite/src/index.ts:67`](../packages/session/session-persistence-sqlite/src/index.ts)
+Source: [`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-projection-cache`
@@ -1576,7 +1594,7 @@ export enum TelemetryMode {
Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`)
-Source: [`packages/session/session-telemetry-otel/src/index.ts:79`](../packages/session/session-telemetry-otel/src/index.ts)
+Source: [`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts)
## `@deepseek-ai/dsh-session-title`
@@ -2003,7 +2021,7 @@ export interface Config {
}
```
-Source: [`packages/core/system-prompt/src/index.ts:177`](../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-time-context`
@@ -2114,7 +2132,7 @@ export interface Config {
}
```
-Source: [`packages/fs/tool-fs/src/index.ts:24`](../packages/fs/tool-fs/src/index.ts)
+Source: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts)
## `@deepseek-ai/dsh-tool-fs-search`
@@ -2338,14 +2356,15 @@ Source: [`packages/subagent/tool-subagent/src/index.ts:25`](../packages/subagent
## `@deepseek-ai/dsh-tool-subagent-report`
-Requires: `subagents` · `tools`
+Requires: `subagents` · `tools` · `systemPrompt`
```ts config-catalog
/** Config: how accepted reports are scheduled on the parent. */
export interface Config {
/**
- * Parent scheduling (default `quiet`). `quiet` adds context without waking;
- * `wakeup` creates one ordinary later parent turn.
+ * Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later
+ * parent turn; `quiet` adds context without waking, so a parked parent learns
+ * of the report only when something else wakes it.
*/
reportDelivery?: SubagentReportDelivery
}
@@ -2353,7 +2372,7 @@ export interface Config {
Depends on: [`SubagentReportDelivery`](subsystems/subagent.md)
-Source: [`packages/subagent/tool-subagent-report/src/index.ts:22`](../packages/subagent/tool-subagent-report/src/index.ts)
+Source: [`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
## `@deepseek-ai/dsh-tool-tasks`
@@ -2534,33 +2553,28 @@ Source: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
Requires: `httpServer`
```ts config-catalog
-/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
+/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
/** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
mode: WebMode
- /** Print the URL line on activation; a headless layer over this bundle turns it off. */
+ /** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
* section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
- * layer turns it off: its user is not interacting through the GUI, so the
+ * non-interactive layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
- /**
- * LAN IPv4 addresses sampled once by the launcher when the effective bind
- * is all-interfaces — the exact snapshot the /api trust fence was
- * configured with, so the printed LAN URL can never name an address the
- * fence rejects. Empty on a loopback bind.
- */
- lanAddresses: string[]
+ /** Explicit `--trusted-host` authorities from this invocation. */
+ trustedHosts: string[]
}
/** Web runtime mode: production, or development when the client-plugin HMR receiver is active. */
export type WebMode = 'production' | 'development'
```
-Source: [`packages/bundle/web-app/src/index.ts:32`](../packages/bundle/web-app/src/index.ts)
+Source: [`packages/bundle/web-app/src/index.ts:43`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2714,7 +2728,7 @@ Source: [`packages/context/workspace-context/src/config.ts:18`](../packages/cont
## Loadable plugins with no config
-These load from a `cordis.yml` entry with no `config:` block; they declare no config surface.
+These load from a `cordis.yml` entry with no `config:` block; they declare no configuration API.
- `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
- `@deepseek-ai/dsh-api-gateway` — requires `typert` ([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
@@ -2739,6 +2753,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-skill` ([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-slash` ([`packages/client/ui-slash/src/index.ts`](../packages/client/ui-slash/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent` ([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-task` ([`packages/client/ui-task/src/index.ts`](../packages/client/ui-task/src/index.ts))
- `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool` ([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory` ([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
@@ -2794,7 +2809,6 @@ Abstract service classes — a deployment loads a concrete implementation packag
Imported as libraries by other packages; a `cordis.yml` cannot load them.
-- `@deepseek-ai/create-sdk` ([`packages/scaffold/create-sdk/src/index.ts`](../packages/scaffold/create-sdk/src/index.ts))
- `@deepseek-ai/dsh-acp-snapshot` ([`packages/support/acp-snapshot/src/index.ts`](../packages/support/acp-snapshot/src/index.ts))
- `@deepseek-ai/dsh-agent-loop-testkit` ([`packages/support/agent-loop-testkit/src/index.ts`](../packages/support/agent-loop-testkit/src/index.ts))
- `@deepseek-ai/dsh-app-boot` ([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
@@ -2807,8 +2821,8 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-client-ui-slots` ([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
- `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
+- `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-environment` ([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts))
-- `@deepseek-ai/dsh-helper` ([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
- `@deepseek-ai/dsh-jsonrpc-demo` ([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts))
- `@deepseek-ai/dsh-llm-mock-server` ([`packages/support/llm-mock-server/src/index.ts`](../packages/support/llm-mock-server/src/index.ts))
@@ -2818,13 +2832,11 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-retention` ([`packages/util/retention/src/index.ts`](../packages/util/retention/src/index.ts))
- `@deepseek-ai/dsh-sandbox-windows-acl` ([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
- `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
-- `@deepseek-ai/dsh-scripts` ([`packages/scaffold/scripts/src/index.ts`](../packages/scaffold/scripts/src/index.ts))
-- `@deepseek-ai/dsh-sdk-client` ([`packages/scaffold/client/src/index.ts`](../packages/scaffold/client/src/index.ts))
-- `@deepseek-ai/dsh-sdk-protocol` ([`packages/scaffold/protocol/src/index.ts`](../packages/scaffold/protocol/src/index.ts))
+- `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts))
+- `@deepseek-ai/dsh-sdk-protocol` ([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts))
- `@deepseek-ai/dsh-session-telemetry` ([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts))
- `@deepseek-ai/dsh-session-title-llm` ([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts))
- `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))
-- `@deepseek-ai/dsh-telemetry` ([`packages/scaffold/telemetry/src/index.ts`](../packages/scaffold/telemetry/src/index.ts))
- `@deepseek-ai/dsh-timeout` ([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts))
- `@deepseek-ai/dsh-type-meta` ([`packages/typert/type-meta/src/index.ts`](../packages/typert/type-meta/src/index.ts))
- `@deepseek-ai/dsh-typert-generator` ([`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts))
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index dffcfcb6e4..bac39fbef0 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -71,7 +71,7 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable
- /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
+ /** Generic background-task controls forwarded through agent-core; set false to omit their tools. */
toolTasks?: NonNullable
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
goals?: agentCore.GoalConfig | false
@@ -261,7 +261,7 @@ export interface Config {
依赖:[`ToolPresentationMode`](subsystems/tools.md)
-来源:[`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
+来源:[`packages/core/agent-tool-mode/src/index.ts:38`](../packages/core/agent-tool-mode/src/index.ts)
## `@deepseek-ai/dsh-attachment-local`
@@ -574,7 +574,7 @@ export interface Config {
需要:`agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
-/** Plugin config: the task, patched in by the launcher. */
+/** Plugin config: the task resolved from this app's injected provider service. */
export interface Config {
/** The prompt text for the single run. */
task: string
@@ -726,7 +726,7 @@ export interface JsonRpcConfig {
依赖:`Readable`(`node:stream`)· `Writable`(`node:stream`)
-来源:[`packages/scaffold/server/src/index.ts:29`](../packages/scaffold/server/src/index.ts)
+来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts)
## `@deepseek-ai/dsh-llm-deepseek`
@@ -988,6 +988,8 @@ export interface ReplayModelConfig {
description?: string
/** Optional positive integer context capacity published by the replay adapter. */
contextWindow?: number
+ /** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
+ inputModalities?: readonly ModelModality[]
/**
* Optional per-request output cap the replay route materializes when callers
* omit one, so replay reconstructs the request header a live catalog produced.
@@ -1003,9 +1005,9 @@ export interface ReplayModelConfig {
}
```
-依赖:[`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
+依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
-来源:[`packages/support/llm-replay/src/index.ts:769`](../packages/support/llm-replay/src/index.ts)
+来源:[`packages/support/llm-replay/src/index.ts:776`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-llm-retry`
@@ -1088,6 +1090,8 @@ export interface StdioConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
}
/** Config for connecting to an MCP server over Streamable HTTP (SSE). */
@@ -1108,10 +1112,38 @@ export interface StreamableHttpConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
+}
+
+/** Automatic reconnect policy for one MCP server connection. */
+export interface ReconnectConfig {
+ /** Reconnect automatically after a lost connection (default true). */
+ enabled?: boolean
+ /** First reconnect delay in milliseconds; doubles per consecutive failed attempt (default 500). */
+ initialDelayMs?: number
+ /** Backoff ceiling in milliseconds; also the uptime after which the attempt budget resets (default 30000). */
+ maxDelayMs?: number
+ /** Consecutive failed attempts per outage before giving up for good (default 10). */
+ maxAttempts?: number
}
```
-来源:[`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
+来源:[`packages/mcp/mcp-client/src/index.ts:94`](../packages/mcp/mcp-client/src/index.ts)
+
+## `@deepseek-ai/dsh-message-feedback`
+
+需要:`storageDomain` · `sessionPersistence` · `sessions`
+
+```ts config-catalog
+/** Required deployment policy for optional notes. */
+export interface Config {
+ /** Maximum UTF-8 byte length accepted for one note. */
+ readonly maxNoteBytes: number
+}
+```
+
+来源:[`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1163,6 +1195,8 @@ export interface Config {
* variables. Empty text drops the section at render, matching the registry.
*/
text: string
+ /** Make this persona the complete system prompt, suppressing every other section. */
+ complete?: boolean
}
```
@@ -1308,22 +1342,6 @@ export interface Config {
来源:[`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
-## `@deepseek-ai/dsh-repository-plugin`
-
-需要:`loader`
-
-```ts config-catalog
-/** Repository Plugin runtime and source-list configuration. */
-export interface Config {
- /** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
- repositories?: string[]
- /** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
- cacheDir?: string
-}
-```
-
-来源:[`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
-
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
@@ -1352,7 +1370,7 @@ export interface Config {
}
```
-来源:[`packages/sandbox/sandbox-local/src/index.ts:43`](../packages/sandbox/sandbox-local/src/index.ts)
+来源:[`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`
@@ -1457,7 +1475,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-来源:[`packages/session/session-persistence-sqlite/src/index.ts:67`](../packages/session/session-persistence-sqlite/src/index.ts)
+来源:[`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-projection-cache`
@@ -2005,7 +2023,7 @@ export interface Config {
}
```
-来源:[`packages/core/system-prompt/src/index.ts:177`](../packages/core/system-prompt/src/index.ts)
+来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-time-context`
@@ -2339,14 +2357,15 @@ export interface Config {
## `@deepseek-ai/dsh-tool-subagent-report`
-需要:`subagents` · `tools`
+需要:`subagents` · `tools` · `systemPrompt`
```ts config-catalog
/** Config: how accepted reports are scheduled on the parent. */
export interface Config {
/**
- * Parent scheduling (default `quiet`). `quiet` adds context without waking;
- * `wakeup` creates one ordinary later parent turn.
+ * Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later
+ * parent turn; `quiet` adds context without waking, so a parked parent learns
+ * of the report only when something else wakes it.
*/
reportDelivery?: SubagentReportDelivery
}
@@ -2354,7 +2373,7 @@ export interface Config {
依赖:[`SubagentReportDelivery`](subsystems/subagent.md)
-来源:[`packages/subagent/tool-subagent-report/src/index.ts:22`](../packages/subagent/tool-subagent-report/src/index.ts)
+来源:[`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
## `@deepseek-ai/dsh-tool-tasks`
@@ -2535,33 +2554,28 @@ export interface WebServiceConfig {
需要:`httpServer`
```ts config-catalog
-/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
+/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
/** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
mode: WebMode
- /** Print the URL line on activation; a headless layer over this bundle turns it off. */
+ /** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
* section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
- * layer turns it off: its user is not interacting through the GUI, so the
+ * non-interactive layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
- /**
- * LAN IPv4 addresses sampled once by the launcher when the effective bind
- * is all-interfaces — the exact snapshot the /api trust fence was
- * configured with, so the printed LAN URL can never name an address the
- * fence rejects. Empty on a loopback bind.
- */
- lanAddresses: string[]
+ /** Explicit `--trusted-host` authorities from this invocation. */
+ trustedHosts: string[]
}
/** Web runtime mode: production, or development when the client-plugin HMR receiver is active. */
export type WebMode = 'production' | 'development'
```
-来源:[`packages/bundle/web-app/src/index.ts:32`](../packages/bundle/web-app/src/index.ts)
+来源:[`packages/bundle/web-app/src/index.ts:43`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2740,6 +2754,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-skill`([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-slash`([`packages/client/ui-slash/src/index.ts`](../packages/client/ui-slash/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent`([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-task`([`packages/client/ui-task/src/index.ts`](../packages/client/ui-task/src/index.ts))
- `@deepseek-ai/dsh-client-ui-theme`([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool`([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory`([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
@@ -2794,7 +2809,6 @@ export interface Config {
由其他包作为库导入;`cordis.yml` 无法加载它们。
-- `@deepseek-ai/create-sdk`([`packages/scaffold/create-sdk/src/index.ts`](../packages/scaffold/create-sdk/src/index.ts))
- `@deepseek-ai/dsh-acp-snapshot`([`packages/support/acp-snapshot/src/index.ts`](../packages/support/acp-snapshot/src/index.ts))
- `@deepseek-ai/dsh-agent-loop-testkit`([`packages/support/agent-loop-testkit/src/index.ts`](../packages/support/agent-loop-testkit/src/index.ts))
- `@deepseek-ai/dsh-app-boot`([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
@@ -2807,8 +2821,8 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-slots`([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
- `@deepseek-ai/dsh-client-web`([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react`([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
+- `@deepseek-ai/dsh-cmdline`([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-environment`([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts))
-- `@deepseek-ai/dsh-helper`([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol`([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
- `@deepseek-ai/dsh-jsonrpc-demo`([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts))
- `@deepseek-ai/dsh-llm-mock-server`([`packages/support/llm-mock-server/src/index.ts`](../packages/support/llm-mock-server/src/index.ts))
@@ -2818,13 +2832,11 @@ export interface Config {
- `@deepseek-ai/dsh-retention`([`packages/util/retention/src/index.ts`](../packages/util/retention/src/index.ts))
- `@deepseek-ai/dsh-sandbox-windows-acl`([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
- `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
-- `@deepseek-ai/dsh-scripts`([`packages/scaffold/scripts/src/index.ts`](../packages/scaffold/scripts/src/index.ts))
-- `@deepseek-ai/dsh-sdk-client`([`packages/scaffold/client/src/index.ts`](../packages/scaffold/client/src/index.ts))
-- `@deepseek-ai/dsh-sdk-protocol`([`packages/scaffold/protocol/src/index.ts`](../packages/scaffold/protocol/src/index.ts))
+- `@deepseek-ai/dsh-sdk-client`([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts))
+- `@deepseek-ai/dsh-sdk-protocol`([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts))
- `@deepseek-ai/dsh-session-telemetry`([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts))
- `@deepseek-ai/dsh-session-title-llm`([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts))
- `@deepseek-ai/dsh-subagent-inprocess`([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))
-- `@deepseek-ai/dsh-telemetry`([`packages/scaffold/telemetry/src/index.ts`](../packages/scaffold/telemetry/src/index.ts))
- `@deepseek-ai/dsh-timeout`([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts))
- `@deepseek-ai/dsh-type-meta`([`packages/typert/type-meta/src/index.ts`](../packages/typert/type-meta/src/index.ts))
- `@deepseek-ai/dsh-typert-generator`([`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts))
diff --git a/docs/cookbook/adding-a-conversation-node.i18n.yaml b/docs/cookbook/adding-a-conversation-node.i18n.yaml
index aa268e9461..52234b5562 100644
--- a/docs/cookbook/adding-a-conversation-node.i18n.yaml
+++ b/docs/cookbook/adding-a-conversation-node.i18n.yaml
@@ -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 docs/cookbook/adding-a-conversation-node.md
-adding-a-conversation-node.md: ea4ec73eb109af6b0e4c7cf50fc8692942c75dd4
-adding-a-conversation-node.zh.md: 4b9a8049e2f1d060ec4bc3334036559b989ea562
+adding-a-conversation-node.md: c1965dc8a3081eebb8c1026ac53d2f7b8964edb7
+adding-a-conversation-node.zh.md: 92445e1432369a4e42cc372b5d5869c3cdeada4a
diff --git a/docs/cookbook/adding-a-conversation-node.md b/docs/cookbook/adding-a-conversation-node.md
index ea4ec73eb1..c1965dc8a3 100644
--- a/docs/cookbook/adding-a-conversation-node.md
+++ b/docs/cookbook/adding-a-conversation-node.md
@@ -120,6 +120,7 @@ function viewData(state: ReviewState): ReviewChatData {
const reviewDefinition: ConversationNodeDefinition = {
kind: 'review-job',
+ target: 'chat',
match: (event) => {
if (event.type === 'review/start') {
return { id: String(event.data.reviewId), role: 'start' }
@@ -161,8 +162,8 @@ const reviewDefinition: ConversationNodeDefinition = {
value: viewData(context.state),
}
},
- buildViewNode: (context, target) => {
- if (target !== 'chat' || context.state === undefined) return null
+ buildViewNode: (context) => {
+ if (context.state === undefined) return null
return {
key: context.key,
kind: 'review-job',
@@ -196,7 +197,7 @@ export function apply(ctx: ClientContext): void {
`buildLocationData(context, scope)` optionally publishes Definition-owned data onto an engine-owned Turn or Step. Use declaration merging to give each key a precise value type. Another Node in the same Location can consume that value through its constrained slot hook, such as `useTurnData(key)`, without receiving the Session or scanning `snapshot.chat.nodes`.
-`buildViewNode(context, target)` materializes the final target-specific Node. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
+`target` and `buildViewNode(context)` declare one target-owned rendering contribution and must appear together. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
## 3. Query an earlier business Context only at start
diff --git a/docs/cookbook/adding-a-conversation-node.zh.md b/docs/cookbook/adding-a-conversation-node.zh.md
index 4b9a8049e2..92445e1432 100644
--- a/docs/cookbook/adding-a-conversation-node.zh.md
+++ b/docs/cookbook/adding-a-conversation-node.zh.md
@@ -120,6 +120,7 @@ function viewData(state: ReviewState): ReviewChatData {
const reviewDefinition: ConversationNodeDefinition = {
kind: 'review-job',
+ target: 'chat',
match: (event) => {
if (event.type === 'review/start') {
return { id: String(event.data.reviewId), role: 'start' }
@@ -161,8 +162,8 @@ const reviewDefinition: ConversationNodeDefinition = {
value: viewData(context.state),
}
},
- buildViewNode: (context, target) => {
- if (target !== 'chat' || context.state === undefined) return null
+ buildViewNode: (context) => {
+ if (context.state === undefined) return null
return {
key: context.key,
kind: 'review-job',
@@ -196,7 +197,7 @@ export function apply(ctx: ClientContext): void {
`buildLocationData(context, scope)` 可以把 Definition 拥有的数据发布到引擎拥有的 Turn 或 Step 上。通过 declaration merging 为每个 key 指定精确 value 类型。同一 Location 内的另一个 Node 可以使用受限 slot hook(例如 `useTurnData(key)`)读取该值,无须取得 Session,也无须扫描 `snapshot.chat.nodes`。
-`buildViewNode(context, target)` 物化最终的目标专用 Node。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
+`target` 与 `buildViewNode(context)` 必须同时声明一项由 target 拥有的渲染贡献。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
## 3. 只在 start 时查询更早的业务 Context
diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml
index 26d61a6041..3418096c10 100644
--- a/docs/cookbook/adding-a-package.i18n.yaml
+++ b/docs/cookbook/adding-a-package.i18n.yaml
@@ -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 docs/cookbook/adding-a-package.md
-adding-a-package.md: 9e82e6d00177768a6368d1cd9740afa585543171
-adding-a-package.zh.md: 072df33138a1ead20c497cebd8e4aa960c2d1fc8
+adding-a-package.md: 4320d9737da1a6144f77fc3f02e2801e723f62b4
+adding-a-package.zh.md: 5e2c9094fbd0fb3f03c115598afdb32ac42e1e01
diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md
index 9e82e6d001..4320d9737d 100644
--- a/docs/cookbook/adding-a-package.md
+++ b/docs/cookbook/adding-a-package.md
@@ -22,7 +22,7 @@ packages///
Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it.
-package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
+package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `@deepseek-ai/cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `@deepseek-ai/schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
@@ -34,7 +34,7 @@ In-package relative imports use explicit `.ts` specifiers in source (for example
| `tsconfig.host.json` (Host package) or `tsconfig.client.json` (Client package) | add `{ "path": "./packages//" }` to `references` — an ordinary package belongs to exactly one aggregate, never both. `api/remotes` uses a repository-specific split because the Host generates a contract that the Client consumes in a later phase; new packages must not copy it ([layout](../development.md#typescript-project-layout)) |
| `knip.json` | only if the package has entrypoints that repository discovery does not already cover |
-A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dshClient` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract.
+A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dsh.client` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract.
Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `.oxlintrc.json`, `scripts/check-workspace-constraints.ts`.
@@ -49,7 +49,7 @@ Keep package-specific service API, config, events, extension points, and design
````markdown
## Model Experience
-### Request surface and condition
+### Request context and condition
#### What the model sees
@@ -74,7 +74,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
- **Consumer-visible gap** — exact missing operation or case, its consequence, and any maintainer constraint.
````
-Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary-model surface, with the three ordered H4 fields shown above and one prose paragraph under each. Quote stable text owned by the package: system-prompt prose goes in a titled H5 plus `markdown` fence under the field that introduces it—normally `What the model sees`—other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema surface links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema surfaces separate when scoping can hide one without the other. In `KV Cache effect`, distinguish append-only growth, a stable repeated prefix, replacement of earlier request tokens, and an independent model request, then name the package-owned changes that can invalidate reuse. “Does not invalidate” means the package preserves an already-reusable prefix; provider cache availability and eviction remain outside the package contract. The [prose standard](../../.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the required section structure.
+Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary model-context entry, with the three ordered H4 fields shown above and one prose paragraph under each. Quote stable text owned by the package: system-prompt prose goes in a titled H5 plus `markdown` fence under the field that introduces it—normally `What the model sees`—other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema entry links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema entries separate when scoping can hide one without the other. In `KV Cache effect`, distinguish append-only growth, a stable repeated prefix, replacement of earlier request tokens, and an independent model request, then name the package-owned changes that can invalidate reuse. “Does not invalidate” means the package preserves an already-reusable prefix; provider cache availability and eviction remain outside the package contract. The [prose standard](../../.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the required section structure.
A package with no context effect or one consumer-owned path uses the audited `None, as ` or `Indirectly, through ` sentence in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts), followed by a `KV Cache effect` H4 and one non-empty paragraph; a model-agnostic generic package may instead join `NO_MODEL_EXPERIENCE_SECTION`. Do not expand either case into a description of another package's work. The limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) is independent. The [Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale.
diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md
index 072df33138..5e2c9094fb 100644
--- a/docs/cookbook/adding-a-package.zh.md
+++ b/docs/cookbook/adding-a-package.zh.md
@@ -22,7 +22,7 @@ packages///
当已有分组与包的角色匹配时,选择该分组(`core`、`llm`、`bash`、`compact`、`subagent`、`todo`、`session-persistence`、`ui`、`util` 或 `support`)。允许新建分组,但分组只是纯容器:没有 `package.json`,没有源文件,包仍然恰好位于其下一层。
-package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
+package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`@deepseek-ai/cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`@deepseek-ai/schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
包内的相对导入在源码中使用显式 `.ts` 后缀(例如 `export * from './types.ts'`)。编译器在输出的 JS 中将其重写为 `.js`,在声明文件中保留显式 `.ts` 后缀;标准的 NodeNext/Node16 TypeScript 消费方会将其解析到同目录的 `.d.ts` 文件。
@@ -34,7 +34,7 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c
| `tsconfig.host.json`(Host 包)或 `tsconfig.client.json`(Client 包) | 在 `references` 中添加 `{ "path": "./packages//" }`——普通包恰好属于一个 aggregate,绝不两个都加。`api/remotes` 因 Host 生成约定与 Client 消费约定之间存在顺序依赖而使用仓库专属拆分,新增包不得仿照([布局](../development.md#typescript-project-layout)) |
| `knip.json` | 仅当包有仓库发现机制尚未覆盖的入口时需要 |
-`packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`);client 插件包还需在 package.json 声明 `dshClient`、导出 `./client`、调用共享 tsdown preset(`packages/client/tsdown.client.ts`)——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。
+`packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`);client 插件包还需在 package.json 声明 `dsh.client`、导出 `./client`、调用共享 tsdown preset(`packages/client/tsdown.client.ts`)——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。
以下内容由 glob 或包 manifest 发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts`、`tsdown.config.ts`、`.oxlintrc.json`、`scripts/check-workspace-constraints.ts`。
@@ -49,7 +49,7 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c
````markdown
## Model Experience
-### Request surface and condition
+### Request context and condition
#### What the model sees
@@ -74,7 +74,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
- **Consumer-visible gap** — exact missing operation or case, its consequence, and any maintainer constraint.
````
-根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助模型的 surface 使用一个 H3,包含上述三个有序 H4 字段,每个字段下有一个正文段落。引用包拥有的稳定文本:系统提示词放在引出它的字段下,用带标题的 H5 加 `markdown` 围栏表示,通常归入 `What the model sees`;其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。tool-schema surface 链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。填写 `KV Cache effect` 时,应区分仅追加增长、稳定重复的前缀、替换既有请求 token 和独立模型请求,并列出会使缓存复用失效、且由本包拥有的变化。“不使缓存失效”仅表示本包保留了已有的可复用前缀;缓存是否可用以及何时淘汰不属于本包约定。[行文标准](../../.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行所需章节结构。
+根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助的模型上下文条目使用一个 H3,包含上述三个有序 H4 字段,每个字段下有一个正文段落。引用包拥有的稳定文本:系统提示词放在引出它的字段下,用带标题的 H5 加 `markdown` 围栏表示,通常归入 `What the model sees`;其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。工具 schema 条目链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。填写 `KV Cache effect` 时,应区分仅追加增长、稳定重复的前缀、替换既有请求 token 和独立模型请求,并列出会使缓存复用失效、且由本包拥有的变化。“不使缓存失效”仅表示本包保留了已有的可复用前缀;缓存是否可用以及何时淘汰不属于本包约定。[行文标准](../../.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行所需章节结构。
没有上下文效果或仅有消费方拥有路径的包使用 [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) 中经过审计的 `None, as ` 或 `Indirectly, through ` 语句,随后添加 `KV Cache effect` H4 和一个非空正文段落;与模型无关的通用包可以改为加入 `NO_MODEL_EXPERIENCE_SECTION`。两种情况都不要展开为对另一个包工作的描述。limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) 独立管理。[Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) 记录了设计动机。
diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml
index e232ad4302..3e2be7f506 100644
--- a/docs/cookbook/adding-a-tool.i18n.yaml
+++ b/docs/cookbook/adding-a-tool.i18n.yaml
@@ -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 docs/cookbook/adding-a-tool.md
-adding-a-tool.md: b030d3c3a6b7dd66b6594779345a96af3a895bd8
-adding-a-tool.zh.md: 4272a7a4571782bc213ca29de1a57d51fbd24075
+adding-a-tool.md: 291e3a8b684a0d042102502279fe4c4393e34735
+adding-a-tool.zh.md: 69bd7ee150f30b1d3e6be08d87347e52952a360c
diff --git a/docs/cookbook/adding-a-tool.md b/docs/cookbook/adding-a-tool.md
index b030d3c3a6..291e3a8b68 100644
--- a/docs/cookbook/adding-a-tool.md
+++ b/docs/cookbook/adding-a-tool.md
@@ -8,7 +8,7 @@ Reference for the contracts a model-facing tool must satisfy. For an ordered fir
```ts
import { readFile } from 'node:fs/promises'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
@@ -50,7 +50,7 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool. S
## Long-running work
-Gate `run_in_background` with producer config, then register through `ctx.tasks.start({ kind, label, owner: exec.agent, run })`. The registry rejects a pre-aborted invocation before the producer body; the runtime validates ownership and control-surface availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup. A successful background branch returns a typed canonical handle such as `{ kind: 'background', taskId }`; its Native renderer may keep human prose such as `started background task bash-1`, but Code Mode must never parse that prose to recover the id.
+Gate `run_in_background` with producer config, then register through `ctx.tasks.start({ kind, label, owner: exec.agent, run })`. The registry rejects a pre-aborted invocation before the producer body; the runtime validates ownership and task-controller availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup. A successful background branch returns a typed canonical handle such as `{ kind: 'background', taskId }`; its Native renderer may keep human prose such as `started background task bash-1`, but Code Mode must never parse that prose to recover the id.
The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. A pre-aborted call is a failure because no task exists whose id could satisfy the successful output schema. Once `ctx.tasks.start()` publishes the id, use a task-owned cancellation signal rather than `exec.signal`: later outer-call cancellation stops waiting for the call but does not kill published work; `task_kill`, owner disposal, and service teardown own that lifetime. Foreground work remains coupled to `exec.signal`. See the [background task runtime Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer.
diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md
index 4272a7a457..69bd7ee150 100644
--- a/docs/cookbook/adding-a-tool.zh.md
+++ b/docs/cookbook/adding-a-tool.zh.md
@@ -8,7 +8,7 @@
```ts
import { readFile } from 'node:fs/promises'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
@@ -50,7 +50,7 @@ export function apply(ctx: Context) {
## 长时间运行的工作
-通过 producer 配置控制 `run_in_background`,然后使用 `ctx.tasks.start({ kind, label, owner: exec.agent, run })` 注册任务。注册表会在进入 producer 主体前将已预先中止的调用判为失败;运行时会在 `run()` 启动工作前校验 owner 和控制面是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。成功的后台分支会返回类型化的规范句柄,如 `{ kind: 'background', taskId }`;其 Native 渲染器可以保留 `started background task bash-1` 这类供人阅读的自然语言,但 Code Mode 绝不能通过解析该文本取得 id。
+通过 producer 配置控制 `run_in_background`,然后使用 `ctx.tasks.start({ kind, label, owner: exec.agent, run })` 注册任务。注册表会在进入 producer 主体前将已预先中止的调用判为失败;运行时会在 `run()` 启动工作前校验 owner 和任务控制器是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。成功的后台分支会返回类型化的规范句柄,如 `{ kind: 'background', taskId }`;其 Native 渲染器可以保留 `started background task bash-1` 这类供人阅读的自然语言,但 Code Mode 绝不能通过解析该文本取得 id。
producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。预先中止的调用属于失败,因为此时没有任务,其 id 无法满足成功输出 schema。`ctx.tasks.start()` 发布 id 后,应使用任务自有的取消信号,而不是 `exec.signal`:之后取消外层调用只会停止等待本次调用,不会终止已经发布的工作;该生命周期归 `task_kill`、owner dispose 和服务 teardown 所有。前台工作仍与 `exec.signal` 耦合。流式 producer 的示例和完整约定见[后台 task 运行时 Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。
diff --git a/docs/cookbook/adding-a-vendored-package.i18n.yaml b/docs/cookbook/adding-a-vendored-package.i18n.yaml
index 4f5b8c3c49..b17f3390cf 100644
--- a/docs/cookbook/adding-a-vendored-package.i18n.yaml
+++ b/docs/cookbook/adding-a-vendored-package.i18n.yaml
@@ -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 docs/cookbook/adding-a-vendored-package.md
-adding-a-vendored-package.md: 724d89c1c7cd728f7123a6975b5500cd40815851
-adding-a-vendored-package.zh.md: d16ec1056431a4ac1c02d50a5ef0f0a64b67ca6d
+adding-a-vendored-package.md: 239ac27565204332559038014fabae83fc2d1057
+adding-a-vendored-package.zh.md: 2bf5c7eeaffec594dbc4d13e0aa33b12a9eccf4f
diff --git a/docs/cookbook/adding-a-vendored-package.md b/docs/cookbook/adding-a-vendored-package.md
index 724d89c1c7..239ac27565 100644
--- a/docs/cookbook/adding-a-vendored-package.md
+++ b/docs/cookbook/adding-a-vendored-package.md
@@ -8,7 +8,7 @@ When the harness needs another upstream Cordis package (e.g. `@cordisjs/plugin-h
```
vendor//
- package.json # from upstream; set "private": true, keep name/exports/type
+ package.json # from upstream; set "private": true, rescope the name, keep exports/type
tsconfig.json # extends ../../tsconfig.base.json (see configuration below)
src/ # the upstream src/ verbatim
README.md LICENSE # if upstream ships them
@@ -29,7 +29,7 @@ vendor//
}
```
-`package.json` invariants: `"private": true` (vendored packages are never published), keep upstream's `name`/`version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
+`package.json` invariants: `"private": true` (vendored packages are never published), rescope the `name` ([mapping](../rescope.md)) while keeping upstream's `version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
Local relative imports/exports in vendored TypeScript source use explicit `.ts` specifiers after copying. This is a repo-local build difference from upstream: `rewriteRelativeImportExtensions` emits `.js` runtime imports while declarations keep explicit `.ts` specifiers that NodeNext/Node16 TypeScript consumers can resolve.
diff --git a/docs/cookbook/adding-a-vendored-package.zh.md b/docs/cookbook/adding-a-vendored-package.zh.md
index d16ec10564..2bf5c7eeaf 100644
--- a/docs/cookbook/adding-a-vendored-package.zh.md
+++ b/docs/cookbook/adding-a-vendored-package.zh.md
@@ -8,7 +8,7 @@
```
vendor//
- package.json # from upstream; set "private": true, keep name/exports/type
+ package.json # from upstream; set "private": true, rescope the name, keep exports/type
tsconfig.json # extends ../../tsconfig.base.json (see configuration below)
src/ # the upstream src/ verbatim
README.md LICENSE # if upstream ships them
@@ -29,7 +29,7 @@ vendor//
}
```
-`package.json` 的不变式:`"private": true`(vendored 包永不发布);保留上游的 `name`/`version`/`exports`/`type`;声明元数据指向 `lib/types`;发布 `.d.ts` 与 `.d.ts.map` 声明输出;在 `peerDependencies` 中列出其 Cordis 依赖(与上游 manifest(元数据清单)一致)。传递性上游依赖本身也必须被 vendor 或已存在于仓库中——vendor 一个包往往意味着 vendor 其整条依赖树(如 `@cordisjs/plugin-http` 会拉入 `@cordisjs/fetch-file`)。
+`package.json` 的不变式:`"private": true`(vendored 包永不发布);改写 `name` 的 scope([映射](../rescope.md)),保留上游的 `version`/`exports`/`type`;声明元数据指向 `lib/types`;发布 `.d.ts` 与 `.d.ts.map` 声明输出;在 `peerDependencies` 中列出其 Cordis 依赖(与上游 manifest(元数据清单)一致)。传递性上游依赖本身也必须被 vendor 或已存在于仓库中——vendor 一个包往往意味着 vendor 其整条依赖树(如 `@cordisjs/plugin-http` 会拉入 `@cordisjs/fetch-file`)。
vendored TypeScript 源码中的本地相对导入/导出在复制后使用显式 `.ts` 后缀。这是仓库本地构建与上游的差异:`rewriteRelativeImportExtensions` 输出 `.js` 运行时导入,而声明文件保留显式 `.ts` 后缀,使 NodeNext/Node16 的 TypeScript 消费方能够解析。
diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml
index ac600e8c6d..3a98437564 100644
--- a/docs/cookbook/extension-cookbook.i18n.yaml
+++ b/docs/cookbook/extension-cookbook.i18n.yaml
@@ -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 docs/cookbook/extension-cookbook.md
-extension-cookbook.md: 95ba269a5d62e14cfde487d5a3aaca5db493657e
-extension-cookbook.zh.md: e3fbe09f1ec09568e3b259aee361d33ba3e62140
+extension-cookbook.md: f292075dfdad5016d81521318b38594e3d7ee8b4
+extension-cookbook.zh.md: 0623c49d9d7075b3823c1fd340b36a5fba21f31e
diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md
index 95ba269a5d..f292075dfd 100644
--- a/docs/cookbook/extension-cookbook.md
+++ b/docs/cookbook/extension-cookbook.md
@@ -13,7 +13,7 @@ A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `exec
This permission gate is one example of a hook plugin. It returns a typed decision from the `tools/pre-execute` gate to allow or deny a call; sandbox, permission, and plan-mode plugins can use this extension point. Hook plugins can intercept other extension points and are not inherently permission gates. A "native hook" is an ordinary Cordis plugin on an interception point; it needs no external protocol.
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise
@@ -37,7 +37,7 @@ This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an
A UI plugin renders from the `session/event` feed (the assistant token stream as `assistant/chunk`, plus turn/step boundaries and tool activity), and drives input back in via `agent.followup()` / `agent.steer()`. A browser plugin contributing a business row to the built-in Web Client instead registers a `ConversationNodeDefinition` and keyed Chat renderer; follow the [Conversation Node guide](adding-a-conversation-node.md).
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -67,7 +67,7 @@ A *protocol driver* adapts a wire peer to `ctx.agents`; it may serve a UI or an
[`packages/acp/acp`](../../packages/acp/acp) is the automation-only worked example: it exposes fresh text sessions over Agent Client Protocol JSON-RPC stdio, emits committed assistant text, and registers a one-shot machine permission answerer for agents it owns. Its [README](../../packages/acp/acp/README.md) defines the exact methods, event order, and lifecycle contract.
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-protocol-bridge'
export const inject = ['agents', 'sessions', 'sessionPersistence']
diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md
index e3fbe09f1e..0623c49d9d 100644
--- a/docs/cookbook/extension-cookbook.zh.md
+++ b/docs/cookbook/extension-cookbook.zh.md
@@ -13,7 +13,7 @@ harness 扩展的参考模式。代码片段省略了 import 和辅助实现,
这个权限门禁是钩子插件的一个示例。它从 `tools/pre-execute` 门禁返回一个类型化的决策,用于允许或拒绝一次调用;沙箱、权限和 plan-mode 插件都可以使用该扩展点。钩子插件也可以拦截其他扩展点,本身并不等同于权限门禁。「原生钩子」是在拦截点上运行的普通 Cordis 插件,不需要外部协议。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/chunk` 形式到达,加上轮次/步骤边界与工具活动),并通过 `agent.followup()` / `agent.steer()` 将输入驱动回去。如果浏览器插件要向内建 Web Client 贡献业务行,则应注册 `ConversationNodeDefinition` 与 keyed Chat renderer;具体步骤见 [Conversation Node 指南](adding-a-conversation-node.md)。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -67,7 +67,7 @@ export function apply(ctx: Context) {
[`packages/acp/acp`](../../packages/acp/acp) 是仅面向自动化的完整示例:它通过 ACP(Agent Client Protocol)JSON-RPC stdio 提供全新文本会话,发出已提交的助手文本,并为其拥有的 agent 注册一次性机器权限应答器。其 [README](../../packages/acp/acp/README.md) 定义确切的方法、事件顺序和生命周期约定。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-protocol-bridge'
export const inject = ['agents', 'sessions', 'sessionPersistence']
diff --git a/docs/cordis-api/fiber.i18n.yaml b/docs/cordis-api/fiber.i18n.yaml
index 6c01366dc1..537be01dbc 100644
--- a/docs/cordis-api/fiber.i18n.yaml
+++ b/docs/cordis-api/fiber.i18n.yaml
@@ -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 docs/cordis-api/fiber.md
-fiber.md: 36d2861ac6a53e8186a92d86c65ba228d4b59ee5
-fiber.zh.md: fafa559ca911677c43893862190009d82c39c56b
+fiber.md: 182b77390b29b8a90504437d0ccc2dfeba23921a
+fiber.zh.md: 9ed3e52618586dc3815b9d913439d11a227fb64b
diff --git a/docs/cordis-api/fiber.md b/docs/cordis-api/fiber.md
index 36d2861ac6..182b77390b 100644
--- a/docs/cordis-api/fiber.md
+++ b/docs/cordis-api/fiber.md
@@ -34,7 +34,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](../../vendor/cordis/src/fiber.ts#L420)
+[Source](../../vendor/cordis/src/fiber.ts#L415)
### ctx.fiber
@@ -97,7 +97,7 @@ public state
Current lifecycle state; transitions emit `internal/status`.
-[Source](../../vendor/cordis/src/fiber.ts#L192)
+[Source](../../vendor/cordis/src/fiber.ts#L194)
### fiber.dispose
@@ -108,7 +108,7 @@ public readonly dispose: () => Promise
Dispose this fiber: unload the plugin, then settle once cleanup finished.
-[Source](../../vendor/cordis/src/fiber.ts#L194)
+[Source](../../vendor/cordis/src/fiber.ts#L196)
### fiber.store
@@ -119,7 +119,7 @@ public store: Dict | undefined
Snapshot of required service implementations while loaded; `undefined` otherwise.
-[Source](../../vendor/cordis/src/fiber.ts#L196)
+[Source](../../vendor/cordis/src/fiber.ts#L198)
### fiber.inertia
@@ -130,7 +130,7 @@ public inertia: Promise | undefined
The in-flight load/unload transition, if one is currently running.
-[Source](../../vendor/cordis/src/fiber.ts#L198)
+[Source](../../vendor/cordis/src/fiber.ts#L200)
### fiber.name
@@ -141,7 +141,7 @@ get name()
The plugin's display name, inherited from the nearest named ancestor, else `'root'`.
-[Source](../../vendor/cordis/src/fiber.ts#L341)
+[Source](../../vendor/cordis/src/fiber.ts#L336)
### fiber.assertActive()
@@ -159,7 +159,7 @@ Throw if the fiber has already been disposed.
**Returns** nothing when the fiber is still active.
-[Source](../../vendor/cordis/src/fiber.ts#L356)
+[Source](../../vendor/cordis/src/fiber.ts#L351)
### fiber.effect(execute, label?)
@@ -190,7 +190,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](../../vendor/cordis/src/fiber.ts#L420)
+[Source](../../vendor/cordis/src/fiber.ts#L415)
### fiber.getEffects()
@@ -207,7 +207,7 @@ Return metadata for currently registered effects.
**Returns** one `EffectMeta` tree per labeled live effect.
-[Source](../../vendor/cordis/src/fiber.ts#L573)
+[Source](../../vendor/cordis/src/fiber.ts#L568)
### fiber.await()
@@ -225,7 +225,7 @@ Wait for current lifecycle work and rethrow startup errors.
**Returns** this fiber, once it has settled into a stable state.
-[Source](../../vendor/cordis/src/fiber.ts#L702)
+[Source](../../vendor/cordis/src/fiber.ts#L704)
### fiber.restart()
@@ -243,7 +243,7 @@ Dispose and immediately reload this plugin with its current config.
**Returns** a promise resolving once the reload settled.
-[Source](../../vendor/cordis/src/fiber.ts#L716)
+[Source](../../vendor/cordis/src/fiber.ts#L718)
### fiber.update(config, noSave?)
@@ -271,7 +271,7 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
**Returns** the update waterfall result; the default restart returns a promise.
-[Source](../../vendor/cordis/src/fiber.ts#L734)
+[Source](../../vendor/cordis/src/fiber.ts#L736)
## Effect
diff --git a/docs/cordis-api/fiber.zh.md b/docs/cordis-api/fiber.zh.md
index fafa559ca9..9ed3e52618 100644
--- a/docs/cordis-api/fiber.zh.md
+++ b/docs/cordis-api/fiber.zh.md
@@ -36,7 +36,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable>
**返回**一个用于撤销该作用的清理函数,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L420)
+[源码](../../vendor/cordis/src/fiber.ts#L415)
### ctx.fiber
@@ -99,7 +99,7 @@ public state
当前生命周期状态;状态转换会发出 `internal/status`。
-[源码](../../vendor/cordis/src/fiber.ts#L192)
+[源码](../../vendor/cordis/src/fiber.ts#L194)
### fiber.dispose
@@ -110,7 +110,7 @@ public readonly dispose: () => Promise
dispose 此 fiber:卸载插件,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L194)
+[源码](../../vendor/cordis/src/fiber.ts#L196)
### fiber.store
@@ -121,7 +121,7 @@ public store: Dict | undefined
加载期间所需服务实现的快照;其他情况下为 `undefined`。
-[源码](../../vendor/cordis/src/fiber.ts#L196)
+[源码](../../vendor/cordis/src/fiber.ts#L198)
### fiber.inertia
@@ -132,7 +132,7 @@ public inertia: Promise | undefined
当前正在进行的加载或卸载转换;如果没有此类转换,则为 undefined。
-[源码](../../vendor/cordis/src/fiber.ts#L198)
+[源码](../../vendor/cordis/src/fiber.ts#L200)
### fiber.name
@@ -143,7 +143,7 @@ get name()
插件的显示名称,继承自最近的具名祖先;如果不存在,则为 `'root'`。
-[源码](../../vendor/cordis/src/fiber.ts#L341)
+[源码](../../vendor/cordis/src/fiber.ts#L336)
### fiber.assertActive()
@@ -161,7 +161,7 @@ assertActive()
**返回**:fiber 仍处于活动状态时不返回任何内容。
-[源码](../../vendor/cordis/src/fiber.ts#L356)
+[源码](../../vendor/cordis/src/fiber.ts#L351)
### fiber.effect(execute, label?)
@@ -192,7 +192,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable>
**返回**一个用于撤销该作用的清理函数,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L420)
+[源码](../../vendor/cordis/src/fiber.ts#L415)
### fiber.getEffects()
@@ -209,7 +209,7 @@ getEffects()
**返回**:每个带标签的活动作用对应一棵 `EffectMeta` 树。
-[源码](../../vendor/cordis/src/fiber.ts#L573)
+[源码](../../vendor/cordis/src/fiber.ts#L568)
### fiber.await()
@@ -227,7 +227,7 @@ async await()
**返回**:进入稳定状态后的此 fiber。
-[源码](../../vendor/cordis/src/fiber.ts#L702)
+[源码](../../vendor/cordis/src/fiber.ts#L704)
### fiber.restart()
@@ -245,7 +245,7 @@ dispose 此插件,并立即使用其当前配置重新加载。
**返回**一个在重新加载完成后兑现的 promise。
-[源码](../../vendor/cordis/src/fiber.ts#L716)
+[源码](../../vendor/cordis/src/fiber.ts#L718)
### fiber.update(config, noSave?)
@@ -273,7 +273,7 @@ update(config: any, noSave = false)
**返回**更新 waterfall 的结果;默认的重新启动操作返回一个 promise。
-[源码](../../vendor/cordis/src/fiber.ts#L734)
+[源码](../../vendor/cordis/src/fiber.ts#L736)
## Effect
diff --git a/docs/cordis-api/inherited.md b/docs/cordis-api/inherited.md
index 8bf23274e5..2f83a9cd66 100644
--- a/docs/cordis-api/inherited.md
+++ b/docs/cordis-api/inherited.md
@@ -1,7 +1,7 @@
-# Inherited Cordis Surface
+# Inherited Cordis API
The framework `ctx` members and events every plugin sees beyond the harness tier — pinned vendor source ([vendoring policy](../../vendor/README.md)), summarized tersely so the harness pages stay focused on repository-owned vocabulary. Detailed Context, Fiber, Registry, and Service APIs are generated in [context.md](context.md), [fiber.md](fiber.md), [registry.md](registry.md), and [service.md](service.md); the event-dispatch methods in [events.md](events.md).
diff --git a/docs/cordis-primer.i18n.yaml b/docs/cordis-primer.i18n.yaml
index 12177e3d35..180ba85c01 100644
--- a/docs/cordis-primer.i18n.yaml
+++ b/docs/cordis-primer.i18n.yaml
@@ -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 docs/cordis-primer.md
-cordis-primer.md: c95909a4a1deab9407efedbb990ef13be6e43a16
-cordis-primer.zh.md: a18b8b37af19a610b71babbe5e67f96bb09e81b1
+cordis-primer.md: d1e7c5fd8eaaa89fe448d238359389d945cd6346
+cordis-primer.zh.md: d6ce0f2024f65b006c9505daffaa06a08bb56875
diff --git a/docs/cordis-primer.md b/docs/cordis-primer.md
index c95909a4a1..d1e7c5fd8e 100644
--- a/docs/cordis-primer.md
+++ b/docs/cordis-primer.md
@@ -35,7 +35,7 @@ For single-decision events, short-circuiting is the design. A policy listener ca
## Loader Configuration
-`@cordisjs/plugin-include` parses `!!js` into expression nodes, but the Loader interpolates only an entry's `config` before mounting the plugin. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, and `isolate`) remains literal; `disabled: !!js ...` is therefore a truthy object that always disables the entry. Use explicit config overlays when environment selection changes which plugins are mounted.
+`@deepseek-ai/cordis-plugin-include` parses `!!js` into expression nodes. Loader interpolates only an entry's `config`, after declared injections activate, against that plugin context (`ctx.serviceName`); Include preserves nested row expressions until target activation. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, `isolate`) stays literal, so `disabled: !!js ...` always disables the entry. Use overlays when the environment selects plugins.
## Practical Rules
diff --git a/docs/cordis-primer.zh.md b/docs/cordis-primer.zh.md
index a18b8b37af..d6ce0f2024 100644
--- a/docs/cordis-primer.zh.md
+++ b/docs/cordis-primer.zh.md
@@ -39,7 +39,7 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
## Loader 配置
-`@cordisjs/plugin-include` 将 `!!js` 解析为表达式节点,但 Loader 仅在挂载插件前对条目的 `config` 做插值。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept` 和 `isolate`)保持字面值;因此 `disabled: !!js ...` 是一个 truthy 对象,会始终禁用该条目。需要根据环境选择挂载哪些插件时,请使用显式的配置覆盖层。
+`@deepseek-ai/cordis-plugin-include` 将 `!!js` 解析为表达式节点。Loader 只在声明的注入激活后,基于该插件上下文(`ctx.serviceName`)插值条目的 `config`;Include 会保留嵌套行表达式,直到目标行激活。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept`、`isolate`)保持字面值,因此 `disabled: !!js ...` 始终禁用该条目。由环境选择插件时,请使用 overlay。
## 实践规则
diff --git a/docs/cordis-tutorial/01-first-plugin.i18n.yaml b/docs/cordis-tutorial/01-first-plugin.i18n.yaml
index 9bf649ab29..cbb4aee438 100644
--- a/docs/cordis-tutorial/01-first-plugin.i18n.yaml
+++ b/docs/cordis-tutorial/01-first-plugin.i18n.yaml
@@ -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 docs/cordis-tutorial/01-first-plugin.md
-01-first-plugin.md: 260026329443f9a5b8860d11a6527dbd687eb44c
-01-first-plugin.zh.md: 69dedb898c7ea29f99233f07126cd413fa0ddbe2
+01-first-plugin.md: 79df2f42df1f34a7ef32cc81607e1b926a1854ae
+01-first-plugin.zh.md: 87cd7a96843d69d68fb95662e59bbfb9ebfc2a37
diff --git a/docs/cordis-tutorial/01-first-plugin.md b/docs/cordis-tutorial/01-first-plugin.md
index 2600263294..79df2f42df 100644
--- a/docs/cordis-tutorial/01-first-plugin.md
+++ b/docs/cordis-tutorial/01-first-plugin.md
@@ -9,7 +9,7 @@ In the loader configuration used here, a Cordis plugin module named-exports an `
In your `tmp/cordis-tutorial` directory (see [setup](index.md#setup)), create `hello.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
@@ -55,7 +55,7 @@ There is no framework bootstrap code in your file: a plugin describes what it co
A function is the most common form, but Cordis accepts three:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
// 1. Function plugin (what you just wrote).
export function apply(ctx: Context) {}
@@ -92,4 +92,4 @@ One caveat worth knowing early: a config entry whose module cannot be **resolved
Next: [Lifecycle and effects](02-lifecycle-and-effects.md) — what happens when a plugin unloads.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/01-first-plugin.zh.md b/docs/cordis-tutorial/01-first-plugin.zh.md
index 69dedb898c..87cd7a9684 100644
--- a/docs/cordis-tutorial/01-first-plugin.zh.md
+++ b/docs/cordis-tutorial/01-first-plugin.zh.md
@@ -9,7 +9,7 @@
在 `tmp/cordis-tutorial` 目录中(参见[环境设置](index.md#setup))创建 `hello.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
@@ -55,7 +55,7 @@ hello from my first plugin
函数是最常见的形式,但 Cordis 接受三种形式:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
// 1. Function plugin (what you just wrote).
export function apply(ctx: Context) {}
@@ -92,4 +92,4 @@ export function apply(ctx: Context) {
下一章:[生命周期与 effect](02-lifecycle-and-effects.md):插件卸载时会发生什么。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml b/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
index 12793267e2..26ffcac268 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
@@ -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 docs/cordis-tutorial/02-lifecycle-and-effects.md
-02-lifecycle-and-effects.md: 7b195b63a1e8730f27b9dd9af8af6a68a588cee9
-02-lifecycle-and-effects.zh.md: 4a3f83dedd5c95c7fcb5c1aebbbb8cb2e849b9cf
+02-lifecycle-and-effects.md: 8e75708eb0cba1aceb7fa3dae5ae334995eb113f
+02-lifecycle-and-effects.zh.md: bc4489e702ab58b5d1efd8a72fc693ee48af2624
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.md b/docs/cordis-tutorial/02-lifecycle-and-effects.md
index 7b195b63a1..8e75708eb0 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.md
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.md
@@ -11,7 +11,7 @@ For a resource Cordis does not already manage — a timer, a connection, a watch
Create `lifecycle.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
@@ -95,4 +95,4 @@ One ordering caveat: disposers start in reverse registration order, but multiple
Next: [Services](03-services.md) — how plugins share capabilities.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md b/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
index 4a3f83dedd..bc4489e702 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
@@ -11,7 +11,7 @@ Cordis 插件可能因修改配置、热重载、显式资源释放或所需服
创建 `lifecycle.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
@@ -95,4 +95,4 @@ PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
下一章:[服务](03-services.md):插件如何共享功能。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/03-services.i18n.yaml b/docs/cordis-tutorial/03-services.i18n.yaml
index bdb7e19387..372e028a47 100644
--- a/docs/cordis-tutorial/03-services.i18n.yaml
+++ b/docs/cordis-tutorial/03-services.i18n.yaml
@@ -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 docs/cordis-tutorial/03-services.md
-03-services.md: 82b08b7b8a2ec8a6b340dd1fdc7fa3de98cedff9
-03-services.zh.md: ba4152454eb79a21b183b867c0ba2ef32cd43923
+03-services.md: 32007284be99ef46b4621089c9b3a80317e77189
+03-services.zh.md: d82be29aa69686b8dc10cc6a45a658683c017cbd
diff --git a/docs/cordis-tutorial/03-services.md b/docs/cordis-tutorial/03-services.md
index 82b08b7b8a..32007284be 100644
--- a/docs/cordis-tutorial/03-services.md
+++ b/docs/cordis-tutorial/03-services.md
@@ -9,9 +9,9 @@ A **service** is a named capability one plugin provides and other plugins consum
Create `greeter.ts` in `tmp/cordis-tutorial`:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
Two pieces work together:
- **Runtime**: `super(ctx, 'greeter')` registers the instance under the name `greeter`. From then on, any plugin can reach it as `ctx.greeter`. The registration is an effect — unloading the provider removes the service.
-- **Compile time**: the `declare module 'cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety.
+- **Compile time**: the `declare module '@deepseek-ai/cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety.
A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx.plugin(GreeterService)` mounts it like any other.
@@ -46,7 +46,7 @@ A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx
Create `consumer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
@@ -95,4 +95,4 @@ Service names live in one flat namespace per application. Prefix or namespace yo
Next: [Events](04-events.md) — communication without a shared service.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/03-services.zh.md b/docs/cordis-tutorial/03-services.zh.md
index ba4152454e..d82be29aa6 100644
--- a/docs/cordis-tutorial/03-services.zh.md
+++ b/docs/cordis-tutorial/03-services.zh.md
@@ -9,9 +9,9 @@
创建 `greeter.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
两部分协同工作:
- **运行时**:`super(ctx, 'greeter')` 以名称 `greeter` 注册该实例。此后,任何插件都可以通过 `ctx.greeter` 访问它。注册属于 effect,卸载提供方时会移除该服务。
-- **编译时**:`declare module 'cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
+- **编译时**:`declare module '@deepseek-ai/cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
`Service` 子类本身就是插件(第 1 章介绍的类形态),因此 `ctx.plugin(GreeterService)` 会像挂载其他插件一样挂载它。
@@ -46,7 +46,7 @@ export function apply(ctx: Context) {
创建 `consumer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
@@ -95,4 +95,4 @@ export function apply(ctx: Context) {
下一章:[事件](04-events.md):无需共享服务即可通信。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/04-events.i18n.yaml b/docs/cordis-tutorial/04-events.i18n.yaml
index b453ffb1f6..4ce3fb9a65 100644
--- a/docs/cordis-tutorial/04-events.i18n.yaml
+++ b/docs/cordis-tutorial/04-events.i18n.yaml
@@ -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 docs/cordis-tutorial/04-events.md
-04-events.md: e77641dffcb82fcb50a24ca2e3e764d152218094
-04-events.zh.md: 00cce854e9a54fddb594ffa8e306f60a725ac012
+04-events.md: db911b9a06d7304d73030a2020de3d465fde2f9c
+04-events.zh.md: e0357b2c04785d52a54f839d1b8ff3ecccb027ed
diff --git a/docs/cordis-tutorial/04-events.md b/docs/cordis-tutorial/04-events.md
index e77641dffc..db911b9a06 100644
--- a/docs/cordis-tutorial/04-events.md
+++ b/docs/cordis-tutorial/04-events.md
@@ -9,9 +9,9 @@ Services support direct calls; **events** let a plugin announce something withou
Create `stats.ts` in `tmp/cordis-tutorial` — a service that counts things and announces each change:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
@@ -46,7 +46,7 @@ The `interface Events` merge is the event-system twin of the `interface Context`
Create `reporter.ts`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
@@ -96,9 +96,9 @@ Every harness event documents its mode in the generated reference on its owning
Waterfall is the mode that powers interception. Each listener receives the arguments plus a `next()` continuation; it can transform what `next()` returns, or return without calling `next()` and short-circuit the rest of the chain — what the Cordis docs call the veto. Create `waterfall-demo.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise): Promise
}
@@ -141,4 +141,4 @@ The harness uses waterfalls for decisions that cooperating plugins may wrap or a
Next: [Configuration](05-config.md) — plugin options from `cordis.yml`.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/04-events.zh.md b/docs/cordis-tutorial/04-events.zh.md
index 00cce854e9..e0357b2c04 100644
--- a/docs/cordis-tutorial/04-events.zh.md
+++ b/docs/cordis-tutorial/04-events.zh.md
@@ -9,9 +9,9 @@
创建 `stats.ts`,将它放在 `tmp/cordis-tutorial` 中。它是一项负责计数并在每次变化时发出通知的服务:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
@@ -46,7 +46,7 @@ export function apply(ctx: Context) {
创建 `reporter.ts`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
@@ -96,9 +96,9 @@ export function apply(ctx: Context) {
waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 `next()` continuation;它可以转换 `next()` 的返回值,也可以不调用 `next()` 就直接返回,从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 `waterfall-demo.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise): Promise
}
@@ -141,4 +141,4 @@ harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`ag
下一章:[配置](05-config.md):来自 `cordis.yml` 的插件选项。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/05-config.i18n.yaml b/docs/cordis-tutorial/05-config.i18n.yaml
index 7db45165c4..4e953918dd 100644
--- a/docs/cordis-tutorial/05-config.i18n.yaml
+++ b/docs/cordis-tutorial/05-config.i18n.yaml
@@ -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 docs/cordis-tutorial/05-config.md
-05-config.md: 834bb140cc1ff976acc8f21c8f54a7fb02636eac
-05-config.zh.md: f5cc6ac1ca4fa02eba6a1b015b9f6ae3b1a925fc
+05-config.md: 2357f663135d6fc78a65f9d0952e0bc3f5eefae4
+05-config.zh.md: fbd94d179494ad0b6f73baff2ca525c786cc9e33
diff --git a/docs/cordis-tutorial/05-config.md b/docs/cordis-tutorial/05-config.md
index 834bb140cc..2357f66313 100644
--- a/docs/cordis-tutorial/05-config.md
+++ b/docs/cordis-tutorial/05-config.md
@@ -9,8 +9,8 @@ Each `cordis.yml` entry can carry a `config` block, and the plugin declares a sc
Create `config-demo.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
@@ -81,4 +81,4 @@ The loader used in this repo supports a `!!js` tag for config values that must b
Next: [Composition and HMR](06-composition-and-hmr.md) — treating `cordis.yml` as the application.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/05-config.zh.md b/docs/cordis-tutorial/05-config.zh.md
index f5cc6ac1ca..fbd94d1794 100644
--- a/docs/cordis-tutorial/05-config.zh.md
+++ b/docs/cordis-tutorial/05-config.zh.md
@@ -9,8 +9,8 @@
创建 `config-demo.ts`,并将其放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
@@ -81,4 +81,4 @@ ValidationError: invalid config:
下一章:[组合与 HMR(热模块替换)](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml b/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
index 3732651e58..6a79850803 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
+++ b/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
@@ -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 docs/cordis-tutorial/06-composition-and-hmr.md
-06-composition-and-hmr.md: a169d7a164be63c939e352e4e5b0bf9bce43da29
-06-composition-and-hmr.zh.md: 07ae46555c390d625a4397933e2ec5ac059bd270
+06-composition-and-hmr.md: 87ea26014657ae8c8199e1ebb486556c827d96ca
+06-composition-and-hmr.zh.md: 830f55de7c1be351fe701cb068197543602619a7
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.md b/docs/cordis-tutorial/06-composition-and-hmr.md
index a169d7a164..87ea260146 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.md
@@ -22,24 +22,24 @@ Groups nest a sub-list of entries that load and unload as one unit, and `isolate
## Hot module replacement
-Because unloading releases effects ([chapter 2](02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@cordisjs/plugin-hmr` plugin watches your files and does exactly that on save.
+Because unloading releases effects ([chapter 2](02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@deepseek-ai/cordis-plugin-hmr` plugin watches your files and does exactly that on save.
In `tmp/cordis-tutorial`, write `cordis.yml`:
```yaml
- id: logger
- name: '@cordisjs/plugin-logger-console'
+ name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
- name: '@cordisjs/plugin-timer'
+ name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
- name: '@cordisjs/plugin-hmr'
+ name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
```
-Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@cordisjs/plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section.
+Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@deepseek-ai/cordis-plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section.
HMR reads Node's loader internals through the Loader's native helper. Run Cordis under tsx:
@@ -65,7 +65,7 @@ The flip side of dependency-driven loading: a plugin whose `inject` names a serv
You can see the states directly. Every context can enumerate the plugin registry; create `diagnose.ts`:
```ts
-import { FiberState, type Context } from 'cordis'
+import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
@@ -85,7 +85,7 @@ export function apply(ctx: Context) {
And a plugin with an unsatisfiable dependency, `needs-timer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'needs-timer'
export const inject = ['timer']
@@ -106,8 +106,8 @@ Run it (plain `node --import tsx ../../vendor/cordis/bin.js`; stop with Ctrl-C):
needs-timer is PENDING — a required service is missing
```
-`inject: ['timer']` has no provider. Add `- name: '@cordisjs/plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself.
+`inject: ['timer']` has no provider. Add `- name: '@deepseek-ai/cordis-plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself.
Next: [Into the harness](07-into-the-harness.md) — the same patterns against real harness services.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.zh.md b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
index 07ae46555c..830f55de7c 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.zh.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
@@ -22,24 +22,24 @@ Cordis 配置项除了 `name` 和 `config`,还接受其他元数据:
## 热模块替换
-卸载会释放 effect([第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@cordisjs/plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
+卸载会释放 effect([第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@deepseek-ai/cordis-plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
在 `tmp/cordis-tutorial` 中编写 `cordis.yml`:
```yaml
- id: logger
- name: '@cordisjs/plugin-logger-console'
+ name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
- name: '@cordisjs/plugin-timer'
+ name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
- name: '@cordisjs/plugin-hmr'
+ name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
```
-列表中增加了两个辅助插件:HMR 通过 Cordis logger 服务记录日志,因此没有控制台导出器时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@cordisjs/plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
+列表中增加了两个辅助插件:HMR 通过 Cordis logger 服务记录日志,因此没有控制台导出器时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@deepseek-ai/cordis-plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
HMR 通过 Loader 的原生辅助工具读取 Node 的 loader 内部结构。请在 tsx 下运行 Cordis:
@@ -65,7 +65,7 @@ hello from my EDITED plugin
你可以直接查看这些状态。每个上下文都能枚举插件注册表;创建 `diagnose.ts`:
```ts
-import { FiberState, type Context } from 'cordis'
+import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
@@ -85,7 +85,7 @@ export function apply(ctx: Context) {
再创建一个依赖无法满足的插件 `needs-timer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'needs-timer'
export const inject = ['timer']
@@ -106,8 +106,8 @@ export function apply(ctx: Context) {
needs-timer is PENDING — a required service is missing
```
-`inject: ['timer']` 没有提供方。向列表添加 `- name: '@cordisjs/plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。
+`inject: ['timer']` 没有提供方。向列表添加 `- name: '@deepseek-ai/cordis-plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。
下一章:[进入 harness](07-into-the-harness.md):把相同模式用于真实的 harness 服务。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml b/docs/cordis-tutorial/07-into-the-harness.i18n.yaml
index 8fb1893fca..fbc6ef1cde 100644
--- a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml
+++ b/docs/cordis-tutorial/07-into-the-harness.i18n.yaml
@@ -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 docs/cordis-tutorial/07-into-the-harness.md
-07-into-the-harness.md: 69133786f58541b015aed080f4ac8fb2a7e488c0
-07-into-the-harness.zh.md: bc9c61da984e3eb691eb6bfbe59ae556823e82de
+07-into-the-harness.md: 2d3c23f9f7f7fc6bd6cabd4e7e68ebfc46e20665
+07-into-the-harness.zh.md: 45dc0ee6f07ab3b0275499cd4ab0c436eebdddef
diff --git a/docs/cordis-tutorial/07-into-the-harness.md b/docs/cordis-tutorial/07-into-the-harness.md
index 69133786f5..2d3c23f9f7 100644
--- a/docs/cordis-tutorial/07-into-the-harness.md
+++ b/docs/cordis-tutorial/07-into-the-harness.md
@@ -9,7 +9,7 @@ This chapter registers a model-callable tool with the harness's `tools` service,
Create `greet-tool.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
@@ -53,7 +53,7 @@ Every pattern here is from the earlier chapters: `inject: ['tools']` ([chapter 3
Create `tool-logger.ts` — a separate plugin that watches every tool call in the app through the harness's `tools/result` event:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
@@ -104,4 +104,4 @@ Where to go next:
- The generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) — everything you can inject and listen to, each on its owning page.
- [Architecture](../architecture.md) — the system map these plugins live in.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/07-into-the-harness.zh.md b/docs/cordis-tutorial/07-into-the-harness.zh.md
index bc9c61da98..45dc0ee6f0 100644
--- a/docs/cordis-tutorial/07-into-the-harness.zh.md
+++ b/docs/cordis-tutorial/07-into-the-harness.zh.md
@@ -9,7 +9,7 @@
创建 `greet-tool.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
@@ -53,7 +53,7 @@ export function apply(ctx: Context) {
创建 `tool-logger.ts`。这是一个独立插件,通过 harness 的 `tools/result` 事件观察应用中的每次工具调用:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
@@ -104,4 +104,4 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,发生在
- [子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块:可以注入和监听的所有内容,各在其所属页面上。
- [架构](../architecture.md):这些插件所处的系统地图。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/index.i18n.yaml b/docs/cordis-tutorial/index.i18n.yaml
index 719e949ffe..06857ab177 100644
--- a/docs/cordis-tutorial/index.i18n.yaml
+++ b/docs/cordis-tutorial/index.i18n.yaml
@@ -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 docs/cordis-tutorial/index.md
-index.md: 307c12854b3075cfd4dd5ea8a19806c58b4e998d
-index.zh.md: a0107b7d15272e6ef8d526b9c0e03a99275644d6
+index.md: fb700344e6d07d3864655009d2edac15ee9eede8
+index.zh.md: a68e931d81e745164d8f9a5dc7ec9aec4cd0e590
diff --git a/docs/cordis-tutorial/index.md b/docs/cordis-tutorial/index.md
index 307c12854b..fb700344e6 100644
--- a/docs/cordis-tutorial/index.md
+++ b/docs/cordis-tutorial/index.md
@@ -13,7 +13,7 @@ If you want the condensed concept reference instead of a walkthrough, read the [
You need a clone of this repository with dependencies installed — the [quick start](../user/guide/quickstart.md) covers prerequisites. No API key is needed for this tutorial; every example runs keylessly.
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -50,9 +50,9 @@ That one-file launcher (see [vendor/cordis/bin.js](../../vendor/cordis/bin.js))
The examples use three TypeScript features beyond ordinary modern JavaScript:
- **Type annotations** describe values without changing runtime behavior: `ctx: Context` says that `ctx` has the Cordis context API, `who: string` accepts text, and `string[]` means an array of strings.
-- **`import type { Context } from 'cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency.
-- **Declaration merging** (`declare module 'cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full.
+- **`import type { Context } from '@deepseek-ai/cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency.
+- **Declaration merging** (`declare module '@deepseek-ai/cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full.
Chapter 5 also uses an `interface` to describe a configuration object's fields and a generic type such as `Schema` to say which object fields a schema validates. You can copy those declarations as shown; the surrounding text explains what each one connects.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/index.zh.md b/docs/cordis-tutorial/index.zh.md
index a0107b7d15..a68e931d81 100644
--- a/docs/cordis-tutorial/index.zh.md
+++ b/docs/cordis-tutorial/index.zh.md
@@ -13,7 +13,7 @@ Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行
你需要克隆本仓库并安装依赖,具体前置条件见[快速入门](../user/guide/quickstart.md)。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -50,9 +50,9 @@ node --import tsx ../../vendor/cordis/bin.js
这些示例使用了普通现代 JavaScript 之外的三项 TypeScript 功能:
- **类型注解** 描述值,但不会改变运行时行为:`ctx: Context` 表示 `ctx` 具备 Cordis 上下文 API,`who: string` 接受文本,而 `string[]` 表示字符串数组。
-- **`import type { Context } from 'cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
-- **声明合并**(`declare module 'cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
+- **`import type { Context } from '@deepseek-ai/cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
+- **声明合并**(`declare module '@deepseek-ai/cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
第 5 章还会使用 `interface` 描述配置对象的字段,并使用 `Schema` 这类泛型表示 schema 校验哪些对象字段。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml
index da29debe08..9d8d8fba47 100644
--- a/docs/development.i18n.yaml
+++ b/docs/development.i18n.yaml
@@ -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 docs/development.md
-development.md: 8e565f21c6e2ede7dab7dbda3c4b18b77ce0920f
-development.zh.md: d9c0fbfbb663334b8f7e2ca11d4e8d9a0652c22e
+development.md: 7d6ca74df1311560b0d61444c92e74927639ca31
+development.zh.md: 1461717cf496b1863f2f0c47ad4b42990dc789e9
diff --git a/docs/development.md b/docs/development.md
index 8e565f21c6..7d6ca74df1 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -129,7 +129,7 @@ The root [contributor instructions](../AGENTS.md#commands) summarize common comm
The one-shot Headless coding agent needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:
```sh
-pnpm run demo:headless "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
The self-referential cordis demo can inspect and modify its live plugin runtime and needs the same credentials (`web` by default, or `acp`):
diff --git a/docs/development.zh.md b/docs/development.zh.md
index d9c0fbfbb6..1461717cf4 100644
--- a/docs/development.zh.md
+++ b/docs/development.zh.md
@@ -129,7 +129,7 @@ keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若
单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:
```sh
-pnpm run demo:headless "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index dfe221ce7f..6c8cfbdee0 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/docs/event-producer-consumer.i18n.yaml
@@ -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 docs/event-producer-consumer.md
-event-producer-consumer.md: e22d7704c3125b731c5270fc43405258db26cf4f
-event-producer-consumer.zh.md: 15aed3649532e41be986dcd4d56fccc2a8c891ca
+event-producer-consumer.md: d47142bfc3e0ea1430dcbc87b6f01d614d8a4b12
+event-producer-consumer.zh.md: 929fe71dfb109f8360ffa6771bb9b267a7f0ab1c
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index e22d7704c3..d47142bfc3 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -8,7 +8,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
@@ -30,19 +30,19 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
-| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
+| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:136`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
-| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:146`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:157`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:193`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 15aed36495..929fe71dfb 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -10,7 +10,7 @@
| 事件 | 模式 | 声明位置 | 派发方 | 监听方 |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
@@ -32,19 +32,19 @@
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
-| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
+| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:136`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
-| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:193`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
diff --git a/docs/glossary.i18n.yaml b/docs/glossary.i18n.yaml
index 1dfb89b07f..5c60b984da 100644
--- a/docs/glossary.i18n.yaml
+++ b/docs/glossary.i18n.yaml
@@ -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 docs/glossary.md
-glossary.md: d556e760dd2f07ab640aafca06b2757b47935406
+glossary.md: 77c6c729417968c683d0128699804c6ca3a7fee7
glossary.zh.md: d6a8e423ff2a6324ce0cdd65dd3edde573fac5b1
diff --git a/docs/glossary.md b/docs/glossary.md
index d556e760dd..77c6c72941 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -16,7 +16,7 @@ Domain vocabulary for the DeepSeek Harness SDK uses one canonical term per conce
- **scope carrier** — the `thisArg` a scope-filtered dispatch carries (built by `scopeTarget`); its filter admits untagged listeners plus the subject's own. A *subject-less* carrier (no key) admits untagged listeners only.
- **scoped dispatch** — the rule: an event about one agent's activity dispatches with that agent's carrier. Events about a registry itself (a tool was added) are *registry-subject* and stay unfiltered.
- **shadowing** — most-specific-wins name resolution: a scoped tool/section/variable replaces its same-named global twin for that scope alone. The per-agent persona and per-agent tool-variant mechanism.
-- **restriction / scope-local registration** — a restriction (`tools.restrict`) filters the GLOBAL tool surface for one scope (compose by intersection); scope-local registrations are merged after that filter. A filtered-away global tool is absent from the prompt AND refuses execution, indistinguishably from a nonexistent one.
+- **restriction / scope-local registration** — a restriction (`tools.restrict`) filters the GLOBAL tool set for one scope (compose by intersection); scope-local registrations are merged after that filter. A filtered-away global tool is absent from the prompt AND refuses execution, indistinguishably from a nonexistent one.
- **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/session-start` fires, or the first prompt is assembled. Setup registers; it never drives the agent.
- **lineage** — parent/child facts carried as data (`parentSession`, durable `delegationDepth`, runtime `subagentDepth`); never affects visibility.
diff --git a/docs/graph-atlas.i18n.yaml b/docs/graph-atlas.i18n.yaml
index 09b1b58178..c697f8e1c1 100644
--- a/docs/graph-atlas.i18n.yaml
+++ b/docs/graph-atlas.i18n.yaml
@@ -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 docs/graph-atlas.md
-graph-atlas.md: bf2aeba1210709cdda68e0ea7d611528f3191744
+graph-atlas.md: 1da995e8ff6b47e0a83f0308342c5f45ccbd3835
graph-atlas.zh.md: 780e5295f74f10ee4fe762e280e5c1c820c481c4
diff --git a/docs/graph-atlas.md b/docs/graph-atlas.md
index bf2aeba121..1da995e8ff 100644
--- a/docs/graph-atlas.md
+++ b/docs/graph-atlas.md
@@ -3,7 +3,7 @@
# Documentation Graph Index
-These diagrams show relationships that the generated catalogs do not. Use them to find package relationships, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type definitions still live in the [subsystem pages](subsystems/core.md) (types + the generated `cordis-surface` regions) and [tool-catalog.md](tool-catalog.md).
+These diagrams show relationships that the generated catalogs do not. Use them to find package relationships, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type definitions still live in the [subsystem pages](subsystems/core.md) (types + the generated Cordis API regions) and [tool-catalog.md](tool-catalog.md).
The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).
diff --git a/docs/i18n/style-samples.md b/docs/i18n/style-samples.md
index eb8a4b673b..61ef77065d 100644
--- a/docs/i18n/style-samples.md
+++ b/docs/i18n/style-samples.md
@@ -6,7 +6,7 @@
## ① 架构叙述
-> This document describes the architecture of the DeepSeek Harness — the foundation of **DeepSeek Code**. The governing principle, from the microkernel design discussion: **everything is a plugin**. The core is deliberately tiny — a handful of abstract services plus one concrete loop plugin (`dsh-agent-loop`) — and every product feature is a plugin against the extension surface described here, without modifying the loop.
+> This document describes the architecture of the DeepSeek Harness — the foundation of **DeepSeek Code**. The governing principle, from the microkernel design discussion: **everything is a plugin**. The core is deliberately tiny — a handful of abstract services plus one concrete loop plugin (`dsh-agent-loop`) — and every product feature is a plugin against the extension API described here, without modifying the loop.
本文介绍 DeepSeek Harness 整体架构,它是 **DeepSeek Code** 的底层基座。微内核设计讨论中确立了核心设计准则:**一切皆插件**。内核刻意做得极精简,仅包含少量抽象服务,外加一个实体循环插件 `dsh-agent-loop`。所有产品功能均基于本文定义的扩展接口开发为独立插件,无需改动主循环逻辑。
diff --git a/docs/i18n/terminology.md b/docs/i18n/terminology.md
index 51ec3673a6..a4d9ce43fa 100644
--- a/docs/i18n/terminology.md
+++ b/docs/i18n/terminology.md
@@ -167,7 +167,7 @@
| same-world subprocess | 与宿主共享文件系统和内核的子进程 | | 同世界子进程 | |
| sandbox | 沙箱 | | | |
| service | 服务 | | | |
-| serving surface | 对外服务接口 | | | |
+| serving interface | 对外服务接口 | | | |
| session | 会话 | | | |
| session event | 会话事件 | | | |
| setup card | 设置卡片 | | | 首次运行时代替行卡直接展开的配置卡 |
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index 8534a28b4d..0d0f7bee09 100644
--- a/docs/module-graph.i18n.yaml
+++ b/docs/module-graph.i18n.yaml
@@ -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 docs/module-graph.md
-module-graph.md: de3441552adbbfedea605046141935e0bb92ff86
-module-graph.zh.md: 8016d0f006baf1aedb852a49202d59c408cbaad7
+module-graph.md: 91c202f0dae1bfbe7b5801f4e9f666e9d8866d87
+module-graph.zh.md: 2609c1a961133567344a8e1ba65bf21a526a62f5
diff --git a/docs/module-graph.md b/docs/module-graph.md
index de3441552a..91c202f0da 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -133,6 +133,7 @@ flowchart TD
end
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
+ pkg_cmdline["cmdline"]
end
subgraph group_bundle["packages/bundle"]
pkg_base["base"]
@@ -166,6 +167,7 @@ flowchart TD
pkg_client_ui_slash["client-ui-slash"]
pkg_client_ui_slots["client-ui-slots"]
pkg_client_ui_subagent["client-ui-subagent"]
+ pkg_client_ui_task["client-ui-task"]
pkg_client_ui_theme["client-ui-theme"]
pkg_client_ui_tool["client-ui-tool"]
pkg_client_ui_trajectory["client-ui-trajectory"]
@@ -200,6 +202,7 @@ flowchart TD
end
subgraph group_feedback["packages/feedback"]
pkg_command_feedback["command-feedback"]
+ pkg_message_feedback["message-feedback"]
end
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
@@ -245,16 +248,12 @@ flowchart TD
pkg_sandbox_policy["sandbox-policy"]
pkg_sandbox_windows_acl["sandbox-windows-acl"]
end
- subgraph group_scaffold["packages/scaffold"]
- pkg_helper["helper"]
+ subgraph group_sdk["packages/sdk"]
pkg_jsonrpc["jsonrpc"]
- pkg_scripts["scripts"]
pkg_sdk_client["sdk-client"]
pkg_sdk_protocol["sdk-protocol"]
- pkg_telemetry["telemetry"]
end
subgraph group_self_modification["packages/self-modification"]
- pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -315,6 +314,7 @@ flowchart TD
pkg_timeout --> pkg_invariants
pkg_scope --> pkg_invariants
pkg_llm_mock_server --> pkg_invariants
+ pkg_cmdline --> pkg_invariants
pkg_base --> pkg_invariants
pkg_client_modules --> pkg_invariants
pkg_client_schema_form --> pkg_invariants
@@ -351,12 +351,6 @@ flowchart TD
pkg_subprocess_e2b --> pkg_timeout
pkg_frontend_static --> pkg_host_webserver
pkg_frontend_static --> pkg_invariants
- pkg_helper --> pkg_brand
- pkg_helper --> pkg_invariants
- pkg_helper --> pkg_subprocess
- pkg_telemetry --> pkg_brand
- pkg_telemetry --> pkg_invariants
- pkg_telemetry --> pkg_paths
pkg_user_id --> pkg_brand
pkg_user_id --> pkg_invariants
pkg_user_id --> pkg_paths
@@ -399,9 +393,6 @@ flowchart TD
pkg_client_ui_settings --> pkg_client_ui_primitives
pkg_client_ui_settings --> pkg_client_ui_slots
pkg_client_ui_settings --> pkg_invariants
- pkg_client_ui_trajectory --> pkg_client_runtime
- pkg_client_ui_trajectory --> pkg_client_ui_primitives
- pkg_client_ui_trajectory --> pkg_invariants
pkg_credentials_local --> pkg_atomic_write
pkg_credentials_local --> pkg_credentials
pkg_credentials_local --> pkg_environment
@@ -462,6 +453,7 @@ flowchart TD
pkg_client_ui_theme --> pkg_client_runtime
pkg_client_ui_theme --> pkg_client_ui_primitives
pkg_client_ui_theme --> pkg_client_ui_slots
+ pkg_client_ui_theme --> pkg_host_webserver
pkg_client_ui_theme --> pkg_invariants
pkg_client_ui_workspace --> pkg_client_locale
pkg_client_ui_workspace --> pkg_client_runtime
@@ -516,12 +508,6 @@ flowchart TD
pkg_host_directory_picker_native --> pkg_client_ui_slots
pkg_host_directory_picker_native --> pkg_client_ui_workspace
pkg_host_directory_picker_native --> pkg_invariants
- pkg_agent_presets --> pkg_atomic_write
- pkg_agent_presets --> pkg_invariants
- pkg_agent_presets --> pkg_paths
- pkg_agent_presets --> pkg_scope
- pkg_agent_presets --> pkg_session
- pkg_agent_presets --> pkg_settings
pkg_persona --> pkg_invariants
pkg_persona --> pkg_system_prompt
pkg_sandbox --> pkg_invariants
@@ -573,8 +559,13 @@ flowchart TD
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
- pkg_host_apiproxy --> pkg_agent_presets
- pkg_host_apiproxy --> pkg_invariants
+ pkg_message_feedback --> pkg_brand
+ pkg_message_feedback --> pkg_invariants
+ pkg_message_feedback --> pkg_llm
+ pkg_message_feedback --> pkg_session
+ pkg_message_feedback --> pkg_session_persistence
+ pkg_message_feedback --> pkg_storage_domain
+ pkg_message_feedback --> pkg_type_meta
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
pkg_host_directory_picker_auto --> pkg_host_webserver
@@ -594,6 +585,14 @@ flowchart TD
pkg_user_interaction --> pkg_agent
pkg_user_interaction --> pkg_invariants
pkg_user_interaction --> pkg_llm
+ pkg_agent_presets --> pkg_agent
+ pkg_agent_presets --> pkg_atomic_write
+ pkg_agent_presets --> pkg_invariants
+ pkg_agent_presets --> pkg_paths
+ pkg_agent_presets --> pkg_scope
+ pkg_agent_presets --> pkg_session
+ pkg_agent_presets --> pkg_settings
+ pkg_agent_presets --> pkg_system_prompt
pkg_pty --> pkg_agent
pkg_pty --> pkg_brand
pkg_pty --> pkg_invariants
@@ -606,8 +605,6 @@ flowchart TD
pkg_sandbox_policy --> pkg_sandbox
pkg_sandbox_policy --> pkg_session
pkg_sandbox_policy --> pkg_system_prompt
- pkg_scripts --> pkg_app_boot
- pkg_scripts --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
@@ -703,11 +700,6 @@ flowchart TD
pkg_headless --> pkg_invariants
pkg_headless --> pkg_llm
pkg_headless --> pkg_session
- pkg_client_test_runtime --> pkg_client_runtime
- pkg_client_test_runtime --> pkg_client_ui_slots
- pkg_client_test_runtime --> pkg_client_web_react
- pkg_client_test_runtime --> pkg_host_apiproxy
- pkg_client_test_runtime --> pkg_invariants
pkg_tmux_context --> pkg_agent
pkg_tmux_context --> pkg_bash
pkg_tmux_context --> pkg_invariants
@@ -718,7 +710,10 @@ flowchart TD
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_invariants
pkg_command_feedback --> pkg_session
+ pkg_command_feedback --> pkg_session_telemetry
pkg_command_feedback --> pkg_user_id
+ pkg_host_apiproxy --> pkg_agent_presets
+ pkg_host_apiproxy --> pkg_invariants
pkg_permission --> pkg_bash
pkg_permission --> pkg_commands
pkg_permission --> pkg_invariants
@@ -794,6 +789,7 @@ flowchart TD
pkg_fs_sandbox --> pkg_invariants
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_tool_fs --> pkg_attachment
pkg_tool_fs --> pkg_fs
pkg_tool_fs --> pkg_invariants
pkg_tool_fs --> pkg_llm
@@ -830,6 +826,8 @@ flowchart TD
pkg_subagent --> pkg_brand
pkg_subagent --> pkg_invariants
pkg_subagent --> pkg_llm
+ pkg_subagent --> pkg_sandbox
+ pkg_subagent --> pkg_sandbox_policy
pkg_subagent --> pkg_scope
pkg_subagent --> pkg_session
pkg_subagent --> pkg_session_persistence
@@ -837,6 +835,7 @@ flowchart TD
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_tasks
pkg_subagent --> pkg_tools
+ pkg_subagent --> pkg_user_approval
pkg_tool_web --> pkg_invariants
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
@@ -890,6 +889,18 @@ flowchart TD
pkg_llm_replay --> pkg_invariants
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
+ pkg_client_test_runtime --> pkg_client_runtime
+ pkg_client_test_runtime --> pkg_client_ui_slots
+ pkg_client_test_runtime --> pkg_client_web_react
+ pkg_client_test_runtime --> pkg_host_apiproxy
+ pkg_client_test_runtime --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_agent
+ pkg_client_ui_trajectory --> pkg_client_locale
+ pkg_client_ui_trajectory --> pkg_client_runtime
+ pkg_client_ui_trajectory --> pkg_client_ui_primitives
+ pkg_client_ui_trajectory --> pkg_compact
+ pkg_client_ui_trajectory --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compact
pkg_session_reference --> pkg_invariants
@@ -924,6 +935,7 @@ flowchart TD
pkg_mcp_client --> pkg_invariants
pkg_mcp_client --> pkg_llm
pkg_mcp_client --> pkg_subprocess
+ pkg_mcp_client --> pkg_timeout
pkg_mcp_client --> pkg_tools
pkg_tool_bash_persistent --> pkg_agent
pkg_tool_bash_persistent --> pkg_invariants
@@ -1020,12 +1032,10 @@ flowchart TD
pkg_subagent_inprocess --> pkg_agent
pkg_subagent_inprocess --> pkg_invariants
pkg_subagent_inprocess --> pkg_llm
- pkg_subagent_inprocess --> pkg_sandbox_policy
pkg_subagent_inprocess --> pkg_session
pkg_subagent_inprocess --> pkg_subagent
pkg_subagent_inprocess --> pkg_system_prompt
pkg_subagent_inprocess --> pkg_tools
- pkg_subagent_inprocess --> pkg_user_approval
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_invariants
pkg_tool_subagent --> pkg_llm
@@ -1040,6 +1050,7 @@ flowchart TD
pkg_tool_subagent_report --> pkg_invariants
pkg_tool_subagent_report --> pkg_llm
pkg_tool_subagent_report --> pkg_subagent
+ pkg_tool_subagent_report --> pkg_system_prompt
pkg_tool_subagent_report --> pkg_tools
pkg_hooks_claude --> pkg_agent
pkg_hooks_claude --> pkg_hook_protocol
@@ -1071,10 +1082,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
- pkg_repository_plugin --> pkg_invariants
- pkg_repository_plugin --> pkg_mcp_client
- pkg_repository_plugin --> pkg_paths
- pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1160,6 +1167,12 @@ flowchart TD
pkg_client_ui_subagent --> pkg_invariants
pkg_client_ui_subagent --> pkg_subagent
pkg_client_ui_subagent --> pkg_token_meter
+ pkg_client_ui_task --> pkg_client_locale
+ pkg_client_ui_task --> pkg_client_runtime
+ pkg_client_ui_task --> pkg_client_ui_conversation
+ pkg_client_ui_task --> pkg_client_ui_primitives
+ pkg_client_ui_task --> pkg_client_ui_slots
+ pkg_client_ui_task --> pkg_invariants
pkg_client_ui_tool --> pkg_client_locale
pkg_client_ui_tool --> pkg_client_runtime
pkg_client_ui_tool --> pkg_client_ui_conversation
@@ -1267,6 +1280,7 @@ flowchart TD
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) |
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) |
| [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) |
+| [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/support/invariants) |
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) |
@@ -1292,8 +1306,6 @@ flowchart TD
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
-| [`helper`](../packages/scaffold/helper) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) |
-| [`telemetry`](../packages/scaffold/telemetry) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
@@ -1307,7 +1319,6 @@ flowchart TD
| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
@@ -1320,7 +1331,7 @@ flowchart TD
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
@@ -1335,7 +1346,6 @@ flowchart TD
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
@@ -1349,15 +1359,15 @@ flowchart TD
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
| [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
-| [`scripts`](../packages/scaffold/scripts) | `scaffold` | [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants) |
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
@@ -1380,10 +1390,10 @@ flowchart TD
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) |
+| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
+| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
@@ -1397,12 +1407,12 @@ flowchart TD
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`pwsh-sandbox`](../packages/bash/pwsh-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`pwsh-local`](../packages/bash/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
@@ -1412,13 +1422,15 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) |
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-bash-persistent`](../packages/pty/tool-bash-persistent) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) |
@@ -1433,19 +1445,18 @@ flowchart TD
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
+| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
-| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
+| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
@@ -1454,12 +1465,13 @@ flowchart TD
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
-| [`jsonrpc`](../packages/scaffold/server) | `scaffold` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`sdk-client`](../packages/scaffold/client) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session) |
-| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/scaffold/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
+| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
+| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
+| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) |
| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/support/invariants) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index 8016d0f006..2609c1a961 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -135,6 +135,7 @@ flowchart TD
end
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
+ pkg_cmdline["cmdline"]
end
subgraph group_bundle["packages/bundle"]
pkg_base["base"]
@@ -168,6 +169,7 @@ flowchart TD
pkg_client_ui_slash["client-ui-slash"]
pkg_client_ui_slots["client-ui-slots"]
pkg_client_ui_subagent["client-ui-subagent"]
+ pkg_client_ui_task["client-ui-task"]
pkg_client_ui_theme["client-ui-theme"]
pkg_client_ui_tool["client-ui-tool"]
pkg_client_ui_trajectory["client-ui-trajectory"]
@@ -202,6 +204,7 @@ flowchart TD
end
subgraph group_feedback["packages/feedback"]
pkg_command_feedback["command-feedback"]
+ pkg_message_feedback["message-feedback"]
end
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
@@ -247,16 +250,12 @@ flowchart TD
pkg_sandbox_policy["sandbox-policy"]
pkg_sandbox_windows_acl["sandbox-windows-acl"]
end
- subgraph group_scaffold["packages/scaffold"]
- pkg_helper["helper"]
+ subgraph group_sdk["packages/sdk"]
pkg_jsonrpc["jsonrpc"]
- pkg_scripts["scripts"]
pkg_sdk_client["sdk-client"]
pkg_sdk_protocol["sdk-protocol"]
- pkg_telemetry["telemetry"]
end
subgraph group_self_modification["packages/self-modification"]
- pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -317,6 +316,7 @@ flowchart TD
pkg_timeout --> pkg_invariants
pkg_scope --> pkg_invariants
pkg_llm_mock_server --> pkg_invariants
+ pkg_cmdline --> pkg_invariants
pkg_base --> pkg_invariants
pkg_client_modules --> pkg_invariants
pkg_client_schema_form --> pkg_invariants
@@ -353,12 +353,6 @@ flowchart TD
pkg_subprocess_e2b --> pkg_timeout
pkg_frontend_static --> pkg_host_webserver
pkg_frontend_static --> pkg_invariants
- pkg_helper --> pkg_brand
- pkg_helper --> pkg_invariants
- pkg_helper --> pkg_subprocess
- pkg_telemetry --> pkg_brand
- pkg_telemetry --> pkg_invariants
- pkg_telemetry --> pkg_paths
pkg_user_id --> pkg_brand
pkg_user_id --> pkg_invariants
pkg_user_id --> pkg_paths
@@ -401,9 +395,6 @@ flowchart TD
pkg_client_ui_settings --> pkg_client_ui_primitives
pkg_client_ui_settings --> pkg_client_ui_slots
pkg_client_ui_settings --> pkg_invariants
- pkg_client_ui_trajectory --> pkg_client_runtime
- pkg_client_ui_trajectory --> pkg_client_ui_primitives
- pkg_client_ui_trajectory --> pkg_invariants
pkg_credentials_local --> pkg_atomic_write
pkg_credentials_local --> pkg_credentials
pkg_credentials_local --> pkg_environment
@@ -464,6 +455,7 @@ flowchart TD
pkg_client_ui_theme --> pkg_client_runtime
pkg_client_ui_theme --> pkg_client_ui_primitives
pkg_client_ui_theme --> pkg_client_ui_slots
+ pkg_client_ui_theme --> pkg_host_webserver
pkg_client_ui_theme --> pkg_invariants
pkg_client_ui_workspace --> pkg_client_locale
pkg_client_ui_workspace --> pkg_client_runtime
@@ -518,12 +510,6 @@ flowchart TD
pkg_host_directory_picker_native --> pkg_client_ui_slots
pkg_host_directory_picker_native --> pkg_client_ui_workspace
pkg_host_directory_picker_native --> pkg_invariants
- pkg_agent_presets --> pkg_atomic_write
- pkg_agent_presets --> pkg_invariants
- pkg_agent_presets --> pkg_paths
- pkg_agent_presets --> pkg_scope
- pkg_agent_presets --> pkg_session
- pkg_agent_presets --> pkg_settings
pkg_persona --> pkg_invariants
pkg_persona --> pkg_system_prompt
pkg_sandbox --> pkg_invariants
@@ -575,8 +561,13 @@ flowchart TD
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
- pkg_host_apiproxy --> pkg_agent_presets
- pkg_host_apiproxy --> pkg_invariants
+ pkg_message_feedback --> pkg_brand
+ pkg_message_feedback --> pkg_invariants
+ pkg_message_feedback --> pkg_llm
+ pkg_message_feedback --> pkg_session
+ pkg_message_feedback --> pkg_session_persistence
+ pkg_message_feedback --> pkg_storage_domain
+ pkg_message_feedback --> pkg_type_meta
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
pkg_host_directory_picker_auto --> pkg_host_webserver
@@ -596,6 +587,14 @@ flowchart TD
pkg_user_interaction --> pkg_agent
pkg_user_interaction --> pkg_invariants
pkg_user_interaction --> pkg_llm
+ pkg_agent_presets --> pkg_agent
+ pkg_agent_presets --> pkg_atomic_write
+ pkg_agent_presets --> pkg_invariants
+ pkg_agent_presets --> pkg_paths
+ pkg_agent_presets --> pkg_scope
+ pkg_agent_presets --> pkg_session
+ pkg_agent_presets --> pkg_settings
+ pkg_agent_presets --> pkg_system_prompt
pkg_pty --> pkg_agent
pkg_pty --> pkg_brand
pkg_pty --> pkg_invariants
@@ -608,8 +607,6 @@ flowchart TD
pkg_sandbox_policy --> pkg_sandbox
pkg_sandbox_policy --> pkg_session
pkg_sandbox_policy --> pkg_system_prompt
- pkg_scripts --> pkg_app_boot
- pkg_scripts --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
@@ -705,11 +702,6 @@ flowchart TD
pkg_headless --> pkg_invariants
pkg_headless --> pkg_llm
pkg_headless --> pkg_session
- pkg_client_test_runtime --> pkg_client_runtime
- pkg_client_test_runtime --> pkg_client_ui_slots
- pkg_client_test_runtime --> pkg_client_web_react
- pkg_client_test_runtime --> pkg_host_apiproxy
- pkg_client_test_runtime --> pkg_invariants
pkg_tmux_context --> pkg_agent
pkg_tmux_context --> pkg_bash
pkg_tmux_context --> pkg_invariants
@@ -720,7 +712,10 @@ flowchart TD
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_invariants
pkg_command_feedback --> pkg_session
+ pkg_command_feedback --> pkg_session_telemetry
pkg_command_feedback --> pkg_user_id
+ pkg_host_apiproxy --> pkg_agent_presets
+ pkg_host_apiproxy --> pkg_invariants
pkg_permission --> pkg_bash
pkg_permission --> pkg_commands
pkg_permission --> pkg_invariants
@@ -796,6 +791,7 @@ flowchart TD
pkg_fs_sandbox --> pkg_invariants
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_tool_fs --> pkg_attachment
pkg_tool_fs --> pkg_fs
pkg_tool_fs --> pkg_invariants
pkg_tool_fs --> pkg_llm
@@ -832,6 +828,8 @@ flowchart TD
pkg_subagent --> pkg_brand
pkg_subagent --> pkg_invariants
pkg_subagent --> pkg_llm
+ pkg_subagent --> pkg_sandbox
+ pkg_subagent --> pkg_sandbox_policy
pkg_subagent --> pkg_scope
pkg_subagent --> pkg_session
pkg_subagent --> pkg_session_persistence
@@ -839,6 +837,7 @@ flowchart TD
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_tasks
pkg_subagent --> pkg_tools
+ pkg_subagent --> pkg_user_approval
pkg_tool_web --> pkg_invariants
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
@@ -892,6 +891,18 @@ flowchart TD
pkg_llm_replay --> pkg_invariants
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
+ pkg_client_test_runtime --> pkg_client_runtime
+ pkg_client_test_runtime --> pkg_client_ui_slots
+ pkg_client_test_runtime --> pkg_client_web_react
+ pkg_client_test_runtime --> pkg_host_apiproxy
+ pkg_client_test_runtime --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_agent
+ pkg_client_ui_trajectory --> pkg_client_locale
+ pkg_client_ui_trajectory --> pkg_client_runtime
+ pkg_client_ui_trajectory --> pkg_client_ui_primitives
+ pkg_client_ui_trajectory --> pkg_compact
+ pkg_client_ui_trajectory --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compact
pkg_session_reference --> pkg_invariants
@@ -926,6 +937,7 @@ flowchart TD
pkg_mcp_client --> pkg_invariants
pkg_mcp_client --> pkg_llm
pkg_mcp_client --> pkg_subprocess
+ pkg_mcp_client --> pkg_timeout
pkg_mcp_client --> pkg_tools
pkg_tool_bash_persistent --> pkg_agent
pkg_tool_bash_persistent --> pkg_invariants
@@ -1022,12 +1034,10 @@ flowchart TD
pkg_subagent_inprocess --> pkg_agent
pkg_subagent_inprocess --> pkg_invariants
pkg_subagent_inprocess --> pkg_llm
- pkg_subagent_inprocess --> pkg_sandbox_policy
pkg_subagent_inprocess --> pkg_session
pkg_subagent_inprocess --> pkg_subagent
pkg_subagent_inprocess --> pkg_system_prompt
pkg_subagent_inprocess --> pkg_tools
- pkg_subagent_inprocess --> pkg_user_approval
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_invariants
pkg_tool_subagent --> pkg_llm
@@ -1042,6 +1052,7 @@ flowchart TD
pkg_tool_subagent_report --> pkg_invariants
pkg_tool_subagent_report --> pkg_llm
pkg_tool_subagent_report --> pkg_subagent
+ pkg_tool_subagent_report --> pkg_system_prompt
pkg_tool_subagent_report --> pkg_tools
pkg_hooks_claude --> pkg_agent
pkg_hooks_claude --> pkg_hook_protocol
@@ -1073,10 +1084,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
- pkg_repository_plugin --> pkg_invariants
- pkg_repository_plugin --> pkg_mcp_client
- pkg_repository_plugin --> pkg_paths
- pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1162,6 +1169,12 @@ flowchart TD
pkg_client_ui_subagent --> pkg_invariants
pkg_client_ui_subagent --> pkg_subagent
pkg_client_ui_subagent --> pkg_token_meter
+ pkg_client_ui_task --> pkg_client_locale
+ pkg_client_ui_task --> pkg_client_runtime
+ pkg_client_ui_task --> pkg_client_ui_conversation
+ pkg_client_ui_task --> pkg_client_ui_primitives
+ pkg_client_ui_task --> pkg_client_ui_slots
+ pkg_client_ui_task --> pkg_invariants
pkg_client_ui_tool --> pkg_client_locale
pkg_client_ui_tool --> pkg_client_runtime
pkg_client_ui_tool --> pkg_client_ui_conversation
@@ -1257,7 +1270,7 @@ flowchart TD
pkg_acp_demo --> pkg_workspace_context
```
-| 包 | 分组 | 依赖项 |
+| Package | Group | Depends on |
| --- | --- | --- |
| [`invariants`](../packages/support/invariants) | `support` | — |
| [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/support/invariants) |
@@ -1269,6 +1282,7 @@ flowchart TD
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) |
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) |
| [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) |
+| [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/support/invariants) |
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) |
@@ -1294,8 +1308,6 @@ flowchart TD
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
-| [`helper`](../packages/scaffold/helper) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) |
-| [`telemetry`](../packages/scaffold/telemetry) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
@@ -1309,7 +1321,6 @@ flowchart TD
| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
@@ -1322,7 +1333,7 @@ flowchart TD
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
@@ -1337,7 +1348,6 @@ flowchart TD
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
@@ -1351,15 +1361,15 @@ flowchart TD
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
| [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
-| [`scripts`](../packages/scaffold/scripts) | `scaffold` | [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants) |
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
@@ -1382,10 +1392,10 @@ flowchart TD
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) |
+| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
+| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
@@ -1399,12 +1409,12 @@ flowchart TD
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`pwsh-sandbox`](../packages/bash/pwsh-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`pwsh-local`](../packages/bash/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
@@ -1414,13 +1424,15 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) |
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-bash-persistent`](../packages/pty/tool-bash-persistent) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) |
@@ -1435,19 +1447,18 @@ flowchart TD
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
+| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
-| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
+| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
@@ -1456,12 +1467,13 @@ flowchart TD
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
-| [`jsonrpc`](../packages/scaffold/server) | `scaffold` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`sdk-client`](../packages/scaffold/client) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session) |
-| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/scaffold/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
+| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
+| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
+| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) |
| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/support/invariants) |
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index a8b6842eb6..88fc306381 100644
--- a/docs/persistence-catalog.i18n.yaml
+++ b/docs/persistence-catalog.i18n.yaml
@@ -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 docs/persistence-catalog.md
-persistence-catalog.md: 34803a69f11a964ccc6da3e74eb678fe398f94c8
-persistence-catalog.zh.md: 3146712e342519fdf687d5b18bd1c95c8c0f0a37
+persistence-catalog.md: 3b6e5b742df6282ec0cde4b4581eb3909844b537
+persistence-catalog.zh.md: 6b65038869b53536bcbb2bd15c04172a7b5531e5
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index 34803a69f1..3b6e5b742d 100644
--- a/docs/persistence-catalog.md
+++ b/docs/persistence-catalog.md
@@ -7,7 +7,7 @@ Every event type that can appear in a session's durable event log: the complete
This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).
-The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
+The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, the optional `ignorable` unknown-type skip marker, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
## Event envelope
@@ -63,6 +63,17 @@ export type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
@@ -79,7 +90,7 @@ export type SessionEvent = {
}[T]
```
-Sources: [`packages/core/session/src/types.ts:316`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:352`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:384`](../packages/core/session/src/types.ts)
+Sources: [`packages/core/session/src/types.ts:331`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:367`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:399`](../packages/core/session/src/types.ts)
## Events
@@ -192,7 +203,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/inter
Types: [StreamChunk](subsystems/llm-streaming.md)
-Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:261`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -208,7 +219,7 @@ Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/
Types: [TokenUsage](subsystems/llm-streaming.md)
-Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/types.ts)
### `command/*`
@@ -359,12 +370,12 @@ Source: [`packages/compact/compact/src/types.ts:33`](../packages/compact/compact
```ts persistence-catalog
/**
* One recorded human remark about this session. Log-only and independent
- * of its trigger; it never enters the model surface or derived history.
+ * of its trigger; it never enters model context or derived history.
*/
'feedback/record': { text: string }
```
-Source: [`packages/feedback/command-feedback/src/index.ts:25`](../packages/feedback/command-feedback/src/index.ts)
+Source: [`packages/feedback/command-feedback/src/index.ts:62`](../packages/feedback/command-feedback/src/index.ts)
### `goal/*`
@@ -488,7 +499,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s
'request/context': RequestContext
```
-Source: [`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
#### `request/header` — log-only
@@ -500,7 +511,7 @@ Source: [`packages/core/session/src/types.ts:289`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
-Source: [`packages/core/session/src/types.ts:284`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -553,7 +564,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s
'session/end-seed': Record
```
-Source: [`packages/core/session/src/types.ts:312`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:327`](../packages/core/session/src/types.ts)
#### `session/title` — log-only
@@ -589,7 +600,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi
'step/end': { turn: number; step: number }
```
-Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -598,7 +609,7 @@ Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/
'step/start': { turn: number; step: number }
```
-Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/types.ts)
### `subagent/*`
@@ -628,7 +639,7 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent
Types: [TodoItem](subsystems/session.md)
-Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -645,7 +656,7 @@ Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/
Types: [CallId](subsystems/core.md)
-Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:274`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only
@@ -714,7 +725,7 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types
}
```
-Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts)
### `tool-workflow/*`
@@ -784,7 +795,7 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow
Types: [TurnEndReason](subsystems/session.md)
-Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
@@ -798,7 +809,7 @@ Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/
'turn/start': { turn: number }
```
-Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts)
### `user/*`
@@ -815,7 +826,7 @@ Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/
'user/message': UserMessage
```
-Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
### `web/*`
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index 3146712e34..6b65038869 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -9,7 +9,7 @@
英文源文件根据源码生成(`scripts/gen-persistence-catalog.ts`),并由 `pnpm run verify-persistence-catalog`(`doc-sync`(文档同步门禁)的一部分)验证新鲜度;本中文文件作为经评审对侧通过双语配对维护。声明块保留源码声明和嵌套属性的 JSDoc,只移除其所在接口/模块带来的缩进,并使用 `ts persistence-catalog` 围栏(doc-typecheck 会跳过这些围栏,因为声明引用了其所属模块中的类型)。payload 中的类型名称会链接到记录该类型的页面。参见 [persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md)。
-以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
+以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`、可选的未知类型跳过标记 `ignorable`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
## 事件信封
@@ -65,6 +65,17 @@ export type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
@@ -361,7 +372,7 @@ export type SessionEvent = {
```ts persistence-catalog
/**
* One recorded human remark about this session. Log-only and independent
- * of its trigger; it never enters the model surface or derived history.
+ * of its trigger; it never enters model context or derived history.
*/
'feedback/record': { text: string }
```
diff --git a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.i18n.yaml b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.i18n.yaml
index e1e354fc6a..6f20cc55dc 100644
--- a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.i18n.yaml
+++ b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.i18n.yaml
@@ -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 docs/postmortem/0002-js-expression-disabled-filesystem-tools.md
-0002-js-expression-disabled-filesystem-tools.md: b2bd37ff2b5a6d01c585514dd83f7fa6604f8945
+0002-js-expression-disabled-filesystem-tools.md: 93c62261f54e273ea5dbb2ca1a236daf9b2a6f15
0002-js-expression-disabled-filesystem-tools.zh.md: 7a26f8456c13ad22535cd04fdec061dccd2ed85d
diff --git a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md
index b2bd37ff2b..93c62261f5 100644
--- a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md
+++ b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md
@@ -16,7 +16,7 @@ Cordis Include parsed each `!!js` scalar into an expression object. The Loader r
## Impact
-Seven filesystem scenarios and the mixed workspace-edit scenario called tools that were absent from the registry. Their structured session logs carried `ToolNotFoundError` with code `UNKNOWN_TOOL`, while stdout rendered generic failed tool cards. The snapshot suite passed because both surfaces matched the refreshed fixtures; it proved deterministic replay of the regression rather than successful filesystem behavior.
+Seven filesystem scenarios and the mixed workspace-edit scenario called tools that were absent from the registry. Their structured session logs carried `ToolNotFoundError` with code `UNKNOWN_TOOL`, while stdout rendered generic failed tool cards. The snapshot suite passed because both outputs matched the refreshed fixtures; it proved deterministic replay of the regression rather than successful filesystem behavior.
The live confined default did not gain unintended filesystem access. A naive interpolation fix would have created that risk: permission presets update bash sandbox and approval state at runtime, but cannot mount, unmount, or confine the filesystem stack.
diff --git a/docs/rescope.i18n.yaml b/docs/rescope.i18n.yaml
new file mode 100644
index 0000000000..2959c354da
--- /dev/null
+++ b/docs/rescope.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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 docs/rescope.md
+rescope.md: 3dde39875021e7a4161e1ae66550e9dedf5eb4fa
+rescope.zh.md: a7f355cf651fb063bf2d4c3cefe18babd4a57401
diff --git a/docs/rescope.md b/docs/rescope.md
new file mode 100644
index 0000000000..3dde398750
--- /dev/null
+++ b/docs/rescope.md
@@ -0,0 +1,53 @@
+# Vendored package rescope
+
+English | [中文](rescope.zh.md)
+
+The Cordis framework and its foundation libraries are vendored under [`vendor/`](../vendor/README.md) and published under the `@deepseek-ai` scope, because every harness package declares the framework as a peer dependency: publishing the harness publishes this layer with it, and under the upstream names that publication would squat them on the registry. This page is the name mapping; the decision and its consequences live in the [rescope Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md), and the upstream commits in [`vendor/README.md`](../vendor/README.md).
+
+## Name mapping
+
+| Directory | Upstream name | Published name | Version | Role |
+|---|---|---|---|---|
+| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | Framework core: `Context`, `Service`, `Fiber`, events |
+| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | Shared utilities the framework and Schemastery build on |
+| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | Config schemas (`Schema`) behind every plugin's `Config` |
+| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` loading, plugin resolution, repository cache |
+| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | Config includes and patch overlays |
+| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | Nested plugin groups |
+| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | Disposal-aware timers on `ctx` |
+| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | Hot module replacement for plugins and config |
+| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | Console logger exporter |
+
+Subpath exports keep their path: `@cordisjs/plugin-loader/repository` becomes `@deepseek-ai/cordis-plugin-loader/repository`.
+
+## What the rename does not touch
+
+- **Directory names and versions.** `vendor/hmr/` stays `vendor/hmr/`, and every package keeps the upstream version its manifest table row records, so the vendored tree still reads as an upstream snapshot.
+- **Dependency ranges.** A dependency entry changes its key, never its range: `"cordis": "^4.0.0-rc.7"` becomes `"@deepseek-ai/cordis": "^4.0.0-rc.7"`. `linkWorkspacePackages` resolves those preserved ranges to the pinned workspaces.
+- **The Loader's `cordis:` builtin prefix.** `cordis:include` and `cordis:group` are a protocol prefix, not a package name.
+- **The `cordis.yml` configuration family**, including `*.cordis.yml`, `*.cordis.snapshot.yml`, and `cordis.patch.yml`.
+- **Harness packages whose own names contain the word**, such as `@deepseek-ai/dsh-tool-cordis`.
+- **Upstream runtime identifiers**, such as Schemastery's `Symbol.for('schemastery')` and its `vendor:` metadata field.
+- **Prose outside `docs/`.** `vendor/*/README.md`, package READMEs, and Agent Notes keep the names they were written with; a bare `cordis` there can also be the Python SDK's option name or an agent-preset id. Inside `docs/`, prose and every Markdown fence follow the rename.
+
+## What your code has to change
+
+| Site | Before | After |
+|---|---|---|
+| Module import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
+| Typed-event merge | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
+| `package.json` dependency key | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
+| `cordis.yml` plugin entry | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
+
+## Applying, verifying, and reverting
+
+[`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) owns the mapping above and performs the rename, so no reference is renamed by hand:
+
+```sh
+pnpm run rescope-vendor # report what would change
+pnpm run rescope-vendor --apply # rewrite every reference
+pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
+pnpm run rescope-vendor --apply --reverse # return to the upstream names
+```
+
+Re-apply it after an upstream sync ([procedure](../vendor/README.md)), and follow it with the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, and `pnpm run verify-translation-pairing --write` for the bilingual pairs it touched.
diff --git a/docs/rescope.zh.md b/docs/rescope.zh.md
new file mode 100644
index 0000000000..a7f355cf65
--- /dev/null
+++ b/docs/rescope.zh.md
@@ -0,0 +1,53 @@
+# Vendored 包改名
+
+[English](rescope.md) | 中文
+
+Cordis 框架及其基础库以源码形式 vendored 在 [`vendor/`](../vendor/README.md) 下,并以 `@deepseek-ai` scope 发布:每个 harness 包都把框架声明为 peer dependency,发布 harness 就会连带发布这一层,用上游名发布等于在 registry 上占用别人的名字。本页是名字映射表;决策与影响见 [改名 Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md),上游 commit 见 [`vendor/README.md`](../vendor/README.md)。
+
+## 名字映射
+
+| 目录 | 上游名 | 发布名 | 版本 | 角色 |
+|---|---|---|---|---|
+| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | 框架核心:`Context`、`Service`、`Fiber`、事件 |
+| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | 框架与 Schemastery 共用的基础工具 |
+| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | 配置 schema(`Schema`),每个插件的 `Config` 都基于它 |
+| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` 装载、插件解析、repository 缓存 |
+| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 配置包含与 patch 叠加 |
+| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 嵌套插件分组 |
+| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | `ctx` 上随 disposal 回收的定时器 |
+| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 插件与配置的热替换 |
+| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 控制台日志导出 |
+
+子路径导出保持原路径:`@cordisjs/plugin-loader/repository` 变成 `@deepseek-ai/cordis-plugin-loader/repository`。
+
+## 改名不碰什么
+
+- **目录名与版本号。** `vendor/hmr/` 仍是 `vendor/hmr/`,每个包保留清单表那行记录的上游版本,所以 vendored 树依旧读作一份上游快照。
+- **依赖 range。** 依赖条目只换键、不换范围:`"cordis": "^4.0.0-rc.7"` 变成 `"@deepseek-ai/cordis": "^4.0.0-rc.7"`;`linkWorkspacePackages` 靠这些保留下来的范围把它们解析到固定的 workspace。
+- **Loader 的 `cordis:` 内建前缀。** `cordis:include`、`cordis:group` 是协议前缀,不是包名。
+- **`cordis.yml` 配置文件家族**,包括 `*.cordis.yml`、`*.cordis.snapshot.yml`、`cordis.patch.yml`。
+- **名字里带这个词的 harness 包**,例如 `@deepseek-ai/dsh-tool-cordis`。
+- **上游运行时标识符**,例如 Schemastery 的 `Symbol.for('schemastery')` 及其 `vendor:` 元数据字段。
+- **`docs/` 之外的散文。** `vendor/*/README.md`、各包 README 与 Agent Note 保留写作当时的名字;那里的裸 `cordis` 也可能是 Python SDK 的选项名或某个 agent-preset 的 id。`docs/` 之内,散文与所有 Markdown 围栏都跟着改。
+
+## 你的代码要改什么
+
+| 位置 | 改前 | 改后 |
+|---|---|---|
+| 模块 import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
+| 类型事件声明合并 | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
+| `package.json` 依赖键 | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
+| `cordis.yml` 插件条目 | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
+
+## 施加、核验与回退
+
+上面这份映射由 [`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) 承载并执行改名,任何引用都不靠手改:
+
+```sh
+pnpm run rescope-vendor # report what would change
+pnpm run rescope-vendor --apply # rewrite every reference
+pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
+pnpm run rescope-vendor --apply --reverse # return to the upstream names
+```
+
+上游 sync 之后重跑它([流程](../vendor/README.md)),并接上它打印的重生成:`pnpm install` 重生成 lockfile、`pnpm run gen-third-party-notices`、以及对它触及的双语对跑 `pnpm run verify-translation-pairing --write`。
diff --git a/docs/subsystems/README.i18n.yaml b/docs/subsystems/README.i18n.yaml
index 445d2b5a45..049d5229b0 100644
--- a/docs/subsystems/README.i18n.yaml
+++ b/docs/subsystems/README.i18n.yaml
@@ -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 docs/subsystems/README.md
-README.md: 7d66cfcf66ffb0bed9430892308934c1f10982f4
-README.zh.md: 90e2b28b15870387539500568bb85b525db63ef6
+README.md: ee753712748d5824fd3e8b03e03616d9fa8713c7
+README.zh.md: bbddde0a494c4783744042b4b5a5f4dee7e44ffd
diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md
index 7d66cfcf66..ee75371274 100644
--- a/docs/subsystems/README.md
+++ b/docs/subsystems/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-One page per subsystem of the DeepSeek Harness: what it is, the data structures it moves, and — where a `ctx` service or event scope backs it — a generated **Cordis surface** section carrying its service and event reference. The folder complements [architecture.md](../architecture.md), which describes *behavior* across subsystems (the service map, the session/turn/step lifecycle, the event taxonomy); each page here is the reference for one subsystem's vocabulary and wiring.
+One page per subsystem of the DeepSeek Harness: what it is, the data structures it moves, and — where a `ctx` service or event scope backs it — a generated **Cordis API** section carrying its service and event reference. The folder complements [architecture.md](../architecture.md), which describes *behavior* across subsystems (the service map, the session/turn/step lifecycle, the event taxonomy); each page here is the reference for one subsystem's vocabulary and wiring.
| Page | Owns |
|---|---|
@@ -18,6 +18,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
| [settings.md](settings.md) | the user-settings seam: `SettingsNamespace` registration, layered resolution (defaults → composition `base` → user document), owner scopes, hot commits |
| [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers |
| [session-query.md](session-query.md) | logical records, bounded exact-event reads, relationship traces, semantic filters/documents, and full-text result pages |
+| [feedback.md](feedback.md) | lifecycle-bound per-message feedback records, optimistic versions, sidecar persistence, and the Host Remote contract |
| [session-title.md](session-title.md) | durable title snapshots, cited source-message seqs, and the asynchronous provider contract |
| [session-reference.md](session-reference.md) | structured cross-session references: `SessionReferenceInput`/`Candidate`, prepared message contexts, the stable error taxonomy |
| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly |
@@ -45,8 +46,8 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
| [http-server.md](http-server.md) | the HTTP carrier: `WebRouteKind`/`WebRoute`, match order, the claimable fallback seat, index taps |
| [storage.md](storage.md) | the storage subsystem: the backend contract (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` |
| [workspace.md](workspace.md) | the workspace registry: `Workspace`/`WorkspaceId`, registration and resolution, the session `cwd` relationship |
-| [client-modules.md](client-modules.md) | the web plugin table: `dshClient` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
+| [client-modules.md](client-modules.md) | the web plugin table: `dsh.client` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
| [session-projection.md](session-projection.md) | the projection seam: `SessionProjectionMap`, the pure `ProjectionDefinition` unit, `ProjectionSnapshot`'s consistent cut, the change feed |
| [telemetry.md](telemetry.md) | the outbound session-reporting capability seam: `TelemetryRecord`/`TelemetrySeverity`, the `TelemetryBackend` contract, and the `telemetry/record` redact waterfall |
-> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services and events use each page's generated **Cordis surface** section.
+> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services and events use each page's generated **Cordis API** section.
diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md
index 90e2b28b15..bbddde0a49 100644
--- a/docs/subsystems/README.zh.md
+++ b/docs/subsystems/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-每个子系统一页,覆盖 DeepSeek Harness 的全部子系统:它是什么、它操作哪些数据结构,以及——当它由某个 `ctx` 服务或事件作用域支撑时——一段生成的 **Cordis surface** 小节,承载其服务与事件参考。本目录与 [architecture.md](../architecture.md) 互补:后者描述跨子系统的*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系);这里的每一页是单个子系统词汇与接线的参考。
+每个子系统一页,覆盖 DeepSeek Harness 的全部子系统:它是什么、它操作哪些数据结构,以及——当它由某个 `ctx` 服务或事件作用域支撑时——一段生成的 **Cordis API** 小节,承载其服务与事件参考。本目录与 [architecture.md](../architecture.md) 互补:后者描述跨子系统的*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系);这里的每一页是单个子系统词汇与接线的参考。
| 页面 | 负责内容 |
|---|---|
@@ -18,6 +18,7 @@
| [settings.md](settings.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 |
| [credentials.md](credentials.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、provider 来源层 |
| [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
+| [feedback.md](feedback.md) | 绑定生命周期的逐消息反馈记录、乐观版本、伴随记录持久化与 Host Remote 契约 |
| [session-title.md](session-title.md) | 持久标题快照、被引用的来源消息 seq 与异步提供方约定 |
| [session-reference.md](session-reference.md) | 结构化跨会话引用:`SessionReferenceInput`/`Candidate`、prepared 消息上下文、稳定错误分类 |
| [system-prompt.md](system-prompt.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 |
@@ -45,8 +46,8 @@
| [http-server.md](http-server.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、可认领的回退席位、index 转换 |
| [storage.md](storage.md) | 存储子系统:后端约定(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` |
| [workspace.md](workspace.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
-| [client-modules.md](client-modules.md) | Web 插件表:`dshClient` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
+| [client-modules.md](client-modules.md) | Web 插件表:`dsh.client` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
| [session-projection.md](session-projection.md) | 投影 seam:`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 |
| [telemetry.md](telemetry.md) | 对外会话上报能力 seam:`TelemetryRecord`/`TelemetrySeverity`、`TelemetryBackend` 约定和 `telemetry/record` 脱敏 waterfall |
-> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务与事件使用每页生成的 **Cordis surface** 小节。
+> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务与事件使用每页生成的 **Cordis API** 小节。
diff --git a/docs/subsystems/approval.i18n.yaml b/docs/subsystems/approval.i18n.yaml
index 088b30ff32..bfc510ac16 100644
--- a/docs/subsystems/approval.i18n.yaml
+++ b/docs/subsystems/approval.i18n.yaml
@@ -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 docs/subsystems/approval.md
-approval.md: 660ced6d75886bbea41985fd1f357bf92e8a7838
-approval.zh.md: 56975914098978af99580694ca19f20a02ed4022
+approval.md: 6ade53d7755f1070a17513d9f3efd4252d6d255f
+approval.zh.md: 92138bee75369f81b69b3551ee47ea97be07441e
diff --git a/docs/subsystems/approval.md b/docs/subsystems/approval.md
index 660ced6d75..6ade53d775 100644
--- a/docs/subsystems/approval.md
+++ b/docs/subsystems/approval.md
@@ -91,9 +91,9 @@ The audit events are log-only and do not enter the model transcript. Model-visib
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/approval.zh.md b/docs/subsystems/approval.zh.md
index 5697591409..92138bee75 100644
--- a/docs/subsystems/approval.zh.md
+++ b/docs/subsystems/approval.zh.md
@@ -91,9 +91,9 @@ interface ApprovalRequest {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/attachment.i18n.yaml b/docs/subsystems/attachment.i18n.yaml
index 0d2e75d8fc..d53f337679 100644
--- a/docs/subsystems/attachment.i18n.yaml
+++ b/docs/subsystems/attachment.i18n.yaml
@@ -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 docs/subsystems/attachment.md
-attachment.md: 51556c3a0391a571656f407ed22edb8b93eb5326
-attachment.zh.md: 2a6079845ae6f19884b9fb3312e32187161d57d1
+attachment.md: bfc1a54107c75b442f6b5b61fb705852ab4213db
+attachment.zh.md: 4da600390ea111e9b2f640c51ab786ca0505db6e
diff --git a/docs/subsystems/attachment.md b/docs/subsystems/attachment.md
index 51556c3a03..bfc1a54107 100644
--- a/docs/subsystems/attachment.md
+++ b/docs/subsystems/attachment.md
@@ -75,9 +75,9 @@ interface StoredImageAttachment {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/attachment.zh.md b/docs/subsystems/attachment.zh.md
index 2a6079845a..4da600390e 100644
--- a/docs/subsystems/attachment.zh.md
+++ b/docs/subsystems/attachment.zh.md
@@ -75,9 +75,9 @@ interface StoredImageAttachment {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/bash.i18n.yaml b/docs/subsystems/bash.i18n.yaml
index 89b7a2feb6..b3cd579b22 100644
--- a/docs/subsystems/bash.i18n.yaml
+++ b/docs/subsystems/bash.i18n.yaml
@@ -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 docs/subsystems/bash.md
-bash.md: b7e7c25ac4e49c34e186ec63f2d84411d71631fc
-bash.zh.md: 409a00f8e533f84852d56c1074319a8f0425fb37
+bash.md: 50e2051778dc0aca5c7b80e1a8b330ae62d4d309
+bash.zh.md: 40ab5b7eea04aeb7c85ab550a8d93ce352fe116e
diff --git a/docs/subsystems/bash.md b/docs/subsystems/bash.md
index b7e7c25ac4..50e2051778 100644
--- a/docs/subsystems/bash.md
+++ b/docs/subsystems/bash.md
@@ -98,7 +98,7 @@ interface BashExecSpec {
}
```
-`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md).
+`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md).
`stdoutMaxBytes` is also trusted-plugin-only. It lets a foreground consumer request complete stdout up to a bounded parser budget without changing stderr, background tasks, or the model-facing bash tool's ordinary output cap.
@@ -224,9 +224,9 @@ interface BashProcessRead {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/bash.zh.md b/docs/subsystems/bash.zh.md
index 409a00f8e5..40ab5b7eea 100644
--- a/docs/subsystems/bash.zh.md
+++ b/docs/subsystems/bash.zh.md
@@ -98,7 +98,7 @@ interface BashExecSpec {
}
```
-`stdin` 和 `env` 是受信任的进程内插件输入,不由 `dsh-tool-bash` 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 [bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
+`stdin` 和 `env` 是受信任的进程内插件输入,不由 `dsh-tool-bash` 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 [bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md)。
`stdoutMaxBytes` 同样仅供受信任插件使用。它让前台消费方能在有界解析预算内请求完整 stdout,而不会改变 stderr、后台任务或面向模型的 bash 工具的常规输出上限。
@@ -224,9 +224,9 @@ interface BashProcessRead {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/client-modules.i18n.yaml b/docs/subsystems/client-modules.i18n.yaml
index 8c320d18e6..22b3d51e3e 100644
--- a/docs/subsystems/client-modules.i18n.yaml
+++ b/docs/subsystems/client-modules.i18n.yaml
@@ -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 docs/subsystems/client-modules.md
-client-modules.md: 808fa62b2e5a4351b1dc98bbe4f3d2af63c9dd2b
-client-modules.zh.md: dcab60bea7c54770cc5e6032afb39d6dd189d037
+client-modules.md: bd0ad83f6ebbbab3be8c91e8d51b1c3492e41e6c
+client-modules.zh.md: aea573ee99be2781427d6843b12936406d563283
diff --git a/docs/subsystems/client-modules.md b/docs/subsystems/client-modules.md
index 808fa62b2e..bd0ad83f6e 100644
--- a/docs/subsystems/client-modules.md
+++ b/docs/subsystems/client-modules.md
@@ -2,7 +2,7 @@
English | [中文](client-modules.zh.md)
-The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModuleHost` (`ClientModuleHostService`). It scans the host Loader's entries for `dshClient` packages, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins//client.js`, and taps the index render to inject the boot manifest — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [http-server.md](http-server.md) supplies the prefix route and index tap this service registers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here.
+The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModuleHost` (`ClientModuleHostService`). It scans the host Loader's entries for packages declaring `dsh.client`, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins//client.js`, and taps the index render to inject the boot manifest — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [http-server.md](http-server.md) supplies the prefix route and index tap this service registers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here.
Source: [`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
@@ -15,7 +15,7 @@ The graph is the wire single source between the Node and browser halves: the hos
* One composed client entry pushed by the host (a graph row). Wire
* single source: the host node half (package root) produces this same shape.
* `immediately` marks stage-one prefetch; `inject` is informational graph
- * metadata (the authoritative edges live in each package's dshClient
+ * metadata (the authoritative edges live in each package's `dsh.client`
* declaration and reach fibers through entry creation).
*/
interface WebBootEntry {
@@ -46,7 +46,7 @@ Each row's `rev` is the bundle's content hash and rides the URL as a cache-busti
## The scan
-A package joins the table by declaring `dshClient` (`platform: 'web'`, optional `inject` edges, optional `immediately`) in its package.json and exporting its built bundle at `exports["./client"]`. Package resolution anchors at the config tree's `ctx.baseUrl` — the cordis.yml directory, whose package declares every composed plugin as a dependency — and construction throws when that anchor is unset.
+A package joins the table by declaring `dsh.client` (`platform: 'web'`, optional `inject` edges, optional `immediately`) in its package.json and exporting its built bundle at `exports["./client"]`. Package resolution anchors at the config tree's `ctx.baseUrl` — the cordis.yml directory, whose package declares every composed plugin as a dependency — and construction throws when that anchor is unset.
Scanning is incremental per package; there is no full-rescan code path. Every cordis `internal/plugin` emission (fiber construction or disposal) marks the fiber's entry name dirty, and a microtask flush reconciles each dirty name against the live loader entries. The activation pass seeds the same dirty set with all current entries and flushes synchronously, so first scan and steady state share one implementation — with opposite failure postures. At activation, a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud `AggregateError` listing every broken package: the fiber FAILS and the boot's fail-loud sweep reports it. In steady state, a broken package logs a warning and must not poison the others.
@@ -66,15 +66,15 @@ In development, [dsh-client-hmr](../../packages/client/hmr/README.md) is the reg
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.clientModuleHost` — `ClientModuleHostService`
-The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
+The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
diff --git a/docs/subsystems/client-modules.zh.md b/docs/subsystems/client-modules.zh.md
index dcab60bea7..aea573ee99 100644
--- a/docs/subsystems/client-modules.zh.md
+++ b/docs/subsystems/client-modules.zh.md
@@ -2,7 +2,7 @@
[English](client-modules.md) | 中文
-Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModuleHost`(`ClientModuleHostService`)形式提供。它扫描宿主 Loader 的 entry 找出 `dshClient` 包(package),组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins//client.js` 提供各个 bundle,并经 index 转换(index tap)注入启动 manifest(元数据清单)——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[http-server.md](http-server.md) 所述的载体提供本服务注册的前缀路由与 index 转换。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。
+Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModuleHost`(`ClientModuleHostService`)形式提供。它扫描宿主 Loader 的 entry,找出声明了 `dsh.client` 的包(package),组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins//client.js` 提供各个 bundle,并经 index 转换(index tap)注入启动 manifest(元数据清单)——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[http-server.md](http-server.md) 所述的载体提供本服务注册的前缀路由与 index 转换。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。
源码:[`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
@@ -15,7 +15,7 @@ Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client
* One composed client entry pushed by the host (a graph row). Wire
* single source: the host node half (package root) produces this same shape.
* `immediately` marks stage-one prefetch; `inject` is informational graph
- * metadata (the authoritative edges live in each package's dshClient
+ * metadata (the authoritative edges live in each package's `dsh.client`
* declaration and reach fibers through entry creation).
*/
interface WebBootEntry {
@@ -46,7 +46,7 @@ interface WebBootGraph {
## 扫描
-包加入这张表的方式,是在自己的 package.json 中声明 `dshClient`(`platform: 'web'`、可选的 `inject` 边、可选的 `immediately`),并在 `exports["./client"]` 导出构建好的 bundle。包解析锚定在配置树的 `ctx.baseUrl`——即 cordis.yml 所在目录,该目录的包把每个被组合的插件声明为依赖——这一锚点未设置时,构造即抛错。
+包加入这张表的方式,是在自己的 package.json 中声明 `dsh.client`(`platform: 'web'`、可选的 `inject` 边、可选的 `immediately`),并在 `exports["./client"]` 导出构建好的 bundle。包解析锚定在配置树的 `ctx.baseUrl`——即 cordis.yml 所在目录,该目录的包把每个被组合的插件声明为依赖——这一锚点未设置时,构造即抛错。
扫描是单包增量的;不存在全量重扫代码路径。fiber 构造或 dispose(资源释放)时的每次 cordis `internal/plugin` 发射都把该 fiber 的 entry 名标脏,一次微任务 flush 把每个脏名与实时 loader entry 对账。激活趟以全部当前 entry 灌入同一个脏集合并同步 flush,因此初扫与稳态共享一条实现——但失败姿态相反。激活时,已加载 entry 中的畸形声明或缺失 bundle 会聚合为一个大声的 `AggregateError`,列出每个损坏的包:该 fiber 进入 FAILED,由启动的大声失败 sweep 上报。稳态下,损坏的包只记录一条警告,且不得殃及其他包。
@@ -66,15 +66,15 @@ interface WebBootGraph {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.clientModuleHost` — `ClientModuleHostService`
-The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
+The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
diff --git a/docs/subsystems/code-runtime.i18n.yaml b/docs/subsystems/code-runtime.i18n.yaml
index 1ae9462b90..5b9f2d43b1 100644
--- a/docs/subsystems/code-runtime.i18n.yaml
+++ b/docs/subsystems/code-runtime.i18n.yaml
@@ -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 docs/subsystems/code-runtime.md
-code-runtime.md: a40801313d1adbdd1d601d319c9e5a563db478b6
-code-runtime.zh.md: 47516c7d21c24399498a05410ab9a46b61736fb4
+code-runtime.md: ef5f2b9b1f39c5e7218cc1f5d9ae6435ef725e3a
+code-runtime.zh.md: 3a136c4160eb30a38968ea4d482201d22ef62fde
diff --git a/docs/subsystems/code-runtime.md b/docs/subsystems/code-runtime.md
index a40801313d..ef5f2b9b1f 100644
--- a/docs/subsystems/code-runtime.md
+++ b/docs/subsystems/code-runtime.md
@@ -164,9 +164,9 @@ interface CodeRunFailure {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/code-runtime.zh.md b/docs/subsystems/code-runtime.zh.md
index 47516c7d21..3a136c4160 100644
--- a/docs/subsystems/code-runtime.zh.md
+++ b/docs/subsystems/code-runtime.zh.md
@@ -164,9 +164,9 @@ interface CodeRunFailure {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/commands.i18n.yaml b/docs/subsystems/commands.i18n.yaml
index 32c5f15c15..f04e332ad9 100644
--- a/docs/subsystems/commands.i18n.yaml
+++ b/docs/subsystems/commands.i18n.yaml
@@ -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 docs/subsystems/commands.md
-commands.md: 5e280d659c2a74b3d8ba20f362922e9eb4e0b84c
-commands.zh.md: 24f099d1926f7dba45122bdc53c23a4274e3f507
+commands.md: 03cb068418ff3e157349989e421f33d76cd7afe6
+commands.zh.md: b900baab4b0fffb50ac10265f4ea3ac2d8f9d01b
diff --git a/docs/subsystems/commands.md b/docs/subsystems/commands.md
index 5e280d659c..03cb068418 100644
--- a/docs/subsystems/commands.md
+++ b/docs/subsystems/commands.md
@@ -51,7 +51,7 @@ The adapter owns cancellation and passes the exact target agent. `rawInput` begi
interface CommandInvocation {
/** Pairing id already written to this invocation's `command/run` event. */
readonly commandId: CommandId
- /** Exact agent whose human-facing surface received the command. */
+ /** Exact agent whose UI received the command. */
readonly agent: Agent
/** Exact text following the registered command name, including separator whitespace. */
readonly rawInput: string
@@ -104,9 +104,9 @@ interface ParsedCommand {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/commands.zh.md b/docs/subsystems/commands.zh.md
index 24f099d192..b900baab4b 100644
--- a/docs/subsystems/commands.zh.md
+++ b/docs/subsystems/commands.zh.md
@@ -51,7 +51,7 @@ interface CommandDefinition {
interface CommandInvocation {
/** Pairing id already written to this invocation's `command/run` event. */
readonly commandId: CommandId
- /** Exact agent whose human-facing surface received the command. */
+ /** Exact agent whose UI received the command. */
readonly agent: Agent
/** Exact text following the registered command name, including separator whitespace. */
readonly rawInput: string
@@ -104,9 +104,9 @@ interface ParsedCommand {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/compaction.i18n.yaml b/docs/subsystems/compaction.i18n.yaml
index 7dc8602b93..5cc4527f73 100644
--- a/docs/subsystems/compaction.i18n.yaml
+++ b/docs/subsystems/compaction.i18n.yaml
@@ -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 docs/subsystems/compaction.md
-compaction.md: 8c2a987bc8423841fc23f81bde334c54bb5402c1
-compaction.zh.md: 42ebd7edcf9b64cb9ddcb9a81fef1fc0b2f834b3
+compaction.md: 9286a8be3f9030ac383701c6390c6c3582ec83a2
+compaction.zh.md: b4b353fef903b5e1a9cd29cee3147004b657c3bb
diff --git a/docs/subsystems/compaction.md b/docs/subsystems/compaction.md
index 8c2a987bc8..9286a8be3f 100644
--- a/docs/subsystems/compaction.md
+++ b/docs/subsystems/compaction.md
@@ -121,9 +121,9 @@ interface PruneResult {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/compaction.zh.md b/docs/subsystems/compaction.zh.md
index 42ebd7edcf..b4b353fef9 100644
--- a/docs/subsystems/compaction.zh.md
+++ b/docs/subsystems/compaction.zh.md
@@ -121,9 +121,9 @@ interface PruneResult {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml
index a7d26cee1b..dc9105c6a8 100644
--- a/docs/subsystems/core.i18n.yaml
+++ b/docs/subsystems/core.i18n.yaml
@@ -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 docs/subsystems/core.md
-core.md: ad00c4da7d77b0e1ab4728173b202ebc17fb56a0
-core.zh.md: 9c606023c85369643e7148f829526b1f75ea3631
+core.md: 1199943eb5770fd5fa7cc21cbed3023b844a55d6
+core.zh.md: 138d1d6292b89b2a9ff01cb925fdb81f008833fa
diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md
index ad00c4da7d..1199943eb5 100644
--- a/docs/subsystems/core.md
+++ b/docs/subsystems/core.md
@@ -31,7 +31,7 @@ Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index
* {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
* only the holder can tear this agent down. The registered factory provider is
* also a structural owner because the scoped agent depends on that provider's
- * service surface; provider unload stops and drains every live handle it made.
+ * service API; provider unload stops and drains every live handle it made.
* `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
* its session from the store, and finally unwinds its scoped world.
*
@@ -310,9 +310,9 @@ The two core IDs are `CallId` (correlates a tool call with its result; dsh-llm)
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -546,7 +546,7 @@ async standingKeyFor(id?: string): Promise
Types: [ScopeKey](scope.md)
-Source: [`packages/preset/agent-presets/src/index.ts:78`](../../packages/preset/agent-presets/src/index.ts)
+Source: [`packages/preset/agent-presets/src/index.ts:80`](../../packages/preset/agent-presets/src/index.ts)
@@ -718,7 +718,7 @@ list(): Agent[]
roots(): Agent[]
```
-Source: [`packages/core/agent/src/index.ts:255`](../../packages/core/agent/src/index.ts)
+Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md
index 9c606023c8..138d1d6292 100644
--- a/docs/subsystems/core.zh.md
+++ b/docs/subsystems/core.zh.md
@@ -33,7 +33,7 @@
* {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
* only the holder can tear this agent down. The registered factory provider is
* also a structural owner because the scoped agent depends on that provider's
- * service surface; provider unload stops and drains every live handle it made.
+ * service API; provider unload stops and drains every live handle it made.
* `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
* its session from the store, and finally unwinds its scoped world.
*
@@ -318,9 +318,9 @@ type Branded = string & { readonly [BRAND]: B }
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -554,7 +554,7 @@ async standingKeyFor(id?: string): Promise
Types: [ScopeKey](scope.md)
-Source: [`packages/preset/agent-presets/src/index.ts:78`](../../packages/preset/agent-presets/src/index.ts)
+Source: [`packages/preset/agent-presets/src/index.ts:80`](../../packages/preset/agent-presets/src/index.ts)
@@ -726,7 +726,7 @@ list(): Agent[]
roots(): Agent[]
```
-Source: [`packages/core/agent/src/index.ts:255`](../../packages/core/agent/src/index.ts)
+Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
diff --git a/docs/subsystems/credentials.i18n.yaml b/docs/subsystems/credentials.i18n.yaml
index c0ca84eb9f..a7c9218e1b 100644
--- a/docs/subsystems/credentials.i18n.yaml
+++ b/docs/subsystems/credentials.i18n.yaml
@@ -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 docs/subsystems/credentials.md
-credentials.md: 1d168999d338ba43c92150b171f89b610850f694
-credentials.zh.md: af92f17b10c80d3f78c61e306c526a165e48381b
+credentials.md: 5ac023231c7eac85edbeda1c72f871ca37c0a891
+credentials.zh.md: b3f6e19ca76fb680ab243715530cb03770b543dc
diff --git a/docs/subsystems/credentials.md b/docs/subsystems/credentials.md
index 1d168999d3..5ac023231c 100644
--- a/docs/subsystems/credentials.md
+++ b/docs/subsystems/credentials.md
@@ -53,9 +53,9 @@ interface CredentialInfo {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/credentials.zh.md b/docs/subsystems/credentials.zh.md
index af92f17b10..b3f6e19ca7 100644
--- a/docs/subsystems/credentials.zh.md
+++ b/docs/subsystems/credentials.zh.md
@@ -53,9 +53,9 @@ interface CredentialInfo {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/feedback.i18n.yaml b/docs/subsystems/feedback.i18n.yaml
new file mode 100644
index 0000000000..bef441ece2
--- /dev/null
+++ b/docs/subsystems/feedback.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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 docs/subsystems/feedback.md
+feedback.md: a0daf47d093f3efb643950c0e124a8db0734fde4
+feedback.zh.md: 1163d30a6af5f818ab3be8ff754de20656e7e743
diff --git a/docs/subsystems/feedback.md b/docs/subsystems/feedback.md
new file mode 100644
index 0000000000..a0daf47d09
--- /dev/null
+++ b/docs/subsystems/feedback.md
@@ -0,0 +1,256 @@
+# Message Feedback
+
+English | [中文](feedback.zh.md)
+
+[`@deepseek-ai/dsh-message-feedback`](../../packages/feedback/message-feedback) owns editable feedback for individual assistant messages. It is deliberately separate from the immutable Session-level `feedback/record` event: message feedback is a local storage-domain sidecar, not Session-log content or a projection, and it performs no telemetry handoff.
+
+Source: [`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
+
+## Public types
+
+```ts type-equiv
+/** Opaque compare-and-set token for one exact feedback item revision. */
+type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
+```
+
+```ts type-equiv
+/** The human's overall judgment of one assistant message. */
+type MessageFeedbackRating = 'positive' | 'negative'
+```
+
+```ts type-equiv
+/** One current feedback value and its opaque mutation token. */
+interface MessageFeedbackItem {
+ /** Stable identity of the assistant message inside the owning Session. */
+ readonly messageId: MessageId
+ /** Overall positive or negative judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional explanation, preserved verbatim after validation. */
+ readonly note?: string
+ /** Equality-only token replaced by every material create or update. */
+ readonly version: MessageFeedbackVersion
+ /** Host-assigned creation time in Unix epoch milliseconds. */
+ readonly createdAt: number
+ /** Host-assigned time of the most recent material update. */
+ readonly updatedAt: number
+}
+```
+
+```ts type-equiv
+/** Read all message feedback belonging to one persisted Session lifecycle. */
+interface MessageFeedbackListRequest {
+ /** Persisted Session whose sidecar should be read. */
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** Current feedback values for one Session, in first-creation order. */
+interface MessageFeedbackListValue {
+ /** Fresh immutable item snapshots. */
+ readonly items: readonly MessageFeedbackItem[]
+}
+```
+
+```ts type-equiv
+/** Create or replace feedback for one assistant message. */
+interface MessageFeedbackPutRequest {
+ /** Persisted Session that owns the target message. */
+ readonly sessionId: SessionId
+ /** Target assistant-message identity. */
+ readonly messageId: MessageId
+ /** Desired overall judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional non-blank explanation. */
+ readonly note?: string
+ /** Observed item version, or `null` to require that no item exists. */
+ readonly ifVersion: MessageFeedbackVersion | null
+}
+```
+
+```ts type-equiv
+/** Delete feedback for one message after observing its current version. */
+interface MessageFeedbackDeleteRequest {
+ /** Persisted Session that owns the sidecar. */
+ readonly sessionId: SessionId
+ /** Message whose feedback should be absent after this operation. */
+ readonly messageId: MessageId
+ /** Observed item version; ignored when the item is already absent. */
+ readonly ifVersion: MessageFeedbackVersion
+}
+```
+
+```ts type-equiv
+/** Idempotent deletion acknowledgement. */
+interface MessageFeedbackDeleteValue {
+ /** Stable postcondition shared by the first deletion and every retry. */
+ readonly absent: true
+}
+```
+
+```ts type-equiv
+/** No persisted Session header exists for the requested id. */
+interface MessageFeedbackSessionNotFound {
+ readonly code: 'session-not-found'
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** The id does not name a derived, append-origin assistant message. */
+interface MessageFeedbackTargetNotFound {
+ readonly code: 'target-not-found'
+ readonly sessionId: SessionId
+ readonly messageId: MessageId
+}
+```
+
+```ts type-equiv
+/** A material mutation did not match the addressed item's current version. */
+interface MessageFeedbackVersionConflict {
+ readonly code: 'version-conflict'
+ /** Authoritative current item, or `null` when it does not exist. */
+ readonly current: MessageFeedbackItem | null
+}
+```
+
+```ts type-equiv
+/** A supplied note contains no non-whitespace character. */
+interface MessageFeedbackNoteBlank {
+ readonly code: 'note-blank'
+}
+```
+
+```ts type-equiv
+/** A supplied note exceeds the configured UTF-8 byte limit. */
+interface MessageFeedbackNoteTooLarge {
+ readonly code: 'note-too-large'
+ readonly maxBytes: number
+ readonly actualBytes: number
+}
+```
+
+```ts type-equiv
+/** Failures shared by the public message-feedback operations. */
+type MessageFeedbackFailure =
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+```
+
+```ts type-equiv
+/** Successful public operation result. */
+interface MessageFeedbackSuccess {
+ readonly ok: true
+ readonly value: T
+}
+```
+
+```ts type-equiv
+/** Rejected public operation result with a stable business failure. */
+interface MessageFeedbackRejected {
+ readonly ok: false
+ readonly error: E
+}
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `list` operation. */
+type MessageFeedbackListResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `put` operation. */
+type MessageFeedbackPutResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected<
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+ >
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `delete` operation. */
+type MessageFeedbackDeleteResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+## Data and concurrency
+
+One Session sidecar row contains its header identity `{createdAt, cwd}` and feedback items keyed by `MessageId`. Each item carries a positive or negative rating, an optional note, Host-assigned `createdAt`/`updatedAt` timestamps, and its own opaque version. Versions are compared only for equality and only against the addressed message; callers do not order or synthesize them.
+
+`put` uses strict optimistic concurrency: every request for an existing item must match its current `ifVersion`, including a no-op. A conflict returns the authoritative current item (or `null`), so a caller can reconcile a lost response or a concurrent edit without another read. Deleting an already absent item succeeds. A per-Session queue encloses inspection, read, conflict evaluation, and whole-row write, so these guarantees cover concurrent calls in one Host process.
+
+## Target and lifecycle authority
+
+`SessionPersistence.inspect()` supplies the target Session observation without publishing or resuming an Agent and without committing cold repair. A cold `listSnapshots()` preflight classifies definite absence; inspection failure for a catalogued Session propagates as infrastructure failure. `put` accepts only a non-empty, append-origin `assistant/message` with the requested `MessageId`; replacement-origin, usage-only empty, and non-assistant records are not feedback targets.
+
+The stored `{createdAt, cwd}` identity must match the inspected header. A mismatch is treated as absence: `list` returns no items, while `put` may replace the stale row with one bound to the current header identity. Forks use a new Session identity and receive no sidecar copy even when their seed contains the same messages.
+
+## Persistence and Remote contract
+
+The service stores whole Session rows in the `message_feedback` storage domain through `ctx.storageDomain`. Before `put` commits a row that references a target message, a matching live target passes through the canonical `ctx.sessions.flush` checkpoint; both live and cold paths are then physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation is revalidated before the sidecar write, so the durable target log always precedes its sidecar commit. `maxNoteBytes` is required and bounds note text by UTF-8 bytes; the Web Host composition sets `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract through `GatewayService` and `@Remote`; the generated Cordis API below is the method-level authority.
+
+Plugin disposal closes mutation admission, drains accepted per-Session queue work, and then closes the storage domain.
+
+## Boundaries and limitations
+
+- The client Remote aggregate mount and UI consumer are separately owned and deferred.
+- The mutation queue is process-local. Storage-domain has no cross-process conditional write, so multiple Host writers to one storage root have no compare-and-swap or lost-update guarantee.
+- Session persistence has no durable deletion API. The service does not treat `session/disposed` or `host/session-removed` as deletion and therefore performs no fake cascade; orphan sidecar rows may remain after out-of-band log removal.
+- A request in the narrow interval after live detach but before the persistence catalog materializes the header can receive `session-not-found`; callers retry after retirement materialization.
+- Cold requests scan the complete Session snapshot catalog because persistence has no lookup-by-id metadata operation. One Session row also has no item-count or aggregate-byte cap; `maxNoteBytes` bounds only each note until a concrete consumer owns a row policy.
+- Header identity detects a reused id only when `{createdAt, cwd}` differs; a cloned log retaining the same header identity is indistinguishable by this contract.
+- The Host contract records no authenticated actor or audit identity and therefore assumes a trusted caller boundary.
+
+
+
+
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+
+### `ctx.messageFeedback` — `MessageFeedbackService`
+
+Storage-domain sidecar service. It inspects persisted Session history and never creates or resumes an Agent or Session.
+
+```ts cordis-catalog
+/**
+ * Read feedback belonging to the current persisted Session lifecycle.
+ * A stale row from a reused Session id is invisible.
+ * @param request - Session identity to inspect and list.
+ * @returns current immutable items or `session-not-found`.
+ */
+@Remote('list') async list(request: MessageFeedbackListRequest): Promise
+
+/**
+ * Create or replace feedback for one derived append-origin assistant
+ * message. Every request must match the addressed item's current version;
+ * a matching no-op returns the stored item without changing its revision.
+ * @param request - target, desired value, and observed item version.
+ * @returns the committed item or an explicit business failure.
+ */
+@Remote('put') put(request: MessageFeedbackPutRequest): Promise
+
+/**
+ * Delete one feedback item. Absence is successful regardless of the
+ * supplied version; an existing item requires an exact version match.
+ * @param request - Session, message, and observed item version.
+ * @returns the stable absent postcondition, or an explicit failure.
+ */
+@Remote('delete') delete(request: MessageFeedbackDeleteRequest): Promise
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:150`](../../packages/feedback/message-feedback/src/index.ts)
+
diff --git a/docs/subsystems/feedback.zh.md b/docs/subsystems/feedback.zh.md
new file mode 100644
index 0000000000..1163d30a6a
--- /dev/null
+++ b/docs/subsystems/feedback.zh.md
@@ -0,0 +1,256 @@
+# 消息反馈
+
+[English](feedback.md) | 中文
+
+[`@deepseek-ai/dsh-message-feedback`](../../packages/feedback/message-feedback)拥有针对单条 assistant 消息的可编辑反馈。它刻意与不可变的 Session 级 `feedback/record` 事件分离:message feedback 是本地 storage-domain 伴随记录(sidecar),不是 Session 日志内容或投影,也不执行遥测交接。
+
+来源:[`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
+
+## 公开类型
+
+```ts type-equiv
+/** Opaque compare-and-set token for one exact feedback item revision. */
+type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
+```
+
+```ts type-equiv
+/** The human's overall judgment of one assistant message. */
+type MessageFeedbackRating = 'positive' | 'negative'
+```
+
+```ts type-equiv
+/** One current feedback value and its opaque mutation token. */
+interface MessageFeedbackItem {
+ /** Stable identity of the assistant message inside the owning Session. */
+ readonly messageId: MessageId
+ /** Overall positive or negative judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional explanation, preserved verbatim after validation. */
+ readonly note?: string
+ /** Equality-only token replaced by every material create or update. */
+ readonly version: MessageFeedbackVersion
+ /** Host-assigned creation time in Unix epoch milliseconds. */
+ readonly createdAt: number
+ /** Host-assigned time of the most recent material update. */
+ readonly updatedAt: number
+}
+```
+
+```ts type-equiv
+/** Read all message feedback belonging to one persisted Session lifecycle. */
+interface MessageFeedbackListRequest {
+ /** Persisted Session whose sidecar should be read. */
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** Current feedback values for one Session, in first-creation order. */
+interface MessageFeedbackListValue {
+ /** Fresh immutable item snapshots. */
+ readonly items: readonly MessageFeedbackItem[]
+}
+```
+
+```ts type-equiv
+/** Create or replace feedback for one assistant message. */
+interface MessageFeedbackPutRequest {
+ /** Persisted Session that owns the target message. */
+ readonly sessionId: SessionId
+ /** Target assistant-message identity. */
+ readonly messageId: MessageId
+ /** Desired overall judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional non-blank explanation. */
+ readonly note?: string
+ /** Observed item version, or `null` to require that no item exists. */
+ readonly ifVersion: MessageFeedbackVersion | null
+}
+```
+
+```ts type-equiv
+/** Delete feedback for one message after observing its current version. */
+interface MessageFeedbackDeleteRequest {
+ /** Persisted Session that owns the sidecar. */
+ readonly sessionId: SessionId
+ /** Message whose feedback should be absent after this operation. */
+ readonly messageId: MessageId
+ /** Observed item version; ignored when the item is already absent. */
+ readonly ifVersion: MessageFeedbackVersion
+}
+```
+
+```ts type-equiv
+/** Idempotent deletion acknowledgement. */
+interface MessageFeedbackDeleteValue {
+ /** Stable postcondition shared by the first deletion and every retry. */
+ readonly absent: true
+}
+```
+
+```ts type-equiv
+/** No persisted Session header exists for the requested id. */
+interface MessageFeedbackSessionNotFound {
+ readonly code: 'session-not-found'
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** The id does not name a derived, append-origin assistant message. */
+interface MessageFeedbackTargetNotFound {
+ readonly code: 'target-not-found'
+ readonly sessionId: SessionId
+ readonly messageId: MessageId
+}
+```
+
+```ts type-equiv
+/** A material mutation did not match the addressed item's current version. */
+interface MessageFeedbackVersionConflict {
+ readonly code: 'version-conflict'
+ /** Authoritative current item, or `null` when it does not exist. */
+ readonly current: MessageFeedbackItem | null
+}
+```
+
+```ts type-equiv
+/** A supplied note contains no non-whitespace character. */
+interface MessageFeedbackNoteBlank {
+ readonly code: 'note-blank'
+}
+```
+
+```ts type-equiv
+/** A supplied note exceeds the configured UTF-8 byte limit. */
+interface MessageFeedbackNoteTooLarge {
+ readonly code: 'note-too-large'
+ readonly maxBytes: number
+ readonly actualBytes: number
+}
+```
+
+```ts type-equiv
+/** Failures shared by the public message-feedback operations. */
+type MessageFeedbackFailure =
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+```
+
+```ts type-equiv
+/** Successful public operation result. */
+interface MessageFeedbackSuccess {
+ readonly ok: true
+ readonly value: T
+}
+```
+
+```ts type-equiv
+/** Rejected public operation result with a stable business failure. */
+interface MessageFeedbackRejected {
+ readonly ok: false
+ readonly error: E
+}
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `list` operation. */
+type MessageFeedbackListResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `put` operation. */
+type MessageFeedbackPutResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected<
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+ >
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `delete` operation. */
+type MessageFeedbackDeleteResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+## 数据与并发
+
+每个 Session 的一条伴随记录包含 header 身份 `{createdAt, cwd}` 和以 `MessageId` 为键的反馈条目。每个条目携带好评或差评、可选备注、Host 分配的 `createdAt`/`updatedAt` 时间戳及自己的 opaque version。version 只能用于相等比较,且只与目标消息比较;调用方不能排序或自行合成它。
+
+`put` 采用严格乐观并发:已有条目的每次请求都必须匹配当前 `ifVersion`,即使请求不会改变目标值。冲突会返回权威当前条目(不存在时为 `null`),因此调用方无需额外读取,即可协调丢失响应或并发编辑。删除已经不存在的条目同样成功。按 Session 划分的队列覆盖检查、读取、冲突判断与整行写入,因此这些保证适用于单个 Host 进程中的并发调用。
+
+## 目标与生命周期权威
+
+`SessionPersistence.inspect()` 提供目标 Session 的观测,且不会发布或恢复 Agent,也不会提交 cold repair。cold 路径先由 `listSnapshots()` 预检明确不存在;已进入目录的 Session 若检查失败,会按基础设施故障原样传播。`put` 只接受具有指定 `MessageId` 的非空、append-origin `assistant/message`;replacement-origin、仅承载 usage 的空记录和非 assistant 记录都不是反馈目标。
+
+存储的 `{createdAt, cwd}` 身份必须与检查所得 header 匹配。不匹配按不存在处理:`list` 返回空条目,`put` 则可用绑定当前 header 身份的新记录替换陈旧行。fork 使用新的 Session 身份,即使种子包含相同消息,也不获得伴随记录副本。
+
+## 持久化与 Remote 契约
+
+服务通过 `ctx.storageDomain` 在 `message_feedback` 存储域中保存完整 Session 行。`put` 提交引用目标消息的伴随记录前,身份匹配的 live 目标先经过权威 `ctx.sessions.flush` checkpoint;随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。写入伴随记录前会再次校验所得观测,因此目标日志的持久提交始终先于其伴随记录。`maxNoteBytes` 为必填项,按 UTF-8 字节限制备注文本;Web Host 组合将其设为 `8192`。该包通过 `GatewayService` 与 `@Remote` 发布 Host `messageFeedback.list`、`messageFeedback.put` 和 `messageFeedback.delete` 一元 Remote 契约;下方生成的 Cordis API 是方法级权威。
+
+Plugin disposal 会先关闭变更接纳,排空已进入各 Session 队列的工作,然后才关闭 storage domain。
+
+## 边界与限制
+
+- 客户端 Remote 聚合挂载与 UI 消费方由各自边界负责并保持延后。
+- 变更队列仅在进程内生效。storage-domain 没有跨进程条件写,因此多个 Host 写入同一存储根目录时,不提供 compare-and-swap 或防止丢失更新的保证。
+- Session persistence 没有持久删除接口。服务不把 `session/disposed` 或 `host/session-removed` 当作删除,因此不伪造级联;在带外移除日志后,孤儿伴随记录可能继续存在。
+- 请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
+- 由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。单个 Session 行也没有条目数或聚合字节上限;在具体消费方拥有行策略之前,`maxNoteBytes` 只限制每条备注。
+- 只有 `{createdAt, cwd}` 不同时,header 身份才能识别复用的 id;本契约无法区分保留相同 header 身份的克隆日志。
+- Host 契约不记录已认证的 actor 或审计身份,因此假设调用方边界可信。
+
+
+
+
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+
+### `ctx.messageFeedback` — `MessageFeedbackService`
+
+Storage-domain sidecar service. It inspects persisted Session history and never creates or resumes an Agent or Session.
+
+```ts cordis-catalog
+/**
+ * Read feedback belonging to the current persisted Session lifecycle.
+ * A stale row from a reused Session id is invisible.
+ * @param request - Session identity to inspect and list.
+ * @returns current immutable items or `session-not-found`.
+ */
+@Remote('list') async list(request: MessageFeedbackListRequest): Promise
+
+/**
+ * Create or replace feedback for one derived append-origin assistant
+ * message. Every request must match the addressed item's current version;
+ * a matching no-op returns the stored item without changing its revision.
+ * @param request - target, desired value, and observed item version.
+ * @returns the committed item or an explicit business failure.
+ */
+@Remote('put') put(request: MessageFeedbackPutRequest): Promise
+
+/**
+ * Delete one feedback item. Absence is successful regardless of the
+ * supplied version; an existing item requires an exact version match.
+ * @param request - Session, message, and observed item version.
+ * @returns the stable absent postcondition, or an explicit failure.
+ */
+@Remote('delete') delete(request: MessageFeedbackDeleteRequest): Promise
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:150`](../../packages/feedback/message-feedback/src/index.ts)
+
diff --git a/docs/subsystems/filesystem.i18n.yaml b/docs/subsystems/filesystem.i18n.yaml
index 783f8b8d66..a1899ab15b 100644
--- a/docs/subsystems/filesystem.i18n.yaml
+++ b/docs/subsystems/filesystem.i18n.yaml
@@ -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 docs/subsystems/filesystem.md
-filesystem.md: 00e28130db9f60e6ad5f8c582ca5531482cdeb32
-filesystem.zh.md: 5cf6be12f41b36c8149a941a4d251c4497ed4738
+filesystem.md: 5e491e08604f13b1862b44037a3e9a32d7ab0c96
+filesystem.zh.md: fadbf0178f7dde67d7d5b378c0760141dde28f7a
diff --git a/docs/subsystems/filesystem.md b/docs/subsystems/filesystem.md
index 00e28130db..5e491e0860 100644
--- a/docs/subsystems/filesystem.md
+++ b/docs/subsystems/filesystem.md
@@ -52,7 +52,7 @@ type FsTargetKey = Branded<'FsTargetKey'>
type FsVersion = Branded<'FsVersion'>
```
-`stat` returns metadata (never content), or `undefined` when the target is absent. `type` lets the tool reject directories/special files before reading, and `size` lets it choose `readText` vs `streamText` without probing by failure. A protocol consumer that needs a byte ceiling applies it while consuming `streamText`, so the filesystem seam needs no consumer-specific bounded-read primitive.
+`stat` returns metadata (never content), or `undefined` when the target is absent. `type` lets consumers reject directories and special files before reading, and `size` lets text consumers choose `readText` vs `streamText` without probing by failure. A text consumer applies its own retention ceiling while consuming `streamText`. Raw-byte consumers use `readBytes(target, signal, maxBytes)`; its required complete-content cap makes a known or discovered overflow fail with `FS_TOO_LARGE` instead of truncating or buffering without a bound.
```ts type-equiv
/**
@@ -248,7 +248,7 @@ Filesystem failures use stable `FsErrorCode` strings carried by `FsError` (`Harn
```ts type-equiv
/**
* Stable, machine-routable codes for filesystem failures. Carried on
- * {@link FsError}; the tool registry surfaces `{ name, code }` on `isError`
+ * {@link FsError}; the tool registry exposes `{ name, code }` on `isError`
* results so retry/permission/UI layers can branch without parsing messages.
*/
type FsErrorCode =
@@ -256,6 +256,7 @@ type FsErrorCode =
| 'FS_NOT_DIRECTORY'
| 'FS_NOT_TEXT'
| 'FS_NOT_REGULAR_FILE'
+ | 'FS_TOO_LARGE'
| 'FS_PERMISSION_DENIED'
| 'FS_SANDBOX_DENIED'
| 'FS_IO_ERROR'
@@ -274,15 +275,15 @@ type FsErrorCode =
## The service and the plugin
-`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls from unseen/absent/present state and records `FsObservation` values. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
+`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `readBytes`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls from unseen/absent/present state and records `FsObservation` values. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -373,6 +374,18 @@ abstract readText(target: FsTarget, signal?: AbortSignal): Promise
*/
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise>
+/**
+ * Read the whole regular file as raw bytes with no decoding or binary
+ * rejection. The bound lives at this seam so a backend can never buffer an
+ * unbounded file: a target known or discovered to exceed `maxBytes` fails
+ * with `FS_TOO_LARGE` instead of returning a truncated result.
+ * @param target - the resolved target to read.
+ * @param signal - aborts the read.
+ * @param maxBytes - inclusive byte cap on the complete content.
+ * @returns the full raw content, at most `maxBytes` long.
+ */
+abstract readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise
+
/**
* List direct children of a directory in stable name order. Returns resolved
* child targets plus cheap metadata only; never reads file contents.
diff --git a/docs/subsystems/filesystem.zh.md b/docs/subsystems/filesystem.zh.md
index 5cf6be12f4..fadbf0178f 100644
--- a/docs/subsystems/filesystem.zh.md
+++ b/docs/subsystems/filesystem.zh.md
@@ -52,7 +52,7 @@ type FsTargetKey = Branded<'FsTargetKey'>
type FsVersion = Branded<'FsVersion'>
```
-`stat` 返回元数据(从不返回内容),目标不存在时返回 `undefined`。`type` 让工具在读取前拒绝目录或特殊文件;`size` 让工具无需通过失败探测即可选择 `readText` 还是 `streamText`。需要字节上限的协议消费方在消费 `streamText` 时执行该上限,因此文件系统 seam 无需消费方专用的有界读取原语。
+`stat` 返回元数据(从不返回内容),目标不存在时返回 `undefined`。`type` 让消费方在读取前拒绝目录和特殊文件;`size` 让文本消费方无需通过失败探测即可选择 `readText` 还是 `streamText`。文本消费方在消费 `streamText` 时执行自己的保留量上限。原始字节消费方调用 `readBytes(target, signal, maxBytes)`;其必填的完整内容上限会使已知或读取中发现的超限以 `FS_TOO_LARGE` 失败,不会截断结果或无界缓冲。
```ts type-equiv
/**
@@ -248,7 +248,7 @@ interface FileReadOutcome {
```ts type-equiv
/**
* Stable, machine-routable codes for filesystem failures. Carried on
- * {@link FsError}; the tool registry surfaces `{ name, code }` on `isError`
+ * {@link FsError}; the tool registry exposes `{ name, code }` on `isError`
* results so retry/permission/UI layers can branch without parsing messages.
*/
type FsErrorCode =
@@ -256,6 +256,7 @@ type FsErrorCode =
| 'FS_NOT_DIRECTORY'
| 'FS_NOT_TEXT'
| 'FS_NOT_REGULAR_FILE'
+ | 'FS_TOO_LARGE'
| 'FS_PERMISSION_DENIED'
| 'FS_SANDBOX_DENIED'
| 'FS_IO_ERROR'
@@ -274,15 +275,15 @@ type FsErrorCode =
## 服务与插件
-`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:根据未见/缺失/存在状态对写入与编辑意图 waterfall 作出决策,并记录 `FsObservation` 值。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
+`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`readBytes`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:根据未见/缺失/存在状态对写入与编辑意图 waterfall 作出决策,并记录 `FsObservation` 值。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -373,6 +374,18 @@ abstract readText(target: FsTarget, signal?: AbortSignal): Promise
*/
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise>
+/**
+ * Read the whole regular file as raw bytes with no decoding or binary
+ * rejection. The bound lives at this seam so a backend can never buffer an
+ * unbounded file: a target known or discovered to exceed `maxBytes` fails
+ * with `FS_TOO_LARGE` instead of returning a truncated result.
+ * @param target - the resolved target to read.
+ * @param signal - aborts the read.
+ * @param maxBytes - inclusive byte cap on the complete content.
+ * @returns the full raw content, at most `maxBytes` long.
+ */
+abstract readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise
+
/**
* List direct children of a directory in stable name order. Returns resolved
* child targets plus cheap metadata only; never reads file contents.
diff --git a/docs/subsystems/goal.i18n.yaml b/docs/subsystems/goal.i18n.yaml
index f610740a21..4eeb224596 100644
--- a/docs/subsystems/goal.i18n.yaml
+++ b/docs/subsystems/goal.i18n.yaml
@@ -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 docs/subsystems/goal.md
-goal.md: 93837e15ee3244671cacaed26983e8e4ca38bd56
-goal.zh.md: 8ea138bc80a400120b065f616182acb0906c68dd
+goal.md: 676b2f49d00681ac7f05b894cda0157ebba4cfcd
+goal.zh.md: 7f83e6f58461e99b05f04794e83c0dfa9bd58d1b
diff --git a/docs/subsystems/goal.md b/docs/subsystems/goal.md
index 93837e15ee..676b2f49d0 100644
--- a/docs/subsystems/goal.md
+++ b/docs/subsystems/goal.md
@@ -148,9 +148,9 @@ interface GoalChanged {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/goal.zh.md b/docs/subsystems/goal.zh.md
index 8ea138bc80..7f83e6f584 100644
--- a/docs/subsystems/goal.zh.md
+++ b/docs/subsystems/goal.zh.md
@@ -148,9 +148,9 @@ interface GoalChanged {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/http-server.i18n.yaml b/docs/subsystems/http-server.i18n.yaml
index 4c3975588b..d4969ead3f 100644
--- a/docs/subsystems/http-server.i18n.yaml
+++ b/docs/subsystems/http-server.i18n.yaml
@@ -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 docs/subsystems/http-server.md
-http-server.md: 232755ba6b77ec940ebe2cb7f19fd962018300f1
-http-server.zh.md: 59778e6455d4f32e30c3e77c88cd76a2ecc1845f
+http-server.md: a8a8aece5fd49cda6db0111ac882d83ae07135c4
+http-server.zh.md: 66fc6a6cfa468a3f96a29200bb2fe0aae581f44b
diff --git a/docs/subsystems/http-server.md b/docs/subsystems/http-server.md
index 232755ba6b..a8a8aece5f 100644
--- a/docs/subsystems/http-server.md
+++ b/docs/subsystems/http-server.md
@@ -50,9 +50,9 @@ A request whose handling throws (a malformed %-escape hitting `decodeURIComponen
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/http-server.zh.md b/docs/subsystems/http-server.zh.md
index 59778e6455..66fc6a6cfa 100644
--- a/docs/subsystems/http-server.zh.md
+++ b/docs/subsystems/http-server.zh.md
@@ -50,9 +50,9 @@ interface Config {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/invariants.i18n.yaml b/docs/subsystems/invariants.i18n.yaml
index 2ff4b67060..b103214416 100644
--- a/docs/subsystems/invariants.i18n.yaml
+++ b/docs/subsystems/invariants.i18n.yaml
@@ -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 docs/subsystems/invariants.md
-invariants.md: ccf81f0252dd2db4eec666df6fab8c7d6a830f25
-invariants.zh.md: 046aa4a56d48fa0386fba3a51d850ab21ce5145d
+invariants.md: d6f5f175fb277af09e620b94e8604e04ce2a9d26
+invariants.zh.md: b8e08fcf05f76cd69d0f14d64f38dfd15aa6346a
diff --git a/docs/subsystems/invariants.md b/docs/subsystems/invariants.md
index ccf81f0252..d6f5f175fb 100644
--- a/docs/subsystems/invariants.md
+++ b/docs/subsystems/invariants.md
@@ -62,9 +62,9 @@ Every workspace package owns a `./invariant` companion ([package contract](../..
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/invariants.zh.md b/docs/subsystems/invariants.zh.md
index 046aa4a56d..b8e08fcf05 100644
--- a/docs/subsystems/invariants.zh.md
+++ b/docs/subsystems/invariants.zh.md
@@ -62,9 +62,9 @@ interface InvariantInstaller {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml
index 052ec4fedd..376b1a976c 100644
--- a/docs/subsystems/llm-streaming.i18n.yaml
+++ b/docs/subsystems/llm-streaming.i18n.yaml
@@ -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 docs/subsystems/llm-streaming.md
-llm-streaming.md: 8ae4e8b376b4c4221e6179eb719fcf162f3031e4
-llm-streaming.zh.md: 7d244ab882521a90217873fc3cdee12cd5232db8
+llm-streaming.md: 2063ba2deadada4689161110200af177320fcc15
+llm-streaming.zh.md: c3d1c4d895f35032bfe7602a2880a7922b984a1f
diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md
index 8ae4e8b376..2063ba2dea 100644
--- a/docs/subsystems/llm-streaming.md
+++ b/docs/subsystems/llm-streaming.md
@@ -234,7 +234,7 @@ interface AppIdentity {
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
- /** Public home URL of the app, used as the `User-Agent` comment. */
+ /** Repository home URL of the app, used as the `User-Agent` comment. */
url: string
}
```
@@ -705,15 +705,15 @@ declare abstract class LlmAdapter {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.llm` — `LlmService`
-The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
+The abstract `llm` service: an adapter registry plus a streaming model-call API, interceptable via the `llm/stream` waterfall.
```ts cordis-catalog
/**
diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md
index 7d244ab882..c3d1c4d895 100644
--- a/docs/subsystems/llm-streaming.zh.md
+++ b/docs/subsystems/llm-streaming.zh.md
@@ -238,7 +238,7 @@ interface AppIdentity {
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
- /** Public home URL of the app, used as the `User-Agent` comment. */
+ /** Repository home URL of the app, used as the `User-Agent` comment. */
url: string
}
```
@@ -713,15 +713,15 @@ declare abstract class LlmAdapter {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.llm` — `LlmService`
-The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
+The abstract `llm` service: an adapter registry plus a streaming model-call API, interceptable via the `llm/stream` waterfall.
```ts cordis-catalog
/**
diff --git a/docs/subsystems/permission.i18n.yaml b/docs/subsystems/permission.i18n.yaml
index 3f9c9b533c..ea67c1431c 100644
--- a/docs/subsystems/permission.i18n.yaml
+++ b/docs/subsystems/permission.i18n.yaml
@@ -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 docs/subsystems/permission.md
-permission.md: 6f9a37dbb784ccbaea74056519992e40f9c07c6d
-permission.zh.md: 8eda50fae09a67c41003d7a914fb139c4132a900
+permission.md: 88fd03877d27ba02ef4f449b0500b6395f67977d
+permission.zh.md: e7459844487793a51188e27e2b87b64060d83bcc
diff --git a/docs/subsystems/permission.md b/docs/subsystems/permission.md
index 6f9a37dbb7..88fd03877d 100644
--- a/docs/subsystems/permission.md
+++ b/docs/subsystems/permission.md
@@ -71,9 +71,9 @@ interface PresetOption {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/permission.zh.md b/docs/subsystems/permission.zh.md
index 8eda50fae0..e745984448 100644
--- a/docs/subsystems/permission.zh.md
+++ b/docs/subsystems/permission.zh.md
@@ -71,9 +71,9 @@ interface PresetOption {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/persistence.i18n.yaml b/docs/subsystems/persistence.i18n.yaml
index 65925a1608..888c2666ba 100644
--- a/docs/subsystems/persistence.i18n.yaml
+++ b/docs/subsystems/persistence.i18n.yaml
@@ -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 docs/subsystems/persistence.md
-persistence.md: 0266d17393d07c258036f7054a02c4ab9d3c74a2
-persistence.zh.md: ced83440160ae91ae37025d8024068fb8148b0c6
+persistence.md: 792d50c52ecb2255dee429098d2ef00744479292
+persistence.zh.md: 6e1f29ee6dfc12193e7e5e4e79b4bfc5f751410f
diff --git a/docs/subsystems/persistence.md b/docs/subsystems/persistence.md
index 0266d17393..792d50c52e 100644
--- a/docs/subsystems/persistence.md
+++ b/docs/subsystems/persistence.md
@@ -87,6 +87,10 @@ interface SessionHeader {
}
```
+## Format refusal — logs a build cannot faithfully read
+
+A backend refuses a log it cannot faithfully interpret with `SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged. A header `version` ahead of `SESSION_FORMAT_VERSION` names the direction ("written by a newer harness — upgrade the harness to open it"); one behind it states that this build ships no upgrade path. After legacy-shape normalization, an event type outside this build's generated vocabulary (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog`) refuses the same way unless the event's envelope carries `ignorable: true` — silently skipping an unrecognized required event could change how the rest of the log must be read. The message appends the raw log path when the backend keeps one artifact per session, so the refused text stays reachable. The JSONL backend refuses a foreign version straight from the raw header line, before validating today's header shape or decoding any event row — a structurally different future format still reports the upgrade direction, never "corrupt"; SQLite gates whole-file structure through its own `SCHEMA_VERSION` pragma first. Design rationale and the deferred upgrader chain live in the [session-log-version-mechanism note](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md).
+
## `CreateSessionOptions` — seeding and metadata
Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the optional coarse `origin`, the `delegationDepth`, the `agentPreset` the agent was composed from, and an existing `createdAt`. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
@@ -118,6 +122,22 @@ interface CreateSessionOptions {
Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
+## `SessionRawArtifact` — verbatim stored artifact text
+
+A backend's own artifact text for one session, byte-identical to what it durably wrote (decoded from its physical encoding). `readRaw` returns it without reconstructing from parsed events, so backend-specific serialization (chunk packing, key order, line breaks) survives; backends without a per-session artifact, such as SQLite, inherit the `undefined` default.
+
+```ts type-equiv
+/** A backend's own raw artifact text for one session, verbatim. */
+interface SessionRawArtifact {
+ /** The session header parsed from the artifact's own first line. */
+ readonly meta: SessionHeader
+ /** The artifact's base filename on disk, without any physical encoding suffix. */
+ readonly filename: string
+ /** The artifact's full text content, decoded from the backend's physical encoding. */
+ readonly content: string
+}
+```
+
## Preparation and restoration ownership
`SessionStore.prepare()` accepts ordinary creation options or fresh persistence graphs transferred through `RestoredSessionOptions`. The restoration branch validates and freezes the transferred header and events in place, so callers must retain no mutable aliases. `SessionPreparation` then owns the exact unpublished Session until publication or rollback; disposal is synchronous and idempotent. Persistence inspection exposes only `SessionInspection`, an immutable logical view borrowed from the same prepared Session.
@@ -217,9 +237,9 @@ Both implement the same abstract `SessionPersistence` (locate/create/append/prep
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -237,6 +257,21 @@ Durable append-only session storage. Implementations preserve contiguous, lossle
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
+/**
+ * Read a session's backend-owned artifact text verbatim — the exact durable
+ * bytes the backend wrote (decoded from its physical encoding, e.g. a
+ * decompressed JSONL). The returned `content` is the raw text, not a
+ * reconstruction from parsed events, so it preserves backend-specific
+ * serialization (chunk packing, key order, line breaks). Backends without a
+ * per-session artifact (SQLite) inherit the `undefined` default.
+ * @param _id - the persisted session to read (unused by the default: no
+ * per-session artifact).
+ * @param signal - optional cancellation for backend read work.
+ * @returns the raw artifact plus its parsed header, or `undefined` when the
+ * session is absent or the backend owns no per-session artifact.
+ */
+readRaw(_id: SessionId, signal?: AbortSignal): Promise
+
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a
@@ -342,5 +377,5 @@ abstract listSnapshots(signal?: AbortSignal): Promise
diff --git a/docs/subsystems/persistence.zh.md b/docs/subsystems/persistence.zh.md
index ced8344016..6e1f29ee6d 100644
--- a/docs/subsystems/persistence.zh.md
+++ b/docs/subsystems/persistence.zh.md
@@ -87,6 +87,10 @@ interface SessionHeader {
}
```
+## 格式拒绝:本构建无法可靠读取的日志
+
+后端用 `SessionFormatUnsupportedError` 拒绝无法可靠解读的日志,它与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏。header 的 `version` 比 `SESSION_FORMAT_VERSION` 新时,消息说明方向("由更新的 harness 写入,请升级 harness 后打开");比它旧时说明本构建没有升级路径。经过 legacy 形状归一化后,本构建生成词汇表(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 生成)之外的事件类型同样被拒绝,除非该事件的信封带 `ignorable: true`:静默跳过一个不认识的必需事件可能改变日志其余部分的解读方式。后端为每个会话保留独立文件时,消息附上原始日志路径,被拒绝的文本仍然可读。JSONL 后端直接从原始 header 行拒绝外来版本,先于当前 header 形状校验和任何事件行解码,因此结构完全不同的未来格式仍会报告升级方向,绝不会报"损坏";SQLite 则先由自己的 `SCHEMA_VERSION` pragma 把关整个文件的结构。设计理由与推迟建设的升级器链见 [session-log 版本机制 Agent Note](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)。
+
## `CreateSessionOptions`:seed 与元数据
通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、可选的粗粒度 `origin`、`delegationDepth`、该 agent 所依据组装的 `agentPreset` 以及已有的 `createdAt`。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
@@ -118,6 +122,22 @@ interface CreateSessionOptions {
因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
+## `SessionRawArtifact`——逐字存储工件文本
+
+后端为单个会话自持的工件文本,与其持久化写入的字节逐字一致(按物理编码解码)。`readRaw` 返回它而不从解析后事件重建,因此后端特定的序列化(chunk 打包、键序、换行)得以保留;没有每会话工件的后端(如 SQLite)继承 `undefined` 默认。
+
+```ts type-equiv
+/** A backend's own raw artifact text for one session, verbatim. */
+interface SessionRawArtifact {
+ /** The session header parsed from the artifact's own first line. */
+ readonly meta: SessionHeader
+ /** The artifact's base filename on disk, without any physical encoding suffix. */
+ readonly filename: string
+ /** The artifact's full text content, decoded from the backend's physical encoding. */
+ readonly content: string
+}
+```
+
## 准备与恢复所有权
`SessionStore.prepare()` 接收普通创建选项,或通过 `RestoredSessionOptions` 转移所有权的新鲜持久化对象图。恢复分支会直接验证并冻结转移来的 header 与事件,因此调用方不得保留可变别名。`SessionPreparation` 随后持有该精确的未发布 Session,直至发布或回滚;dispose 是同步且幂等的。持久化检查只暴露 `SessionInspection`,即从同一个已准备 Session 借用的不可变逻辑视图。
@@ -217,9 +237,9 @@ interface SessionPersistenceSnapshot {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -237,6 +257,21 @@ Durable append-only session storage. Implementations preserve contiguous, lossle
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
+/**
+ * Read a session's backend-owned artifact text verbatim — the exact durable
+ * bytes the backend wrote (decoded from its physical encoding, e.g. a
+ * decompressed JSONL). The returned `content` is the raw text, not a
+ * reconstruction from parsed events, so it preserves backend-specific
+ * serialization (chunk packing, key order, line breaks). Backends without a
+ * per-session artifact (SQLite) inherit the `undefined` default.
+ * @param _id - the persisted session to read (unused by the default: no
+ * per-session artifact).
+ * @param signal - optional cancellation for backend read work.
+ * @returns the raw artifact plus its parsed header, or `undefined` when the
+ * session is absent or the backend owns no per-session artifact.
+ */
+readRaw(_id: SessionId, signal?: AbortSignal): Promise
+
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a
@@ -342,5 +377,5 @@ abstract listSnapshots(signal?: AbortSignal): Promise
diff --git a/docs/subsystems/plan.i18n.yaml b/docs/subsystems/plan.i18n.yaml
index ccf85341d1..e0da73ae33 100644
--- a/docs/subsystems/plan.i18n.yaml
+++ b/docs/subsystems/plan.i18n.yaml
@@ -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 docs/subsystems/plan.md
-plan.md: 749f1052119f25e34dcfa8e6939a97037dc3b6dc
-plan.zh.md: 31a402651f5c15b9c0cc264a7a51a020b4e63d60
+plan.md: bedc2a37cac94b4a7b623432fe9eae9ce552c853
+plan.zh.md: 5d6a5bae2a1be83a8ca21a660125822ab97e0c5e
diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md
index 749f105211..bedc2a37ca 100644
--- a/docs/subsystems/plan.md
+++ b/docs/subsystems/plan.md
@@ -42,9 +42,9 @@ When [`ctx.commands`](commands.md) is composed, the plugin registers `/plan [off
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/plan.zh.md b/docs/subsystems/plan.zh.md
index 31a402651f..5d6a5bae2a 100644
--- a/docs/subsystems/plan.zh.md
+++ b/docs/subsystems/plan.zh.md
@@ -42,9 +42,9 @@ interface PlanModeConfig {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/pty.i18n.yaml b/docs/subsystems/pty.i18n.yaml
index 76b4500aea..dce6702f83 100644
--- a/docs/subsystems/pty.i18n.yaml
+++ b/docs/subsystems/pty.i18n.yaml
@@ -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 docs/subsystems/pty.md
-pty.md: 45b157c73926d85d5fc2bb037d3954da7bf75063
-pty.zh.md: 4409ef0b0504780b0b5564a34b61d4f1b2c02775
+pty.md: dcbde73f62de030cbfcc17ae9336a76ff4995ec5
+pty.zh.md: 1c96170f8256a0a6fe40d3909519a120321f4142
diff --git a/docs/subsystems/pty.md b/docs/subsystems/pty.md
index 45b157c739..dcbde73f62 100644
--- a/docs/subsystems/pty.md
+++ b/docs/subsystems/pty.md
@@ -94,9 +94,9 @@ interface PtySendResult {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/pty.zh.md b/docs/subsystems/pty.zh.md
index 4409ef0b05..1c96170f82 100644
--- a/docs/subsystems/pty.zh.md
+++ b/docs/subsystems/pty.zh.md
@@ -94,9 +94,9 @@ interface PtySendResult {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/sandbox.i18n.yaml b/docs/subsystems/sandbox.i18n.yaml
index 32bedb3e94..3e2089e635 100644
--- a/docs/subsystems/sandbox.i18n.yaml
+++ b/docs/subsystems/sandbox.i18n.yaml
@@ -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 docs/subsystems/sandbox.md
-sandbox.md: 20e0f36a5edb211ea409208d4e5e4a9be2e91d46
-sandbox.zh.md: 5f5465af46aa88d72b4a39f728f18855156b24ba
+sandbox.md: 0b770c89b176b6459d6ea2f8a85cd2e18a8800b8
+sandbox.zh.md: 45510c6636daf279582017009742fb6ef4b3e2ce
diff --git a/docs/subsystems/sandbox.md b/docs/subsystems/sandbox.md
index 20e0f36a5e..0b770c89b1 100644
--- a/docs/subsystems/sandbox.md
+++ b/docs/subsystems/sandbox.md
@@ -2,13 +2,13 @@
English | [中文](sandbox.zh.md)
-The process-sandbox seam of [dsh-sandbox](../../packages/sandbox/sandbox) wraps a same-world subprocess argv in a file-effect policy without coupling consumers to a platform runner. [dsh-sandbox-local](../../packages/sandbox/sandbox-local) supplies the Linux bwrap/Landlock and macOS Seatbelt backends; [dsh-bash-sandbox](../../packages/bash/bash-sandbox) is the first consumer. Containers, microVMs, and remote execution are sibling implementations of whole capability seams, not providers of `ctx.sandbox`.
+The process-sandbox seam of [dsh-sandbox](../../packages/sandbox/sandbox) wraps a same-world subprocess argv in a file-effect policy without coupling consumers to a platform runner. [dsh-sandbox-local](../../packages/sandbox/sandbox-local) supplies Linux bwrap/Landlock, macOS Seatbelt, and the Windows ACL restricted-token backend; [dsh-bash-sandbox](../../packages/bash/bash-sandbox) and [dsh-pwsh-sandbox](../../packages/bash/pwsh-sandbox) consume it. Containers, microVMs, and remote execution are sibling implementations of whole capability seams, not providers of `ctx.sandbox`.
Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox/src/index.ts)
## Modes and enforcement
-`SandboxMode` governs filesystem effects only. `read-only` denies every write — the POSIX runners additionally grant the `/dev/null` sink their shells require, while the Windows ACL runner grants nothing; `workspace-write` permits writes under the workspace root and the backend's promised temp area; `danger-full-access` bypasses confinement. Network and process visibility are outside this vocabulary.
+`SandboxMode` governs filesystem effects only. `read-only` asks the backend to deny writes — the POSIX runners additionally grant the `/dev/null` sink their shells require, while the Windows ACL runner grants no explicit writable root and reports partial enforcement for its ambient ACL gaps; `workspace-write` permits writes under the workspace root and the backend's promised temp area; `danger-full-access` bypasses confinement. Network and process visibility are outside this vocabulary.
```ts type-equiv
/**
@@ -27,7 +27,7 @@ Only the first two modes can be sent to a provider. A `danger-full-access` consu
type ConfinedSandboxMode = Exclude
```
-Enforcement is a reported fact. `full` means the backend governs every file effect promised by the mode; `partial` means an active backend or older kernel ABI governs only a subset, so consumers that require the absolute promise must reject or surface that distinction.
+Enforcement is a reported fact. `full` means the backend governs every file effect promised by the mode; `partial` means an active backend or older kernel ABI governs only a subset, so consumers that require the absolute promise must reject or surface that distinction. Older Landlock ABIs and the Windows ACL runner's Everyone/hard-link boundaries are current partial cases.
```ts type-equiv
/**
@@ -55,10 +55,10 @@ interface SandboxExecutionPolicy {
workspaceRoot: string
/**
* Opaque identity of the calling session (the branded `dsh-session`
- * SessionId). Backends key per-session state off it (e.g. the windows-acl
- * per-session private temp subdirectory — the write grant itself is
- * per-workspace, derived from the workspace root); absent for agentless
- * calls, which fall back to per-call backend state.
+ * SessionId). Backends key per-session state off it (e.g. windows-acl gives
+ * each live session/workspace pair a random private temp directory and SID,
+ * while the workspace SID and standing grant remain per-workspace); absent
+ * for agentless calls, which fall back to per-call backend state.
*/
sessionId?: SessionId
}
@@ -159,9 +159,9 @@ Provider selection, probing, caching, and backend-specific enforcement reports b
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/sandbox.zh.md b/docs/subsystems/sandbox.zh.md
index 5f5465af46..45510c6636 100644
--- a/docs/subsystems/sandbox.zh.md
+++ b/docs/subsystems/sandbox.zh.md
@@ -2,13 +2,13 @@
[English](sandbox.md) | 中文
-[dsh-sandbox](../../packages/sandbox/sandbox) 的进程沙箱 seam 将与宿主共享文件系统和内核的子进程 argv 包装在文件效果策略中,而不将消费方耦合到特定平台运行器。[dsh-sandbox-local](../../packages/sandbox/sandbox-local) 提供 Linux bwrap/Landlock 与 macOS Seatbelt 后端;[dsh-bash-sandbox](../../packages/bash/bash-sandbox) 是第一个消费方。容器、microVM 和远程执行是完整能力 seam 的同级实现,而非 `ctx.sandbox` 的提供方。
+[dsh-sandbox](../../packages/sandbox/sandbox) 的进程沙箱 seam 将与宿主共享文件系统和内核的子进程 argv 包装在文件效果策略中,而不将消费方耦合到特定平台运行器。[dsh-sandbox-local](../../packages/sandbox/sandbox-local) 提供 Linux bwrap/Landlock、macOS Seatbelt 与 Windows ACL 受限令牌后端;[dsh-bash-sandbox](../../packages/bash/bash-sandbox) 和 [dsh-pwsh-sandbox](../../packages/bash/pwsh-sandbox) 是其消费方。容器、microVM 和远程执行是完整能力 seam 的同级实现,而非 `ctx.sandbox` 的提供方。
源码:[`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox/src/index.ts)
## 模式与强制执行
-`SandboxMode` 仅管控文件系统效果。`read-only` 拒绝所有写入——POSIX runner 还会授予其 shell 所需的 `/dev/null` 接收器,而 Windows ACL runner 不授予任何写入;`workspace-write` 允许在工作区根目录及后端承诺的临时区域下写入;`danger-full-access` 绕过隔离。网络与进程可见性不在此处的定义范围内。
+`SandboxMode` 仅管控文件系统效果。`read-only` 要求后端拒绝写入——POSIX runner 还会授予其 shell 所需的 `/dev/null` 接收器,而 Windows ACL runner 不授予任何显式可写根目录,并因环境 ACL 缺口报告部分强制执行;`workspace-write` 允许在工作区根目录及后端承诺的临时区域下写入;`danger-full-access` 绕过隔离。网络与进程可见性不在此处的定义范围内。
```ts type-equiv
/**
@@ -27,7 +27,7 @@ type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
type ConfinedSandboxMode = Exclude
```
-强制执行完整性是后端报告的事实。`full` 表示后端管控了该模式承诺的所有文件效果;`partial` 表示活跃后端或较旧的内核 ABI 仅管控其中一个子集,因此要求绝对保证的消费方必须拒绝或向上暴露这一区别。
+强制执行完整性是后端报告的事实。`full` 表示后端管控了该模式承诺的所有文件效果;`partial` 表示活跃后端或较旧的内核 ABI 仅管控其中一个子集,因此要求绝对保证的消费方必须拒绝或向上暴露这一区别。当前的部分强制执行情形包括较旧的 Landlock ABI,以及 Windows ACL runner 的 Everyone 与硬链接边界。
```ts type-equiv
/**
@@ -55,10 +55,10 @@ interface SandboxExecutionPolicy {
workspaceRoot: string
/**
* Opaque identity of the calling session (the branded `dsh-session`
- * SessionId). Backends key per-session state off it (e.g. the windows-acl
- * per-session private temp subdirectory — the write grant itself is
- * per-workspace, derived from the workspace root); absent for agentless
- * calls, which fall back to per-call backend state.
+ * SessionId). Backends key per-session state off it (e.g. windows-acl gives
+ * each live session/workspace pair a random private temp directory and SID,
+ * while the workspace SID and standing grant remain per-workspace); absent
+ * for agentless calls, which fall back to per-call backend state.
*/
sessionId?: SessionId
}
@@ -159,9 +159,9 @@ interface ConfinedArgv {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/scope.i18n.yaml b/docs/subsystems/scope.i18n.yaml
index db073233b5..3965f6adb7 100644
--- a/docs/subsystems/scope.i18n.yaml
+++ b/docs/subsystems/scope.i18n.yaml
@@ -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 docs/subsystems/scope.md
-scope.md: 73a697f2843293daffff85dabf4656346f7dcd04
+scope.md: c425217de224839d719da213e33f58bd2e6bf002
scope.zh.md: 06e2528eca958102110caa3a7d370e0778c0f7b2
diff --git a/docs/subsystems/scope.md b/docs/subsystems/scope.md
index 73a697f284..c425217de2 100644
--- a/docs/subsystems/scope.md
+++ b/docs/subsystems/scope.md
@@ -28,7 +28,7 @@ type Scoped = object & { readonly [ScopedBrand]: T }
## Owned registration context
-`Scope` pairs the tagged registration context with two teardown surfaces. `rawDispose` preserves the exact Cordis disposer identity needed by an ordered composite effect; `dispose()` is the public shared quiescence boundary for direct and racing callers.
+`Scope` pairs the tagged registration context with two teardown paths. `rawDispose` preserves the exact Cordis disposer identity needed by an ordered composite effect; `dispose()` is the public shared quiescence boundary for direct and racing callers.
```ts type-equiv
/** A minted registration scope and its quiescent disposal boundaries. */
diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml
index bd07ed71fb..ec94be68a8 100644
--- a/docs/subsystems/session-projection.i18n.yaml
+++ b/docs/subsystems/session-projection.i18n.yaml
@@ -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 docs/subsystems/session-projection.md
-session-projection.md: 6fdcfc64a5265c36f4396d91ee689c5f1cd1da18
-session-projection.zh.md: 3ca65ba40be32ed11e319c05410c035683f46488
+session-projection.md: 281e7630eed6480de07a66bf7050798c83396f77
+session-projection.zh.md: 71a8a3f85d698b2d0828a797bc81069a0617d17b
diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md
index 6fdcfc64a5..281e7630ee 100644
--- a/docs/subsystems/session-projection.md
+++ b/docs/subsystems/session-projection.md
@@ -96,9 +96,9 @@ type ProjectionChangeListener = (
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md
index 3ca65ba40b..71a8a3f85d 100644
--- a/docs/subsystems/session-projection.zh.md
+++ b/docs/subsystems/session-projection.zh.md
@@ -96,9 +96,9 @@ type ProjectionChangeListener = (
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session-query.i18n.yaml b/docs/subsystems/session-query.i18n.yaml
index e69b3ef46f..32f4c5a459 100644
--- a/docs/subsystems/session-query.i18n.yaml
+++ b/docs/subsystems/session-query.i18n.yaml
@@ -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 docs/subsystems/session-query.md
-session-query.md: 7a414c89124dde9972396f77281f50bf1643d716
-session-query.zh.md: e8b98709c98bbd013730d4bf3c9807a418680d7d
+session-query.md: cd21b4c01dc70e0a1e5c498f321694472aad77d9
+session-query.zh.md: 3f18cda064ecf5fbaa635cc0fd768596c74cfeb1
diff --git a/docs/subsystems/session-query.md b/docs/subsystems/session-query.md
index 7a414c8912..cd21b4c01d 100644
--- a/docs/subsystems/session-query.md
+++ b/docs/subsystems/session-query.md
@@ -359,9 +359,9 @@ type SessionQueryErrorCode =
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session-query.zh.md b/docs/subsystems/session-query.zh.md
index e8b98709c9..3f18cda064 100644
--- a/docs/subsystems/session-query.zh.md
+++ b/docs/subsystems/session-query.zh.md
@@ -359,9 +359,9 @@ type SessionQueryErrorCode =
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session-reference.i18n.yaml b/docs/subsystems/session-reference.i18n.yaml
index a7f5e0fe0b..5a9663a288 100644
--- a/docs/subsystems/session-reference.i18n.yaml
+++ b/docs/subsystems/session-reference.i18n.yaml
@@ -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 docs/subsystems/session-reference.md
-session-reference.md: 29fd85c74c54b0542a241b02cd31fc6c93012ae8
-session-reference.zh.md: 2a551de5d7bd08ed70b56035b617b2d69c9d6244
+session-reference.md: 59a1b76ea4ac69c465101d6e1c2e2f913a5c3130
+session-reference.zh.md: fc74821cb37df133ff830a742ed8da36878f7cad
diff --git a/docs/subsystems/session-reference.md b/docs/subsystems/session-reference.md
index 29fd85c74c..59a1b76ea4 100644
--- a/docs/subsystems/session-reference.md
+++ b/docs/subsystems/session-reference.md
@@ -70,9 +70,9 @@ type SessionReferenceErrorCode =
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session-reference.zh.md b/docs/subsystems/session-reference.zh.md
index 2a551de5d7..fc74821cb3 100644
--- a/docs/subsystems/session-reference.zh.md
+++ b/docs/subsystems/session-reference.zh.md
@@ -70,9 +70,9 @@ type SessionReferenceErrorCode =
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session-title.i18n.yaml b/docs/subsystems/session-title.i18n.yaml
index 29804cae7e..8fa4916c0a 100644
--- a/docs/subsystems/session-title.i18n.yaml
+++ b/docs/subsystems/session-title.i18n.yaml
@@ -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 docs/subsystems/session-title.md
-session-title.md: 339177b03a03b7d82375f7a293934cb164e9f266
-session-title.zh.md: b1353025a4f621e9a3117c0406ad304c35828a49
+session-title.md: 5c76b2bdcb5b511caee7b528bbf87156bbfabe91
+session-title.zh.md: 383a14c63d56d708bfe9bd663708b8de3643a6be
diff --git a/docs/subsystems/session-title.md b/docs/subsystems/session-title.md
index 339177b03a..5c76b2bdcb 100644
--- a/docs/subsystems/session-title.md
+++ b/docs/subsystems/session-title.md
@@ -149,9 +149,9 @@ interface SessionTitleProvider {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session-title.zh.md b/docs/subsystems/session-title.zh.md
index b1353025a4..383a14c63d 100644
--- a/docs/subsystems/session-title.zh.md
+++ b/docs/subsystems/session-title.zh.md
@@ -149,9 +149,9 @@ interface SessionTitleProvider {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml
index 7ba2ae289d..f5659afd13 100644
--- a/docs/subsystems/session.i18n.yaml
+++ b/docs/subsystems/session.i18n.yaml
@@ -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 docs/subsystems/session.md
-session.md: 0b78e51ebf6e2ad5c312268ad4bfb4392b0486df
-session.zh.md: d1e91f684a835e08406f524efe876baa1a6a72cb
+session.md: bdedaeb67434fb39f9f8e4925d3e62c3024bbd93
+session.zh.md: 7756ee772363f5f2759ec0e8826201fdfb49d459
diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md
index 0b78e51ebf..bdedaeb674 100644
--- a/docs/subsystems/session.md
+++ b/docs/subsystems/session.md
@@ -215,6 +215,17 @@ type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
@@ -595,9 +606,9 @@ The backends that consume this contract are on [persistence.md](persistence.md).
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -733,7 +744,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
-Source: [`packages/core/session/src/index.ts:810`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:792`](../../packages/core/session/src/index.ts)
@@ -762,7 +773,7 @@ Creation announcement during session publication. A synchronous throw vetoes and
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:74`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:54`](../../packages/core/session/src/index.ts)
@@ -785,7 +796,7 @@ Emitted once when an announced session leaves the store, including publication r
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:84`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:64`](../../packages/core/session/src/index.ts)
@@ -810,7 +821,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:76`](../../packages/core/session/src/index.ts)
@@ -832,5 +843,5 @@ Awaited parallel durability checkpoint: every listener runs and the caller await
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:105`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:85`](../../packages/core/session/src/index.ts)
diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md
index d1e91f684a..7756ee7723 100644
--- a/docs/subsystems/session.zh.md
+++ b/docs/subsystems/session.zh.md
@@ -217,6 +217,17 @@ type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
@@ -599,9 +610,9 @@ interface TurnEndReasonMap {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -737,7 +748,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
-Source: [`packages/core/session/src/index.ts:810`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:792`](../../packages/core/session/src/index.ts)
@@ -766,7 +777,7 @@ Creation announcement during session publication. A synchronous throw vetoes and
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:74`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:54`](../../packages/core/session/src/index.ts)
@@ -789,7 +800,7 @@ Emitted once when an announced session leaves the store, including publication r
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:84`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:64`](../../packages/core/session/src/index.ts)
@@ -814,7 +825,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:76`](../../packages/core/session/src/index.ts)
@@ -836,5 +847,5 @@ Awaited parallel durability checkpoint: every listener runs and the caller await
Types: [Scoped](scope.md)
-Source: [`packages/core/session/src/index.ts:105`](../../packages/core/session/src/index.ts)
+Source: [`packages/core/session/src/index.ts:85`](../../packages/core/session/src/index.ts)
diff --git a/docs/subsystems/settings.i18n.yaml b/docs/subsystems/settings.i18n.yaml
index 3fa99a4ecf..8d243e8eb2 100644
--- a/docs/subsystems/settings.i18n.yaml
+++ b/docs/subsystems/settings.i18n.yaml
@@ -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 docs/subsystems/settings.md
-settings.md: fc10f72c0ed59a982817eb65ecc410e039a2cbb3
-settings.zh.md: 96bb2d8b0cd65e1ebd812e000fe3b96e8b315493
+settings.md: 6499c8260ad90c6aa4576d76abe30f261fdb0192
+settings.zh.md: 2b9f89d54feebe4436a182ff9e42d31ac080101d
diff --git a/docs/subsystems/settings.md b/docs/subsystems/settings.md
index fc10f72c0e..6499c8260a 100644
--- a/docs/subsystems/settings.md
+++ b/docs/subsystems/settings.md
@@ -165,9 +165,9 @@ type SettingsUpdateSource = 'update' | 'provider'
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/settings.zh.md b/docs/subsystems/settings.zh.md
index 96bb2d8b0c..2b9f89d54f 100644
--- a/docs/subsystems/settings.zh.md
+++ b/docs/subsystems/settings.zh.md
@@ -165,9 +165,9 @@ type SettingsUpdateSource = 'update' | 'provider'
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/skills.i18n.yaml b/docs/subsystems/skills.i18n.yaml
index 6f089b7ff4..8ddc5c16b4 100644
--- a/docs/subsystems/skills.i18n.yaml
+++ b/docs/subsystems/skills.i18n.yaml
@@ -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 docs/subsystems/skills.md
-skills.md: f222da732ba3800a214e236bb7a64d5709067cdb
-skills.zh.md: f20c68596f5350ac33c2956dfb124ae6c8972882
+skills.md: 05cc0575544a66f48f85df316e3b35a2e1a55b25
+skills.zh.md: fc09aa970a7707ee6169512da48ff33cbcea9125
diff --git a/docs/subsystems/skills.md b/docs/subsystems/skills.md
index f222da732b..05cc057554 100644
--- a/docs/subsystems/skills.md
+++ b/docs/subsystems/skills.md
@@ -238,9 +238,9 @@ The model-facing `skill({ name })` tool validates the kebab-case name, finds the
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/skills.zh.md b/docs/subsystems/skills.zh.md
index f20c68596f..fc09aa970a 100644
--- a/docs/subsystems/skills.zh.md
+++ b/docs/subsystems/skills.zh.md
@@ -238,9 +238,9 @@ interface Config {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/spill.i18n.yaml b/docs/subsystems/spill.i18n.yaml
index da210bd7d2..af9f45236c 100644
--- a/docs/subsystems/spill.i18n.yaml
+++ b/docs/subsystems/spill.i18n.yaml
@@ -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 docs/subsystems/spill.md
-spill.md: bc323f8d2591e93a56e938c245274ef6be86085f
-spill.zh.md: f81d5e5de3633561a5a090a14c39b43268b8c63f
+spill.md: 37fd446deb7da94eb09d3f7d198f9f9e44e7856d
+spill.zh.md: e07a16ce3d7b2d94a0a2448b09a20c66e83c1830
diff --git a/docs/subsystems/spill.md b/docs/subsystems/spill.md
index bc323f8d25..37fd446deb 100644
--- a/docs/subsystems/spill.md
+++ b/docs/subsystems/spill.md
@@ -88,9 +88,9 @@ The local backend ([dsh-spill-local](../../packages/spill/spill-local)) writes u
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/spill.zh.md b/docs/subsystems/spill.zh.md
index f81d5e5de3..e07a16ce3d 100644
--- a/docs/subsystems/spill.zh.md
+++ b/docs/subsystems/spill.zh.md
@@ -88,9 +88,9 @@ type SpillLocator = Branded<'SpillLocator'>
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/storage.i18n.yaml b/docs/subsystems/storage.i18n.yaml
index 1aaffa63cc..31f5adfc9b 100644
--- a/docs/subsystems/storage.i18n.yaml
+++ b/docs/subsystems/storage.i18n.yaml
@@ -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 docs/subsystems/storage.md
-storage.md: fd5a8d7eaeb545ef307434b211556633bba8b503
-storage.zh.md: 4ce5ebb2ad59315d2a60d57de087f3050c323722
+storage.md: 33933f70176e1f4960eb423f9a1067f50b6762e4
+storage.zh.md: 0205468445166bddd646d05088ecbb935f656eb9
diff --git a/docs/subsystems/storage.md b/docs/subsystems/storage.md
index fd5a8d7eae..33933f7017 100644
--- a/docs/subsystems/storage.md
+++ b/docs/subsystems/storage.md
@@ -128,9 +128,9 @@ type DomainChanged = DomainChangedPut | DomainChangedDeleted
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/storage.zh.md b/docs/subsystems/storage.zh.md
index 4ce5ebb2ad..0205468445 100644
--- a/docs/subsystems/storage.zh.md
+++ b/docs/subsystems/storage.zh.md
@@ -128,9 +128,9 @@ type DomainChanged = DomainChangedPut | DomainChangedDeleted
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml
index ddbc9ec3db..98064e82f1 100644
--- a/docs/subsystems/subagent.i18n.yaml
+++ b/docs/subsystems/subagent.i18n.yaml
@@ -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 docs/subsystems/subagent.md
-subagent.md: 961a16f58cb936205290d23ce6b9d94cb94607d5
-subagent.zh.md: ac97d27d9824ca62701e11ae99264adbddafbbd8
+subagent.md: 647b1034eae6f305c50113dab7f20e4797a7dc9e
+subagent.zh.md: e3a2a10489a1bd72f62f94b128593f1e03446319
diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md
index 961a16f58c..647b1034ea 100644
--- a/docs/subsystems/subagent.md
+++ b/docs/subsystems/subagent.md
@@ -209,6 +209,27 @@ interface SubagentReportMessageSource {
type SubagentReportDelivery = 'quiet' | 'wakeup'
```
+Reporting is the child's own choice, so the manager keeps a separate account of its own: when a resident Activation settles, it delivers one notice to the child's durable direct parent describing how that epoch ended and carrying its final assistant content. That delivery is unconditional for every child whose id a caller received, happens before the ownership release that would let the parent be judged settled, and reaches a resident parent through the same waking-admission accounting as a report. A parent whose own lineage is already tearing down receives it without a wake, because waking a quiescent Agent starts a turn rather than queueing work. Its provenance is a distinct kind so a transcript never presents a runtime account as something the child wrote.
+
+```ts type-equiv
+/**
+ * Durable attribution for the runtime's own account of a continuable child
+ * settling. Deliberately a different kind from
+ * {@link SubagentReportMessageSource}: a report is content the child chose,
+ * while this message is the manager stating what became of the child, and a
+ * transcript that merged them would credit the child with words it never wrote.
+ */
+interface SubagentSettledMessageSource {
+ readonly kind: 'subagent-settled'
+ /** A runtime account shown without expanding the row (`notice` context form). */
+ readonly form: 'notice'
+ /** One-line account of how the child ended. */
+ readonly summary: string
+ /** Session id of the child that settled. */
+ readonly senderSessionId: SessionId
+}
+```
+
```ts type-equiv
/** Options for one continuable child's report to its direct parent. */
interface SubagentReportOptions {
@@ -265,7 +286,7 @@ A local one-shot provider appends the descriptor inside the child's initial turn
## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries
-`SubagentService.listChildren(parentSessionId)` enumerates the parent's direct session-backed subagents from the live-preferred merge of `ctx.sessions.list()` and optional `ctx.sessionPersistence.list()` — no query service, and no Agent is loaded or resumed. Candidates are the direct children whose durable header carries `origin: 'subagent'`; the marker classifies enumeration and coarse generic-route denial but cannot establish a valid descriptor, resumability, or authorization — the projection fold owns identity, and the Activation contract owns resume. Each row's `mode`/`label` is the registered `subagent` projection unit's value, served through a three-rung ladder: the registry's watermark cache for a live child (zero log reads); the optional projection checkpoint cache for a cold one (`cachedSnapshot` — an identity passing the own-suffix seq gate is final, because an own descriptor is immutable once appended); otherwise one `persistence.inspect()` reading folded through the registry (bounded concurrency, recomputed per listing). The cache is a pure optional accelerator: absent, serving the `null` sentinel or missing the key, failing the seq gate, or faulting, it falls silently through to the authoritative refold. The fold is `subagent/descriptor` last-wins with no failure channel: the child's own descriptor overrides a fork-seeded ancestor's, and a malformed or unknown-version payload folds to a serializable `null` sentinel, treated as no value. The result is one `SubagentListEntry[]` in `createdAt`-then-id order: a served identity yields a `child` entry with `mode: 'one-shot' | 'continuable'` and `activity: 'running' | 'inactive'`; continuable entries always carry `label`, while one-shot entries carry it only when the start caller supplied presentation metadata. A settled candidate whose fold served no identity yields a `corrupt` diagnostic — missing, malformed, and unknown-version descriptors deliberately undistinguished (`unsupported` remains in the type but is never produced); a running candidate without an identity is omitted (the creation window before its descriptor lands); a failed cold inspection yields one `unavailable` diagnostic retried on the next listing, so one damaged sibling cannot hide healthy children. `hasChildren` marks a direct descendant with durable subagent origin, read from the same merged material. Activity snapshots only whether the logical record is live in `ctx.sessions`, not outcome or resumability. Absent persistence, enumeration is live-only rather than an error — a cold child cannot be resumed then either. `listChildren()` throws `SubagentError` with code `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` when the `ctx.sessionProjections` registry is absent and `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` when the session store is, both checked before any read so a deployment with zero children still fails deterministically; the list tool requires `ctx.subagents` and `ctx.agents` at plugin load. A service consumer such as a UI can display both modes and choose an unlabeled one-shot fallback, while the model-facing `list_agents` adapter (the separately loadable `/list-agents` plugin of [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)) keeps only continuable entries and refines status through the live Agent registry's `running`/`idle`/`complete` vocabulary. Listing does not consult the continuation manager's Activation map, Agent registry, or provider availability; `send_message` remains the authoritative delivery-time operation, and a listed running continuable child may still reject delivery as an ownership conflict. The read-path rationale lives in [the list-identity-projection Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md).
+`SubagentService.listChildren(parentSessionId)` enumerates the parent's direct session-backed subagents from the live-preferred merge of `ctx.sessions.list()` and optional `ctx.sessionPersistence.list()` — no query service, and no Agent is loaded or resumed. Candidates are the direct children whose durable header carries `origin: 'subagent'`; the marker classifies enumeration and coarse generic-route denial but cannot establish a valid descriptor, resumability, or authorization — the projection fold owns identity, and the Activation contract owns resume. Each row's `mode`/`label` is the registered `subagent` projection unit's value, served through a three-rung ladder: the registry's watermark cache for a live child (zero log reads); the optional projection checkpoint cache for a cold one (`cachedSnapshot` — an identity passing the own-suffix seq gate is final, because an own descriptor is immutable once appended); otherwise one `persistence.inspect()` reading folded through the registry (bounded concurrency, recomputed per listing). The cache is a pure optional accelerator: absent, serving the `null` sentinel or missing the key, failing the seq gate, or faulting, it falls silently through to the authoritative refold. The fold is `subagent/descriptor` last-wins with no failure channel: the child's own descriptor overrides a fork-seeded ancestor's, and a malformed or unknown-version payload folds to a serializable `null` sentinel, treated as no value. The result is one `SubagentListEntry[]` in `createdAt`-then-id order: a served identity yields a `child` entry with `mode: 'one-shot' | 'continuable'` and `activity: 'running' | 'inactive'`; continuable entries always carry `label`, while one-shot entries carry it only when the start caller supplied presentation metadata. A settled candidate whose fold served no identity yields a `corrupt` diagnostic — missing, malformed, and unknown-version descriptors deliberately undistinguished (`unsupported` remains in the type but is never produced); a running candidate without an identity is omitted (the creation window before its descriptor lands); a failed cold inspection yields one `unavailable` diagnostic retried on the next listing, so one damaged sibling cannot hide healthy children. `hasChildren` marks a direct descendant with durable subagent origin, read from the same merged material. Activity snapshots only whether the logical record is live in `ctx.sessions`, not outcome or resumability. Absent persistence, enumeration is live-only rather than an error — a cold child cannot be resumed then either. `listChildren()` throws `SubagentError` with code `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` when the `ctx.sessionProjections` registry is absent and `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` when the session store is, both checked before any read so a deployment with zero children still fails deterministically; the list tool requires `ctx.subagents` and `ctx.agents` at plugin load. A service consumer such as a UI can display both modes and choose an unlabeled one-shot fallback, while the model-facing `list_agents` adapter (the separately loadable `/list-agents` plugin of [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)) keeps only continuable entries and refines status through the live Agent registry into its own `running`/`idle`/`ready` vocabulary, whose `ready` names a storage-only child as resumable rather than terminal. Listing does not consult the continuation manager's Activation map, Agent registry, or provider availability; `send_message` remains the authoritative delivery-time operation, and a listed running continuable child may still reject delivery as an ownership conflict. The read-path rationale lives in [the list-identity-projection Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md).
`SubagentService.listDescendants(rootSessionId)` applies the same live-preferred corpus and projection-backed interpretation to the root's complete descendant tree in stable pre-order. Ordinary sessions and one-shot children remain traversal nodes, so continuable descendants below them are discovered; only `origin: 'subagent'` candidates produce rows. Each returned child or diagnostic adds its position from the enumerated durable header, while a cold inspection revalidates that complete lifecycle before serving identity:
@@ -293,7 +314,12 @@ The outcome of a one-shot run, resolved by `SubagentRun.result`. `structured` is
* The terminal outcome of a subagent run, resolved by {@link SubagentRun.result}.
*/
interface SubagentResult {
- /** The child's final assistant output (the last assistant message's content). */
+ /**
+ * The child's final assistant output is the content of its last non-empty
+ * assistant message. Empty-content messages, including usage-only messages,
+ * are skipped. Without a non-empty message, the output is its accumulated
+ * assistant text stream, or `[]` when the child produced neither.
+ */
readonly output: ContentBlock[]
/**
* The structured result after a requested `outputSchema` was successfully
@@ -385,7 +411,10 @@ Each provider is a named child-agent transport, and multiple providers may coexi
/**
* One registered transport for running child agents. Providers are trusted
* same-process implementations; callers treat descriptors and returned values
- * as borrowed immutable data.
+ * as borrowed immutable data. The service may call one provider concurrently
+ * for distinct children. Providers isolate operation-local mutable state; a
+ * shared capacity controller may delay an operation but must not couple its
+ * settlement or cleanup to a sibling.
*/
interface SubagentProvider {
/** Unique registry name (e.g. `spawn`, `fork`, `acp`). */
@@ -406,7 +435,8 @@ interface SubagentProvider {
* initial turn. Before fulfillment, the provider owns setup and cleans any
* unpublished partial resources before rejecting. Ownership transfers on
* fulfillment; subsequent turn or infrastructure failure settles through
- * the returned run.
+ * the returned run. Distinct starts may overlap; cancellation, failure,
+ * result settlement, and disposal remain independent for each run.
*/
start(request: ResolvedSubagentStartRequest): Promise
/**
@@ -421,6 +451,8 @@ interface SubagentProvider {
* continuation manager owns identity reservation, composition, Agent
* creation, prompt delivery, cold resume, ownership, and disposal, so a
* provider never sees the child's Agent, handle, turns, or teardown.
+ * Distinct preparations may overlap; each follows its own signal and returns
+ * data belonging only to `request.sessionId`.
*/
prepareContinuable?(request: ContinuableCreateRequest): Promise
}
@@ -439,9 +471,9 @@ The spawn and fork backends create an ordinary one-shot agent through `parent.ct
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -614,7 +646,7 @@ async start(name: string, request: SubagentStartRequest): Promise
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [MessageId](llm-streaming.md) · [SessionId](core.md)
-Source: [`packages/subagent/subagent/src/index.ts:167`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:171`](../../packages/subagent/subagent/src/index.ts)
@@ -640,7 +672,7 @@ A published child settled. Scope-filtered dispatch uses the same delegating pare
Types: [Scoped](scope.md)
-Source: [`packages/subagent/subagent/src/index.ts:162`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:166`](../../packages/subagent/subagent/src/index.ts)
@@ -657,7 +689,7 @@ A provider became resolvable in the registry.
'subagent/provider-added'(provider: SubagentProvider): void
```
-Source: [`packages/subagent/subagent/src/index.ts:136`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:140`](../../packages/subagent/subagent/src/index.ts)
@@ -674,7 +706,7 @@ A provider left the registry. Accepted runs remain holder-owned.
'subagent/provider-removed'(name: string): void
```
-Source: [`packages/subagent/subagent/src/index.ts:142`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:146`](../../packages/subagent/subagent/src/index.ts)
@@ -698,5 +730,5 @@ A provider established a published child. For in-process providers, `ctx.agents.
Types: [Scoped](scope.md)
-Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:157`](../../packages/subagent/subagent/src/index.ts)
diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md
index ac97d27d98..e3a2a10489 100644
--- a/docs/subsystems/subagent.zh.md
+++ b/docs/subsystems/subagent.zh.md
@@ -209,6 +209,27 @@ interface SubagentReportMessageSource {
type SubagentReportDelivery = 'quiet' | 'wakeup'
```
+上报是 child 自己的选择,因此管理器还保有一份属于自己的记账:当驻留 Activation 结算时,它会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与上报相同的唤醒准入记账到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个静息 Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
+
+```ts type-equiv
+/**
+ * Durable attribution for the runtime's own account of a continuable child
+ * settling. Deliberately a different kind from
+ * {@link SubagentReportMessageSource}: a report is content the child chose,
+ * while this message is the manager stating what became of the child, and a
+ * transcript that merged them would credit the child with words it never wrote.
+ */
+interface SubagentSettledMessageSource {
+ readonly kind: 'subagent-settled'
+ /** A runtime account shown without expanding the row (`notice` context form). */
+ readonly form: 'notice'
+ /** One-line account of how the child ended. */
+ readonly summary: string
+ /** Session id of the child that settled. */
+ readonly senderSessionId: SessionId
+}
+```
+
```ts type-equiv
/** Options for one continuable child's report to its direct parent. */
interface SubagentReportOptions {
@@ -265,7 +286,7 @@ interface ContinuableCreateSpec {
## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目
-`SubagentService.listChildren(parentSessionId)` 从 `ctx.sessions.list()` 与可选 `ctx.sessionPersistence.list()` 的实时优先合并中枚举 parent 直接且由会话支撑的 subagent——不经查询服务,也不会加载或恢复任何 Agent。候选是持久 header 携带 `origin: 'subagent'` 的直接 child;该标记只负责枚举分类与粗粒度的通用路由拒绝,不能证明描述符有效、child 可恢复或操作已获授权——身份由投影折叠负责,恢复由 Activation 约定负责。每行的 `mode`/`label` 是已注册 `subagent` projection unit 的值,经三级阶梯供值:存活 child 由注册表水位缓存供值(零日志读取);冷 child 先读可选的投影 checkpoint 缓存(`cachedSnapshot`——过 own-suffix seq 门的身份即定值,own descriptor 一经追加不可变);否则在一次 `persistence.inspect()` 读取上经注册表折叠(有界并发,每次列表重新计算)。该缓存是纯可选加速层:服务缺席、行里是 `null` 哨兵或 key 缺席、seq 门不过、读取出错,都静默落到权威重折。折叠规则是 `subagent/descriptor` last-wins 且没有失败通道:子 agent 自己的描述符覆盖 fork seed 中祖先的描述符,格式错误或版本不认识的载荷折叠为可序列化的 `null` 哨兵,视同无值。结果是按 `createdAt`、再按 id 排序的 `SubagentListEntry[]`:取到身份即生成带有 `mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'` 的 `child` 条目;可继续条目始终携带 `label`,一次性条目则只在启动调用方提供展示元数据时携带该字段。已定局而折叠无身份的候选生成 `corrupt` diagnostic——缺失、格式错误与版本不认识的描述符有意不再细分(`unsupported` 仍保留在类型中但从不产出);运行中而无身份的候选被省略(描述符落盘前的创建窗口);冷检查失败生成一条 `unavailable` diagnostic 并在下次列表自然重试,因此一个损坏的 sibling 不会隐藏健康 child。`hasChildren` 标记存在持久 subagent origin 的直接后代,读取自同一份合并材料。活动状态只表示逻辑记录是否在 `ctx.sessions` 中存活,而不表示结果或可恢复性。缺少持久化时,枚举退化为仅存活枚举而不是报错——此时冷 child 本就无法恢复。缺少 `ctx.sessionProjections` 注册表时,`listChildren()` 抛出携带错误码 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 的 `SubagentError`,缺少会话存储时则抛出 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`,两者都在任何读取之前检查,因此零 child 的部署同样确定失败;列表工具在插件加载时要求 `ctx.subagents` 与 `ctx.agents`。UI 等服务消费方可以展示两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 适配器([dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) 中可单独加载的 `/list-agents` 插件)则只保留可继续条目,并通过在线 Agent 注册表将状态细化为 `running`/`idle`/`complete` 词汇。枚举不会查询继续执行管理器的 Activation map、Agent 注册表或提供方可用性;`send_message` 仍是消息送达时的权威操作,列表中的运行中可继续 child 仍可能因所有权冲突而拒绝投递。读路径的设计理由见[列表身份投影 Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md)。
+`SubagentService.listChildren(parentSessionId)` 从 `ctx.sessions.list()` 与可选 `ctx.sessionPersistence.list()` 的实时优先合并中枚举 parent 直接且由会话支撑的 subagent——不经查询服务,也不会加载或恢复任何 Agent。候选是持久 header 携带 `origin: 'subagent'` 的直接 child;该标记只负责枚举分类与粗粒度的通用路由拒绝,不能证明描述符有效、child 可恢复或操作已获授权——身份由投影折叠负责,恢复由 Activation 约定负责。每行的 `mode`/`label` 是已注册 `subagent` projection unit 的值,经三级阶梯供值:存活 child 由注册表水位缓存供值(零日志读取);冷 child 先读可选的投影 checkpoint 缓存(`cachedSnapshot`——过 own-suffix seq 门的身份即定值,own descriptor 一经追加不可变);否则在一次 `persistence.inspect()` 读取上经注册表折叠(有界并发,每次列表重新计算)。该缓存是纯可选加速层:服务缺席、行里是 `null` 哨兵或 key 缺席、seq 门不过、读取出错,都静默落到权威重折。折叠规则是 `subagent/descriptor` last-wins 且没有失败通道:子 agent 自己的描述符覆盖 fork seed 中祖先的描述符,格式错误或版本不认识的载荷折叠为可序列化的 `null` 哨兵,视同无值。结果是按 `createdAt`、再按 id 排序的 `SubagentListEntry[]`:取到身份即生成带有 `mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'` 的 `child` 条目;可继续条目始终携带 `label`,一次性条目则只在启动调用方提供展示元数据时携带该字段。已定局而折叠无身份的候选生成 `corrupt` diagnostic——缺失、格式错误与版本不认识的描述符有意不再细分(`unsupported` 仍保留在类型中但从不产出);运行中而无身份的候选被省略(描述符落盘前的创建窗口);冷检查失败生成一条 `unavailable` diagnostic 并在下次列表自然重试,因此一个损坏的 sibling 不会隐藏健康 child。`hasChildren` 标记存在持久 subagent origin 的直接后代,读取自同一份合并材料。活动状态只表示逻辑记录是否在 `ctx.sessions` 中存活,而不表示结果或可恢复性。缺少持久化时,枚举退化为仅存活枚举而不是报错——此时冷 child 本就无法恢复。缺少 `ctx.sessionProjections` 注册表时,`listChildren()` 抛出携带错误码 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 的 `SubagentError`,缺少会话存储时则抛出 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`,两者都在任何读取之前检查,因此零 child 的部署同样确定失败;列表工具在插件加载时要求 `ctx.subagents` 与 `ctx.agents`。UI 等服务消费方可以展示两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 适配器([dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) 中可单独加载的 `/list-agents` 插件)则只保留可继续条目,并通过在线 Agent 注册表将状态细化为自己的 `running`/`idle`/`ready` 词汇,其中 `ready` 把仅存于存储的 child 命名为可恢复而非终态。枚举不会查询继续执行管理器的 Activation map、Agent 注册表或提供方可用性;`send_message` 仍是消息送达时的权威操作,列表中的运行中可继续 child 仍可能因所有权冲突而拒绝投递。读路径的设计理由见[列表身份投影 Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md)。
`SubagentService.listDescendants(rootSessionId)` 将同一份实时优先语料与基于投影的解释应用到根的完整后代树,并按稳定 pre-order 输出。普通会话和一次性 child 仍作为遍历节点,因此其下的可继续后代仍可发现;只有 `origin: 'subagent'` 的候选会生成条目。每个返回的 child 或 diagnostic 都从枚举所得的持久 header 附加树位置;冷检查在提供身份前还会重新校验完整生命周期:
@@ -293,7 +314,12 @@ type SubagentDescendantListEntry = SubagentListEntry & {
* The terminal outcome of a subagent run, resolved by {@link SubagentRun.result}.
*/
interface SubagentResult {
- /** The child's final assistant output (the last assistant message's content). */
+ /**
+ * The child's final assistant output is the content of its last non-empty
+ * assistant message. Empty-content messages, including usage-only messages,
+ * are skipped. Without a non-empty message, the output is its accumulated
+ * assistant text stream, or `[]` when the child produced neither.
+ */
readonly output: ContentBlock[]
/**
* The structured result after a requested `outputSchema` was successfully
@@ -387,7 +413,10 @@ interface SubagentRun {
/**
* One registered transport for running child agents. Providers are trusted
* same-process implementations; callers treat descriptors and returned values
- * as borrowed immutable data.
+ * as borrowed immutable data. The service may call one provider concurrently
+ * for distinct children. Providers isolate operation-local mutable state; a
+ * shared capacity controller may delay an operation but must not couple its
+ * settlement or cleanup to a sibling.
*/
interface SubagentProvider {
/** Unique registry name (e.g. `spawn`, `fork`, `acp`). */
@@ -408,7 +437,8 @@ interface SubagentProvider {
* initial turn. Before fulfillment, the provider owns setup and cleans any
* unpublished partial resources before rejecting. Ownership transfers on
* fulfillment; subsequent turn or infrastructure failure settles through
- * the returned run.
+ * the returned run. Distinct starts may overlap; cancellation, failure,
+ * result settlement, and disposal remain independent for each run.
*/
start(request: ResolvedSubagentStartRequest): Promise
/**
@@ -423,6 +453,8 @@ interface SubagentProvider {
* continuation manager owns identity reservation, composition, Agent
* creation, prompt delivery, cold resume, ownership, and disposal, so a
* provider never sees the child's Agent, handle, turns, or teardown.
+ * Distinct preparations may overlap; each follows its own signal and returns
+ * data belonging only to `request.sessionId`.
*/
prepareContinuable?(request: ContinuableCreateRequest): Promise
}
@@ -441,9 +473,9 @@ spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 agent,将
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -616,7 +648,7 @@ async start(name: string, request: SubagentStartRequest): Promise
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [MessageId](llm-streaming.md) · [SessionId](core.md)
-Source: [`packages/subagent/subagent/src/index.ts:167`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:171`](../../packages/subagent/subagent/src/index.ts)
@@ -642,7 +674,7 @@ A published child settled. Scope-filtered dispatch uses the same delegating pare
Types: [Scoped](scope.md)
-Source: [`packages/subagent/subagent/src/index.ts:162`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:166`](../../packages/subagent/subagent/src/index.ts)
@@ -659,7 +691,7 @@ A provider became resolvable in the registry.
'subagent/provider-added'(provider: SubagentProvider): void
```
-Source: [`packages/subagent/subagent/src/index.ts:136`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:140`](../../packages/subagent/subagent/src/index.ts)
@@ -676,7 +708,7 @@ A provider left the registry. Accepted runs remain holder-owned.
'subagent/provider-removed'(name: string): void
```
-Source: [`packages/subagent/subagent/src/index.ts:142`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:146`](../../packages/subagent/subagent/src/index.ts)
@@ -700,5 +732,5 @@ A provider established a published child. For in-process providers, `ctx.agents.
Types: [Scoped](scope.md)
-Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:157`](../../packages/subagent/subagent/src/index.ts)
diff --git a/docs/subsystems/subprocess.i18n.yaml b/docs/subsystems/subprocess.i18n.yaml
index 26064089c2..6347333266 100644
--- a/docs/subsystems/subprocess.i18n.yaml
+++ b/docs/subsystems/subprocess.i18n.yaml
@@ -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 docs/subsystems/subprocess.md
-subprocess.md: 4fb20e1b440b8fc10540fa2adbd00ae63fd7e221
-subprocess.zh.md: a41f14aa550c4780b5661b546880d932d513b817
+subprocess.md: 456156a687474ae49619cd8a1f4b5179c27ad01d
+subprocess.zh.md: 97c6604c1c600dae301d3f34b50c93cc6f5a7c7d
diff --git a/docs/subsystems/subprocess.md b/docs/subsystems/subprocess.md
index 4fb20e1b44..456156a687 100644
--- a/docs/subsystems/subprocess.md
+++ b/docs/subsystems/subprocess.md
@@ -252,9 +252,9 @@ The abstract [`SubprocessService`](../../packages/subprocess/subprocess/src/inde
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/subprocess.zh.md b/docs/subsystems/subprocess.zh.md
index a41f14aa55..97c6604c1c 100644
--- a/docs/subsystems/subprocess.zh.md
+++ b/docs/subsystems/subprocess.zh.md
@@ -252,9 +252,9 @@ interface SubprocessOutcome {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/system-prompt.i18n.yaml b/docs/subsystems/system-prompt.i18n.yaml
index c24ae31019..3ac169b4e0 100644
--- a/docs/subsystems/system-prompt.i18n.yaml
+++ b/docs/subsystems/system-prompt.i18n.yaml
@@ -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 docs/subsystems/system-prompt.md
-system-prompt.md: bdc0e994fb8e784a19814574c405d8cc3dce2d11
-system-prompt.zh.md: db6932b18f4721020fed567d49727f863eb06608
+system-prompt.md: f03d11866bcdcf3bcb5c5fe96931b059cb8d0336
+system-prompt.zh.md: 23b3e0b20cc4772baf22727d2e57866aa7cbbeb0
diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md
index bdc0e994fb..f03d11866b 100644
--- a/docs/subsystems/system-prompt.md
+++ b/docs/subsystems/system-prompt.md
@@ -39,7 +39,7 @@ interface ToolProviderResult {
## Prompt sections
-`PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context.
+`PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context. One effective `complete` section becomes the sole prompt section after cooperative assembly.
```ts type-equiv
/** One contributed section of the system prompt (registry input). */
@@ -58,6 +58,13 @@ interface PromptSection {
* interpolated later, by {@link renderPrompt}.
*/
readonly text: string | ((context: AssembleContext) => string)
+ /**
+ * Treat this contribution as the complete system prompt. Assembly still
+ * runs the cooperative waterfall so tools, contexts, and variables can be
+ * resolved, then restores this exact section as the sole prompt section.
+ * More than one effective complete section makes assembly fail.
+ */
+ readonly complete?: boolean
}
```
@@ -81,9 +88,9 @@ interface PromptContext {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -132,14 +139,16 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine
/**
* Assemble global and scoped providers, detach tool parameters, apply
* canonical ordering, then run the assembly waterfall. Scoped sections and
- * variables shadow globals; the returned waterfall value is authoritative.
+ * variables shadow globals. The returned waterfall value is authoritative
+ * except that an effective complete section is restored afterwards as the
+ * sole prompt section.
* @param context - the optional scope and plugin-defined assembly fields.
- * @returns the authoritative post-waterfall assembly.
+ * @returns the post-waterfall assembly with any complete prompt enforced.
*/
async assemble(context: AssembleContext = {}): Promise
```
-Source: [`packages/core/system-prompt/src/index.ts:325`](../../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:334`](../../packages/core/system-prompt/src/index.ts)
@@ -149,7 +158,7 @@ Source: [`packages/core/system-prompt/src/index.ts:325`](../../packages/core/sys
#### `system-prompt/assemble` — waterfall
-Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns.
+Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt.
```ts cordis-catalog
/**
@@ -157,7 +166,9 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
* receive only that scope's assemblies. The returned value is authoritative.
* A supplied signal controls only this explicit assembly request and must not
- * be retained to control later turns.
+ * be retained to control later turns. A registered complete section is
+ * restored after this waterfall, so listeners cannot add to or replace
+ * that scope's system prompt.
* @param assembly - the mutable assembly built from registered providers.
* @param context - the caller's per-assembly context.
* @mode waterfall
@@ -167,7 +178,7 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc
Types: [Scoped](scope.md)
-Source: [`packages/core/system-prompt/src/index.ts:29`](../../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:31`](../../packages/core/system-prompt/src/index.ts)
@@ -184,5 +195,5 @@ Emitted when any prompt provider changes. This registry notification is unfilter
'system-prompt/change'(): void
```
-Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:37`](../../packages/core/system-prompt/src/index.ts)
diff --git a/docs/subsystems/system-prompt.zh.md b/docs/subsystems/system-prompt.zh.md
index db6932b18f..23b3e0b20c 100644
--- a/docs/subsystems/system-prompt.zh.md
+++ b/docs/subsystems/system-prompt.zh.md
@@ -39,7 +39,7 @@ interface ToolProviderResult {
## 提示词段落
-`PromptSection` 是一份只读的同进程注册约定。其文本可以是静态的,也可以从当前组装上下文动态解析。
+`PromptSection` 是一份只读的同进程注册约定。其文本可以是静态的,也可以从当前组装上下文动态解析。协作式组装完成后,一个有效的 `complete` 段会成为唯一的提示词段落。
```ts type-equiv
/** One contributed section of the system prompt (registry input). */
@@ -58,6 +58,13 @@ interface PromptSection {
* interpolated later, by {@link renderPrompt}.
*/
readonly text: string | ((context: AssembleContext) => string)
+ /**
+ * Treat this contribution as the complete system prompt. Assembly still
+ * runs the cooperative waterfall so tools, contexts, and variables can be
+ * resolved, then restores this exact section as the sole prompt section.
+ * More than one effective complete section makes assembly fail.
+ */
+ readonly complete?: boolean
}
```
@@ -81,9 +88,9 @@ interface PromptContext {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -132,14 +139,16 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine
/**
* Assemble global and scoped providers, detach tool parameters, apply
* canonical ordering, then run the assembly waterfall. Scoped sections and
- * variables shadow globals; the returned waterfall value is authoritative.
+ * variables shadow globals. The returned waterfall value is authoritative
+ * except that an effective complete section is restored afterwards as the
+ * sole prompt section.
* @param context - the optional scope and plugin-defined assembly fields.
- * @returns the authoritative post-waterfall assembly.
+ * @returns the post-waterfall assembly with any complete prompt enforced.
*/
async assemble(context: AssembleContext = {}): Promise
```
-Source: [`packages/core/system-prompt/src/index.ts:325`](../../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:334`](../../packages/core/system-prompt/src/index.ts)
@@ -149,7 +158,7 @@ Source: [`packages/core/system-prompt/src/index.ts:325`](../../packages/core/sys
#### `system-prompt/assemble` — waterfall
-Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns.
+Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt.
```ts cordis-catalog
/**
@@ -157,7 +166,9 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
* receive only that scope's assemblies. The returned value is authoritative.
* A supplied signal controls only this explicit assembly request and must not
- * be retained to control later turns.
+ * be retained to control later turns. A registered complete section is
+ * restored after this waterfall, so listeners cannot add to or replace
+ * that scope's system prompt.
* @param assembly - the mutable assembly built from registered providers.
* @param context - the caller's per-assembly context.
* @mode waterfall
@@ -167,7 +178,7 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc
Types: [Scoped](scope.md)
-Source: [`packages/core/system-prompt/src/index.ts:29`](../../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:31`](../../packages/core/system-prompt/src/index.ts)
@@ -184,5 +195,5 @@ Emitted when any prompt provider changes. This registry notification is unfilter
'system-prompt/change'(): void
```
-Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:37`](../../packages/core/system-prompt/src/index.ts)
diff --git a/docs/subsystems/tasks.i18n.yaml b/docs/subsystems/tasks.i18n.yaml
index 6a95ccf73e..a73e8129c9 100644
--- a/docs/subsystems/tasks.i18n.yaml
+++ b/docs/subsystems/tasks.i18n.yaml
@@ -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 docs/subsystems/tasks.md
-tasks.md: 2da8212b3edf7111180b60162108af0aebb249c1
-tasks.zh.md: 825a69029ae931f9668cca863c6ebb2324634a9a
+tasks.md: 2798eb2963e83d42321a343d3c0a6a2bb658be41
+tasks.zh.md: 8d4539514cb4bc654dd86031d51417c4f79f8982
diff --git a/docs/subsystems/tasks.md b/docs/subsystems/tasks.md
index 2da8212b3e..2798eb2963 100644
--- a/docs/subsystems/tasks.md
+++ b/docs/subsystems/tasks.md
@@ -38,7 +38,7 @@ interface TaskStart {
label: string
/**
* Optional UTF-8 byte cap for each complete model-facing completion notice or
- * output read, including control-surface status metadata.
+ * output read, including controller status metadata.
*/
outputLimitBytes?: number
/**
@@ -97,7 +97,7 @@ interface TaskOutcome {
## Consumer views
-Snapshots are fresh read-only projections. `ownerSession` carries the shared `SessionId` used for authorization; completion listeners separately receive the exact owner object used for lifecycle cleanup. `reported` suppresses a completion notice after another surface has delivered or committed to deliver the terminal state.
+Snapshots are fresh read-only projections. `ownerSession` carries the shared `SessionId` used for authorization; completion listeners separately receive the exact owner object used for lifecycle cleanup. `reported` suppresses a completion notice after another reporter has delivered or committed to deliver the terminal state.
```ts type-equiv
/**
@@ -129,7 +129,7 @@ interface TaskSnapshot {
finishedAt?: number
/**
* True when a kill, read, or wait has reported or committed to report the
- * terminal state. Completion surfaces suppress redundant notices when set.
+ * terminal state. Completion reporters suppress redundant notices when set.
*/
reported: boolean
}
@@ -151,15 +151,15 @@ interface TaskRead {
## Service behavior
-The abstract [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition specifies atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, failure-isolated `onTaskDone` listeners, and when `attachSurface` becomes available; [`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) is the process-local Service provider. Authorization compares owner sessions; owner cleanup selects the exact registered `Agent` instance. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the Service Definition contract, [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md) for the registry lifecycle, and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing Consumer.
+The abstract [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition specifies atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, failure-isolated `onTaskDone` and `onTasksChanged` listeners, and when `attachController` becomes available; [`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) is the process-local Service provider. Authorization compares owner sessions; owner cleanup selects the exact registered `Agent` instance. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the Service Definition contract, [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md) for the registry lifecycle, and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing Consumer.
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -169,10 +169,10 @@ Abstract background task registry. Subclass, implement the abstract methods, and
Implementations must honor these semantics:
-- Registrations outlive producer and control-surface fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record.
+- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record.
- Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary.
- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome.
-- start refuses work while no attached control surface serves the spec's owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition's scope serve exactly the agents composed under it.
+- start refuses work while no attached task controller serves the spec's owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition's scope serve exactly the agents composed under it.
```ts cordis-catalog
/**
@@ -247,16 +247,40 @@ abstract wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSigna
abstract onTaskDone(listener: TaskDoneListener): () => void
/**
- * Attach an effect-scoped surface that can read and stop tasks. It serves the
- * owners its registering context's scope covers, and {@link start} refuses an
- * owner no attached surface serves.
- * @param name - diagnostic label; duplicate names remain independent.
- * @returns disposer that detaches this surface.
+/**
+ * Register an effect-scoped observer of visible-set changes. It fires after
+ * every commit that changes what {@link list} returns for that owner —
+ * registration, every stopping transition (including the one teardown
+ * performs before it awaits a slow producer), settlement, owner-disposal
+ * removal, and the emptying that service disposal commits — so an observer
+ * re-reads rather than accumulating deltas.
+ *
+ * Delivery is owner-relative on the same terms as {@link onTaskDone}: an
+ * observer registered from an unscoped context — a host composition's own
+ * carrier — sees every owner, while one registered under an agent
+ * composition's scope sees exactly the agents composed under it.
+ *
+ * This is not a superset of {@link onTaskDone}: that one delivers the terminal
+ * record under first-wins semantics a task controller couples to notice
+ * delivery, while this one carries no delivery meaning and marks nothing
+ * reported. Listeners are contained and never awaited.
+ * @param listener - receives the owner whose visible set changed, or
+ * `undefined` when an unowned task changed and every caller's set did.
+ * @returns disposer that unregisters the listener.
*/
-abstract attachSurface(name: string): () => void
+abstract onTasksChanged(listener: TasksChangedListener): () => void
+
+/**
+ * Attach an effect-scoped controller that can read and stop tasks. It serves the
+ * owners its registering context's scope covers, and {@link start} refuses an
+ * owner no attached controller serves.
+ * @param name - diagnostic label; duplicate names remain independent.
+ * @returns disposer that detaches this controller.
+ */
+abstract attachController(name: string): () => void
```
Types: [Agent](core.md)
-Source: [`packages/tasks/tasks/src/index.ts:55`](../../packages/tasks/tasks/src/index.ts)
+Source: [`packages/tasks/tasks/src/index.ts:58`](../../packages/tasks/tasks/src/index.ts)
diff --git a/docs/subsystems/tasks.zh.md b/docs/subsystems/tasks.zh.md
index 825a69029a..8d4539514c 100644
--- a/docs/subsystems/tasks.zh.md
+++ b/docs/subsystems/tasks.zh.md
@@ -38,7 +38,7 @@ interface TaskStart {
label: string
/**
* Optional UTF-8 byte cap for each complete model-facing completion notice or
- * output read, including control-surface status metadata.
+ * output read, including controller status metadata.
*/
outputLimitBytes?: number
/**
@@ -129,7 +129,7 @@ interface TaskSnapshot {
finishedAt?: number
/**
* True when a kill, read, or wait has reported or committed to report the
- * terminal state. Completion surfaces suppress redundant notices when set.
+ * terminal state. Completion reporters suppress redundant notices when set.
*/
reported: boolean
}
@@ -151,15 +151,15 @@ interface TaskRead {
## 服务行为
-抽象的 [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition 规定原子 `start`、限定调用方作用域的 `get` 和 `list`、`read`、`kill`、有界 `wait`、故障隔离的 `onTaskDone` 监听器,以及 `attachSurface` 何时可用;[`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) 是其进程局部 Service provider。授权会比较拥有者会话;拥有者清理会选择确切的已注册 `Agent` 实例。Service Definition 约定见 [`dsh-tasks`](../../packages/tasks/tasks/README.md),注册表生命周期见 [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md),面向模型的 Consumer 见 [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md)。
+抽象的 [`TaskService`](../../packages/tasks/tasks/src/index.ts) Service Definition 规定原子 `start`、限定调用方作用域的 `get` 和 `list`、`read`、`kill`、有界 `wait`、故障隔离的 `onTaskDone` 与 `onTasksChanged` 监听器,以及 `attachController` 何时可用;[`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) 是其进程局部 Service provider。授权会比较拥有者会话;拥有者清理会选择确切的已注册 `Agent` 实例。Service Definition 约定见 [`dsh-tasks`](../../packages/tasks/tasks/README.md),注册表生命周期见 [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md),面向模型的 Consumer 见 [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md)。
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -169,10 +169,10 @@ Abstract background task registry. Subclass, implement the abstract methods, and
Implementations must honor these semantics:
-- Registrations outlive producer and control-surface fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record.
+- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record.
- Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary.
- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome.
-- start refuses work while no attached control surface serves the spec's owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition's scope serve exactly the agents composed under it.
+- start refuses work while no attached task controller serves the spec's owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition's scope serve exactly the agents composed under it.
```ts cordis-catalog
/**
@@ -247,16 +247,40 @@ abstract wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSigna
abstract onTaskDone(listener: TaskDoneListener): () => void
/**
- * Attach an effect-scoped surface that can read and stop tasks. It serves the
- * owners its registering context's scope covers, and {@link start} refuses an
- * owner no attached surface serves.
- * @param name - diagnostic label; duplicate names remain independent.
- * @returns disposer that detaches this surface.
+/**
+ * Register an effect-scoped observer of visible-set changes. It fires after
+ * every commit that changes what {@link list} returns for that owner —
+ * registration, every stopping transition (including the one teardown
+ * performs before it awaits a slow producer), settlement, owner-disposal
+ * removal, and the emptying that service disposal commits — so an observer
+ * re-reads rather than accumulating deltas.
+ *
+ * Delivery is owner-relative on the same terms as {@link onTaskDone}: an
+ * observer registered from an unscoped context — a host composition's own
+ * carrier — sees every owner, while one registered under an agent
+ * composition's scope sees exactly the agents composed under it.
+ *
+ * This is not a superset of {@link onTaskDone}: that one delivers the terminal
+ * record under first-wins semantics a task controller couples to notice
+ * delivery, while this one carries no delivery meaning and marks nothing
+ * reported. Listeners are contained and never awaited.
+ * @param listener - receives the owner whose visible set changed, or
+ * `undefined` when an unowned task changed and every caller's set did.
+ * @returns disposer that unregisters the listener.
*/
-abstract attachSurface(name: string): () => void
+abstract onTasksChanged(listener: TasksChangedListener): () => void
+
+/**
+ * Attach an effect-scoped controller that can read and stop tasks. It serves the
+ * owners its registering context's scope covers, and {@link start} refuses an
+ * owner no attached controller serves.
+ * @param name - diagnostic label; duplicate names remain independent.
+ * @returns disposer that detaches this controller.
+ */
+abstract attachController(name: string): () => void
```
Types: [Agent](core.md)
-Source: [`packages/tasks/tasks/src/index.ts:55`](../../packages/tasks/tasks/src/index.ts)
+Source: [`packages/tasks/tasks/src/index.ts:58`](../../packages/tasks/tasks/src/index.ts)
diff --git a/docs/subsystems/telemetry.i18n.yaml b/docs/subsystems/telemetry.i18n.yaml
index f5cda71a1d..5db02bb432 100644
--- a/docs/subsystems/telemetry.i18n.yaml
+++ b/docs/subsystems/telemetry.i18n.yaml
@@ -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 docs/subsystems/telemetry.md
-telemetry.md: 5ea5c67210ce1387cbd886935e914baf7f904fbb
-telemetry.zh.md: bd8fc8acc4c8522d8b1e4bc543431c0abf224411
+telemetry.md: 30d0497fd69a2f7b0f0fa77a040c11e0ee338d53
+telemetry.zh.md: f0adccf873463f1e88d87dfed51a14e2a1e9700c
diff --git a/docs/subsystems/telemetry.md b/docs/subsystems/telemetry.md
index 5ea5c67210..30d0497fd6 100644
--- a/docs/subsystems/telemetry.md
+++ b/docs/subsystems/telemetry.md
@@ -2,7 +2,7 @@
English | [中文](telemetry.zh.md)
-Outbound session reporting is one [capability seam](../capability-seams.md): its Service Definition ([dsh-session-telemetry](../../packages/session/session-telemetry), `ctx.telemetry`) declares the minimal backend contract, and its capture coordinator owns the capture points, fixed chunk projection, `telemetry/record` redaction waterfall, and handoff cursor; the Service provider a deployment loads ([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel)) uses the OpenTelemetry JS SDK's log pipeline with its configuration unchanged. This optional capability is not part of the agent loop, and nothing here reaches a model request. The harness stops after it calls `emit()`; the reporting SDK owns batching, retry, queueing, and loss policy. The [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) records that rule and the rejected alternatives. The [Service Definition README](../../packages/session/session-telemetry/README.md) defines the capture-point, cursor, and projection contracts.
+Outbound session reporting is split as a [capability seam](../capability-seams.md): the Service Definition and capture coordinator ([dsh-session-telemetry](../../packages/session/session-telemetry), `ctx.telemetry`) own the capture points, fixed chunk projection, `telemetry/record` redaction waterfall, handoff cursor, and minimal backend contract; the Service provider a deployment loads ([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel)) is the OpenTelemetry JS SDK's log pipeline configured verbatim. It is one optional capability, not part of the agent-loop spine, and nothing here reaches a model request. The boundary axiom — the harness's aspect ends at `emit()`; batching, retry, queueing, and loss policy belong to the reporting SDK — and the rejected alternatives are pinned in the [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md); the capture points, cursor, and projection contracts live in the [Service Definition README](../../packages/session/session-telemetry/README.md).
Source: [`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts)
@@ -56,6 +56,21 @@ interface TelemetryRecord {
Only the first `assistant/chunk` of each `(turn, step)` ships — the stream-started signal; the rest drop at capture, so `seq` gaps are routine on the wire and never a loss signal. Every other [session event](session.md) type, including plugin-merged ones the seam never heard of, passes through whole. Delivery is best-effort: the cursor marks handed-off, not delivered, records can be lost (crash, reload window) and duplicated (cursor-less re-adoption, SDK retries), so receivers dedupe ledger records on `(session.id, event.seq)`; ops records deliberately omit that identity — they are signals to alert on, not entries to sum, and tolerate duplicates instead.
+## The sharing disclosure
+
+The seam's acknowledgement contract (owned by the [Service Definition README's sharing-disclosure section](../../packages/session/session-telemetry/README.md#the-sharing-disclosure)): every backend discloses its deployment-selected sharing policy through the required abstract `sharing` member on `ctx.telemetry`, and consumers render "not configured" only when no telemetry service is mounted. The disclosure states the current policy, never delivery or retention — handoff is the non-blocking enqueue, and batching, retry, and loss policy stay the reporting SDK's.
+
+```ts type-equiv
+/**
+ * Deployment-selected session-sharing policy disclosed by a mounted
+ * {@link Telemetry} backend to human-facing acknowledgement surfaces (the
+ * `/feedback` command's confirmation text). The seam owns the vocabulary so
+ * any backend can disclose a policy without depending on the OTel package;
+ * the values mirror the OTel backend's serialized `TelemetryMode` choices.
+ */
+type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled'
+```
+
## The backend contract
```ts type-equiv
@@ -104,7 +119,7 @@ interface TelemetryBackend {
}
```
-`Telemetry` (`ctx.telemetry`, [signatures](#ctxtelemetry--telemetry-abstract-seam)) is the loadable form of this contract: each context accepts one implementation and throws on a duplicate. A backend constructs `TelemetryCoordinator` in its constructor to install capture.
+`Telemetry` (`ctx.telemetry`, [signatures](#ctxtelemetry--telemetry-abstract-seam)) is the contract's loadable form — one implementation per context, duplicate load throws — and a backend composes the seam's `TelemetryCoordinator` in its constructor to install the capture side.
## The redact waterfall: `telemetry/record`
@@ -114,9 +129,9 @@ Every record passes the `telemetry/record` [waterfall](../cordis-primer.md#cordi
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -141,7 +156,7 @@ flush?(): void
abstract shutdown(): Promise
```
-Source: [`packages/session/session-telemetry/src/index.ts:139`](../../packages/session/session-telemetry/src/index.ts)
+Source: [`packages/session/session-telemetry/src/index.ts:148`](../../packages/session/session-telemetry/src/index.ts)
diff --git a/docs/subsystems/telemetry.zh.md b/docs/subsystems/telemetry.zh.md
index bd8fc8acc4..f0adccf873 100644
--- a/docs/subsystems/telemetry.zh.md
+++ b/docs/subsystems/telemetry.zh.md
@@ -2,7 +2,7 @@
[English](telemetry.md) | 中文
-对外会话上报是一项[能力 seam](../capability-seams.md):其 Service Definition([dsh-session-telemetry](../../packages/session/session-telemetry),`ctx.telemetry`)声明最小后端约定,其捕获协调器负责捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)和 handoff 游标;部署方加载的 Service provider([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))按原配置使用 OpenTelemetry JS SDK 日志流水线。这项能力可选,不属于 agent loop(智能体循环),这里也没有任何内容会进入模型请求。Harness 调用 `emit()` 后停止处理;上报 SDK 负责批处理、重试、排队和丢失策略。[复活 Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)记录了这条规则和被否决的替代方案。[Service Definition README](../../packages/session/session-telemetry/README.md) 定义捕获点、游标和投影约定。
+对外的会话上报拆分为一项[能力 seam](../capability-seams.md):Service Definition 与捕获协调器([dsh-session-telemetry](../../packages/session/session-telemetry),`ctx.telemetry`)拥有捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)、handoff 游标与最小后端约定;部署方加载的 Service provider([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop(智能体循环)主干,这里也没有任何内容会进入模型请求。边界公理(harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK)连同被否决的替代方案,均已在[复活 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的约定见 [Service Definition README](../../packages/session/session-telemetry/README.md)。
源码:[`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts)
@@ -56,6 +56,21 @@ interface TelemetryRecord {
每个 `(turn, step)` 只发出第一条 `assistant/chunk`,即「流已开始」的信号;其余分片在捕获时丢弃,因此导出流中的 `seq` 缺口是常态,绝不是丢失信号。其他所有[会话事件](session.md)类型都会完整透传,包括该 seam 从未听说过、由插件合并进来的事件类型。投递是尽力而为的:游标标记的是「已交接」而非「已送达」,记录可能丢失(崩溃、重载窗口)也可能重复(无游标的重新接管、SDK 重试),因此接收端对 ledger 记录基于 `(session.id, event.seq)` 去重;ops 记录刻意省略这类标识——它们是用于告警的信号,而非用于累加的条目,重复被容忍而非被去重。
+## 共享披露
+
+该 seam 的确认契约(归属 [Service Definition README 的共享披露段](../../packages/session/session-telemetry/README.md#the-sharing-disclosure)):每个后端都通过 `ctx.telemetry` 上必需的抽象 `sharing` 成员披露其部署级共享策略,消费方只有在未挂载任何遥测服务时才渲染「未配置」。披露只陈述当前策略,绝不承诺投递或留存——交接是非阻塞入队,批处理、重试与丢失策略仍归上报 SDK。
+
+```ts type-equiv
+/**
+ * Deployment-selected session-sharing policy disclosed by a mounted
+ * {@link Telemetry} backend to human-facing acknowledgement surfaces (the
+ * `/feedback` command's confirmation text). The seam owns the vocabulary so
+ * any backend can disclose a policy without depending on the OTel package;
+ * the values mirror the OTel backend's serialized `TelemetryMode` choices.
+ */
+type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled'
+```
+
## 后端约定
```ts type-equiv
@@ -104,7 +119,7 @@ interface TelemetryBackend {
}
```
-`Telemetry`(`ctx.telemetry`,[签名](#ctxtelemetry--telemetry-abstract-seam))是该约定的可加载类型:每个上下文只允许一个实现,重复加载会抛出异常。后端在构造函数中创建 `TelemetryCoordinator`,以安装捕获处理。
+`Telemetry`(`ctx.telemetry`,[签名](#ctxtelemetry--telemetry-abstract-seam))是该约定的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。
## 脱敏 waterfall:`telemetry/record`
@@ -114,9 +129,9 @@ interface TelemetryBackend {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -141,7 +156,7 @@ flush?(): void
abstract shutdown(): Promise
```
-Source: [`packages/session/session-telemetry/src/index.ts:139`](../../packages/session/session-telemetry/src/index.ts)
+Source: [`packages/session/session-telemetry/src/index.ts:148`](../../packages/session/session-telemetry/src/index.ts)
diff --git a/docs/subsystems/token-meter.i18n.yaml b/docs/subsystems/token-meter.i18n.yaml
index 17bbb937cc..d65a0dae82 100644
--- a/docs/subsystems/token-meter.i18n.yaml
+++ b/docs/subsystems/token-meter.i18n.yaml
@@ -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 docs/subsystems/token-meter.md
-token-meter.md: 8a01980191bf29b1fbc0f8b1d0b33d1fb149319c
-token-meter.zh.md: 7411ce26cb75697607a706469d7923d46ff3f421
+token-meter.md: d0066485e58b1e9ff60543111f186c0a2a69348d
+token-meter.zh.md: d92bc797d28d6dafd3cd5d2ca00577684b248ce5
diff --git a/docs/subsystems/token-meter.md b/docs/subsystems/token-meter.md
index 8a01980191..d0066485e5 100644
--- a/docs/subsystems/token-meter.md
+++ b/docs/subsystems/token-meter.md
@@ -46,9 +46,9 @@ Surface order is authoritative; replacement nodes can have higher durable seqs t
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/token-meter.zh.md b/docs/subsystems/token-meter.zh.md
index 7411ce26cb..d92bc797d2 100644
--- a/docs/subsystems/token-meter.zh.md
+++ b/docs/subsystems/token-meter.zh.md
@@ -46,9 +46,9 @@ interface TokenSurfaceNode {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/tools.i18n.yaml b/docs/subsystems/tools.i18n.yaml
index fbf617f20a..79fe385cd9 100644
--- a/docs/subsystems/tools.i18n.yaml
+++ b/docs/subsystems/tools.i18n.yaml
@@ -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 docs/subsystems/tools.md
-tools.md: 6ff2d967c5631d096dd78236ffd0383ddb2b0493
-tools.zh.md: 82ade5d8d4117387138296cf54fbc8e88ad335e7
+tools.md: 54d20de3b4d1f2ff6d03a8e89e9356eef403382a
+tools.zh.md: f8a35ffa7121438b0623f2a0972f90df2923ac89
diff --git a/docs/subsystems/tools.md b/docs/subsystems/tools.md
index 6ff2d967c5..54d20de3b4 100644
--- a/docs/subsystems/tools.md
+++ b/docs/subsystems/tools.md
@@ -17,7 +17,7 @@ interface ToolOutputDefinition {
readonly schema: JsonSchemaNode
/** Pure projection from validated arguments and value to Native/model content. */
render(args: unknown, value: JsonValue): ContentBlock[]
- /** Pure replayable presentation projection, computed only for surface calls. */
+ /** Pure replayable presentation projection, computed only for top-level calls. */
presentationMeta?(args: unknown, value: JsonValue): JsonValue
}
```
@@ -366,7 +366,7 @@ type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure
The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. The canonical `value` is execution-local: the loop persists only `content`, `error`, and `meta`, while `tool/code-dispatch` stores the sub-call's rendered `content` and `isError` verbatim. Replay reproduces presentation but cannot reconstruct canonical intermediate values.
-On success the registry snapshots and validates the body value, freezes it, and invokes the pure renderer plus the optional direct-surface metadata projector. It separately materializes the durable presentation fields immediately before `tools/result`; an invalid value, renderer/projector failure, or non-JSON presentation becomes a JSON-safe `isError`. The final live observer therefore sees the exact execution-local value beside fields safe for the later durable append.
+On success the registry snapshots and validates the body value, freezes it, and invokes the pure renderer plus the optional top-level-call metadata projector. It separately materializes the durable presentation fields immediately before `tools/result`; an invalid value, renderer/projector failure, or non-JSON presentation becomes a JSON-safe `isError`. The final live observer therefore sees the exact execution-local value beside fields safe for the later durable append.
Before final content, the registry materializes the candidate result; a failure in content, structured error, additional context, or presentation metadata becomes a JSON-safe `isError` result that still reaches `finalizeContent`. The registry invokes that callback exactly once, then materializes and freezes the accepted result immediately before `tools/result`, so the observed live outcome is safe for the later durable `tool/result` append.
@@ -468,9 +468,9 @@ The full presentation field docs live in [`packages/core/tools/src/presentation.
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -480,12 +480,14 @@ Tool registry and execution pipeline. Scoped registrations shadow globals; one v
```ts cordis-catalog
/**
- * Present this agent's tools in `mode` instead of the deployment default.
+ * Present the calling scope's tools in `mode` instead of the deployment
+ * default. Nearest scope on the chain wins, so a preset's standing
+ * declaration covers every agent joined under it.
*
- * Scoped only, and one declaration per agent: this is how an agent preset
- * composes a Code Mode agent beside native ones in the same process, and a
+ * Scoped only, and one declaration per scope: this is how an agent preset
+ * composes Code Mode agents beside native ones in the same process, and a
* process-global override would be the `mode` config field instead.
- * @param mode - the presentation this agent's model sees.
+ * @param mode - the presentation the covered agents' models see.
* @returns the exact disposer that restores the deployment default.
*/
presentAs(mode: ToolPresentationMode): () => void
@@ -502,7 +504,7 @@ register(definition: ToolDefinition): () => void
* Restrict global tools for the calling agent scope. Empty filters, unknown
* names, scope-local names, and reserved transport names fail. Restrictions
* intersect; scoped registrations remain visible.
- * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).
+ * @param filter - global-tool mask: `allow` (keep only) and/or `deny` (remove).
* @returns the exact disposer that lifts this restriction.
*/
restrict(filter: ToolRestriction): () => void
diff --git a/docs/subsystems/tools.zh.md b/docs/subsystems/tools.zh.md
index 82ade5d8d4..f8a35ffa71 100644
--- a/docs/subsystems/tools.zh.md
+++ b/docs/subsystems/tools.zh.md
@@ -17,7 +17,7 @@ interface ToolOutputDefinition {
readonly schema: JsonSchemaNode
/** Pure projection from validated arguments and value to Native/model content. */
render(args: unknown, value: JsonValue): ContentBlock[]
- /** Pure replayable presentation projection, computed only for surface calls. */
+ /** Pure replayable presentation projection, computed only for top-level calls. */
presentationMeta?(args: unknown, value: JsonValue): JsonValue
}
```
@@ -468,9 +468,9 @@ type ObjectJsonSchema = JsonSchemaNode & { type: 'object' }
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -480,12 +480,14 @@ Tool registry and execution pipeline. Scoped registrations shadow globals; one v
```ts cordis-catalog
/**
- * Present this agent's tools in `mode` instead of the deployment default.
+ * Present the calling scope's tools in `mode` instead of the deployment
+ * default. Nearest scope on the chain wins, so a preset's standing
+ * declaration covers every agent joined under it.
*
- * Scoped only, and one declaration per agent: this is how an agent preset
- * composes a Code Mode agent beside native ones in the same process, and a
+ * Scoped only, and one declaration per scope: this is how an agent preset
+ * composes Code Mode agents beside native ones in the same process, and a
* process-global override would be the `mode` config field instead.
- * @param mode - the presentation this agent's model sees.
+ * @param mode - the presentation the covered agents' models see.
* @returns the exact disposer that restores the deployment default.
*/
presentAs(mode: ToolPresentationMode): () => void
@@ -502,7 +504,7 @@ register(definition: ToolDefinition): () => void
* Restrict global tools for the calling agent scope. Empty filters, unknown
* names, scope-local names, and reserved transport names fail. Restrictions
* intersect; scoped registrations remain visible.
- * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).
+ * @param filter - global-tool mask: `allow` (keep only) and/or `deny` (remove).
* @returns the exact disposer that lifts this restriction.
*/
restrict(filter: ToolRestriction): () => void
diff --git a/docs/subsystems/typert.i18n.yaml b/docs/subsystems/typert.i18n.yaml
index d58e6036ab..aa7ab3aafc 100644
--- a/docs/subsystems/typert.i18n.yaml
+++ b/docs/subsystems/typert.i18n.yaml
@@ -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 docs/subsystems/typert.md
-typert.md: 8ce52837ceee8e9c8c4a61de28f972912faa2cf0
-typert.zh.md: a025716dad9a9279a45530a0a9109a13a2c34b0c
+typert.md: 3c593952687a76089fcd6d66958c282294ae5abd
+typert.zh.md: 5d9c987de8c6d54394ccd1165ad1d430d41a31e5
diff --git a/docs/subsystems/typert.md b/docs/subsystems/typert.md
index 8ce52837ce..3c59395268 100644
--- a/docs/subsystems/typert.md
+++ b/docs/subsystems/typert.md
@@ -206,9 +206,9 @@ interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/typert.zh.md b/docs/subsystems/typert.zh.md
index a025716dad..5d9c987de8 100644
--- a/docs/subsystems/typert.zh.md
+++ b/docs/subsystems/typert.zh.md
@@ -206,9 +206,9 @@ interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/user-interaction.i18n.yaml b/docs/subsystems/user-interaction.i18n.yaml
index 0ea5d31f17..62e403439c 100644
--- a/docs/subsystems/user-interaction.i18n.yaml
+++ b/docs/subsystems/user-interaction.i18n.yaml
@@ -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 docs/subsystems/user-interaction.md
-user-interaction.md: 1ba4372d2c8ce2d8eeab46ce3aa4acdb11092ad3
-user-interaction.zh.md: 3e7e92d90216516ce1e38c69af88ed71701f1843
+user-interaction.md: 1f7ab09970f131928ada6e4014468b3d9fad64cf
+user-interaction.zh.md: 293b7fb391d8e6b2ec4179011437b4442b7fa5e4
diff --git a/docs/subsystems/user-interaction.md b/docs/subsystems/user-interaction.md
index 1ba4372d2c..1f7ab09970 100644
--- a/docs/subsystems/user-interaction.md
+++ b/docs/subsystems/user-interaction.md
@@ -137,15 +137,15 @@ class UserInteractionError extends HarnessError {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.userInteraction` — `UserInteractionService`
-`ctx.userInteraction`: one active UI provider plus an `ask()` surface.
+`ctx.userInteraction`: one active UI provider plus an `ask()` API.
```ts cordis-catalog
/**
diff --git a/docs/subsystems/user-interaction.zh.md b/docs/subsystems/user-interaction.zh.md
index 3e7e92d902..293b7fb391 100644
--- a/docs/subsystems/user-interaction.zh.md
+++ b/docs/subsystems/user-interaction.zh.md
@@ -137,15 +137,15 @@ class UserInteractionError extends HarnessError {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.userInteraction` — `UserInteractionService`
-`ctx.userInteraction`: one active UI provider plus an `ask()` surface.
+`ctx.userInteraction`: one active UI provider plus an `ask()` API.
```ts cordis-catalog
/**
diff --git a/docs/subsystems/web.i18n.yaml b/docs/subsystems/web.i18n.yaml
index bf0eef9ce4..0965349f7d 100644
--- a/docs/subsystems/web.i18n.yaml
+++ b/docs/subsystems/web.i18n.yaml
@@ -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 docs/subsystems/web.md
-web.md: a411fc804133b012b77b7232e653decdb0f09c3d
-web.zh.md: 595c315cf36dbbdf5b9a26870d5aeb1bbc0bb405
+web.md: 45f178e21d698cf731adeee9026ea943cf7f1a36
+web.zh.md: bcbbeeb847584501044c4b9127277bdd24b499f2
diff --git a/docs/subsystems/web.md b/docs/subsystems/web.md
index a411fc8041..45f178e21d 100644
--- a/docs/subsystems/web.md
+++ b/docs/subsystems/web.md
@@ -8,7 +8,7 @@ Source: [`packages/web/web/src/types.ts`](../../packages/web/web/src/types.ts)
## Why one capability has two operations
-Search and fetch share no request schema and no business logic, but they are deliberately one `ctx.web` middle layer: one provider-selection policy owner, one abort/error vocabulary, one product-facing "how this harness reaches the web" config surface. The cost is the parallel `searchX`/`fetchX` method pairs on the service; that parallelism is intentional, not a missed extraction. Providers register **capabilities** (a `WebSearchProvider` or `WebFetchProvider`), not tools; the model-facing names, schemas, prompt guidance, and presentation all live in the single `dsh-tool-web` consumer.
+Search and fetch share no request schema and no business logic, but they are deliberately one `ctx.web` middle layer: one provider-selection policy owner, one abort/error vocabulary, and one product-facing "how this harness reaches the web" configuration API. The cost is the parallel `searchX`/`fetchX` method pairs on the service; that parallelism is intentional, not a missed extraction. Providers register **capabilities** (a `WebSearchProvider` or `WebFetchProvider`), not tools; the model-facing names, schemas, prompt guidance, and presentation all live in the single `dsh-tool-web` consumer.
## Search request and result
@@ -38,7 +38,7 @@ interface WebSearchRequest {
* Normalized search outcome. `content` is optional provider-generated answer
* text or summary (Exa and DeepSeek return none; Perplexity returns a
* generated answer).
- * `sources[]` is the portable citation surface. `truncated` is set by the seam
+ * `sources[]` is the portable citation shape. `truncated` is set by the seam
* when it cut `sources[]` down to `maxResults`.
*/
interface WebSearchResult {
@@ -135,9 +135,9 @@ Selection never depends on registration, config, or HMR order: a capability has
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/web.zh.md b/docs/subsystems/web.zh.md
index 595c315cf3..bcbbeeb847 100644
--- a/docs/subsystems/web.zh.md
+++ b/docs/subsystems/web.zh.md
@@ -38,7 +38,7 @@ interface WebSearchRequest {
* Normalized search outcome. `content` is optional provider-generated answer
* text or summary (Exa and DeepSeek return none; Perplexity returns a
* generated answer).
- * `sources[]` is the portable citation surface. `truncated` is set by the seam
+ * `sources[]` is the portable citation shape. `truncated` is set by the seam
* when it cut `sources[]` down to `maxResults`.
*/
interface WebSearchResult {
@@ -135,9 +135,9 @@ type WebFetchBody =
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/workflow.i18n.yaml b/docs/subsystems/workflow.i18n.yaml
index 4061410c14..2d59e4ee70 100644
--- a/docs/subsystems/workflow.i18n.yaml
+++ b/docs/subsystems/workflow.i18n.yaml
@@ -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 docs/subsystems/workflow.md
-workflow.md: 3c7cc39feb8493b9ace11ae10c031a34a3942aee
-workflow.zh.md: c945a339c91402004a790ebc1ce7ffd5f8921ef6
+workflow.md: b7f29526d4a1e6664ecc6cd54670d2c608947bee
+workflow.zh.md: b714f9ce0ce68808a44b3824cfe2cac7c2d04b6e
diff --git a/docs/subsystems/workflow.md b/docs/subsystems/workflow.md
index 3c7cc39feb..b7f29526d4 100644
--- a/docs/subsystems/workflow.md
+++ b/docs/subsystems/workflow.md
@@ -131,9 +131,9 @@ The top-level `dsh-tool-workflow` consumer projects display facts into its calli
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/workflow.zh.md b/docs/subsystems/workflow.zh.md
index c945a339c9..b714f9ce0c 100644
--- a/docs/subsystems/workflow.zh.md
+++ b/docs/subsystems/workflow.zh.md
@@ -131,9 +131,9 @@ interface WorkflowRun {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/workspace.i18n.yaml b/docs/subsystems/workspace.i18n.yaml
index b2c2248876..58a9ad9c33 100644
--- a/docs/subsystems/workspace.i18n.yaml
+++ b/docs/subsystems/workspace.i18n.yaml
@@ -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 docs/subsystems/workspace.md
-workspace.md: ca088a2091a7f47a3d52992fec13fae44061a608
-workspace.zh.md: e414c759a043f934e1a8b5d89c7a3b6101bbb6f4
+workspace.md: 7bd5fda31e5d5c29d7446589b9af2679a46cba9a
+workspace.zh.md: 288b9468cc9e75613ab89efbe516eb287dae0441
diff --git a/docs/subsystems/workspace.md b/docs/subsystems/workspace.md
index ca088a2091..7bd5fda31e 100644
--- a/docs/subsystems/workspace.md
+++ b/docs/subsystems/workspace.md
@@ -129,9 +129,9 @@ Sessions get their cwd at create time from whoever creates them, not from this r
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/workspace.zh.md b/docs/subsystems/workspace.zh.md
index e414c759a0..288b9468cc 100644
--- a/docs/subsystems/workspace.zh.md
+++ b/docs/subsystems/workspace.zh.md
@@ -129,9 +129,9 @@ interface Workspace {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/testing.i18n.yaml b/docs/testing.i18n.yaml
index 2b608ec739..8ff27e971f 100644
--- a/docs/testing.i18n.yaml
+++ b/docs/testing.i18n.yaml
@@ -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 docs/testing.md
-testing.md: f5e8a478ec86c29c52f4127c51682c1c44fd23a7
-testing.zh.md: bd1fa7d23263d7c6e3bed65ef4ed09576ca47cc1
+testing.md: e66be9a6325c9d464a3999bd587f8b6c61c651c9
+testing.zh.md: 9ebcd12a4660fe660fd570d3adf978d968f1f320
diff --git a/docs/testing.md b/docs/testing.md
index f5e8a478ec..e66be9a632 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -9,7 +9,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
- **Unit** (`pnpm run test`): vitest over package and example specs under their `tests/**` directories plus repository script specs under `scripts/**/*.spec.ts`; tests stay with the code area they exercise. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent tests for contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`).
- **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. Per-file 100% on `packages/bash/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar.
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
-- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless backend scenarios boot their explicit example composition through an unexported JSONL test driver, while `apps/cli` separately owns product `dsh run` acceptance. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
+- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless backend scenarios boot their explicit example composition through an unexported JSONL test driver, while `apps/cli` separately owns product `dsh --profile headless` acceptance. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
- **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares replayed browser output with `apps/web/tests/snapshots/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` [builds first](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md) for plugin CSS.
Committed session-format JSONL uses the canonical packed-row layout, and the keyless snapshot gate discovers every such fixture by its `session` header; the [temporary migrator](../scripts/migrate-packed-session-fixtures.ts) rewrites older fixture layouts.
@@ -32,7 +32,7 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword
- Product-visible plugins require a non-unit REAL-composition test. Hand-built `ctx.plugin(...)` suites are insufficient: boot test-only `cordis.yml` through Loader and app/process, mock only external services or nondeterministic inputs, and assert model-visible request/log, durable state, or user-visible output. Keep opt-ins out of shipped defaults.
- A guard only guards if the regression actually fails it. For a plugin without `inject` (bundle/composition plugins), a Loader smoke stays green when a default export replaces the required named exports — add an explicit `expect('default' in mod).toBe(false)` plus an `unwrapExports` round-trip assertion, and prove it: introduce the regression, watch red, revert.
-- "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` under plain `node`, exposing failures tsx masks (settle races, module resolution, swallowed load failures). The same applies to non-index runtime entries (the worker-thread sibling `lib/worker.cjs`) and singleton modules shared across bundles (`packages/scaffold/server/tests/built-scope-carrier.e2e.ts`). Keep the built-artifact smokes green (`packages/examples/*/tests/built-bin.e2e.ts`, `packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`), and assert a genuinely-missing config exits non-zero.
+- "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` under plain `node`, exposing failures tsx masks (settle races, module resolution, swallowed load failures). The same applies to non-index runtime entries (the worker-thread sibling `lib/worker.cjs`) and singleton modules shared across bundles (`packages/sdk/server/tests/built-scope-carrier.e2e.ts`). Keep the built-artifact smokes green (`packages/examples/*/tests/built-bin.e2e.ts`, `packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`), and assert a genuinely-missing config exits non-zero.
## Test resolution: source plane only
diff --git a/docs/testing.zh.md b/docs/testing.zh.md
index bd1fa7d232..9ebcd12a46 100644
--- a/docs/testing.zh.md
+++ b/docs/testing.zh.md
@@ -9,7 +9,7 @@
- **单元测试**(`pnpm run test`):vitest 运行包(package)和示例各自的 `tests/**` 目录下的测试,以及匹配 `scripts/**/*.spec.ts` 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR(热模块替换)安全测试(dispose(资源释放)贡献的 fiber,断言清理完成)。优先覆盖边界情况、错误路径、事件顺序、并发竞态,以及针对约定回归的永久测试(见 `packages/core/agent-loop/tests/contract-regressions.spec.ts`)。
- **覆盖率门禁**(`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。`packages/bash/pwsh-local/src` 的按文件 100% 覆盖需要真实的 `pwsh`:缺少它时其 executor 套件会自动跳过,`vitest.config.ts` 会豁免该文件以使无 pwsh 的主机保持绿色,而 CI runner 自带 pwsh,仍按完整标准执行门禁。
- **真实 API e2e**(`pnpm run test:e2e`):带密钥测试调用真实提供方 API,包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY`、`PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md))。
-- **快照**(`pnpm run test:snapshot`):无密钥预期输出覆盖对外行为(传输约定与呈现),持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff([ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md));headless 后端场景通过未导出的 JSONL 测试 driver 启动各自显式的示例组装,而 `apps/cli` 则单独负责产品 CLI(命令行界面)`dsh run` 的验收。当模型 transcript(文本记录)发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture(测试前置数据)将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
+- **快照**(`pnpm run test:snapshot`):无密钥预期输出覆盖对外行为(传输约定与呈现),持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff([ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md));headless 后端场景通过未导出的 JSONL 测试 driver 启动各自显式的示例组装,而 `apps/cli` 则单独负责产品 CLI(命令行界面)`dsh --profile headless` 的验收。当模型 transcript(文本记录)发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture(测试前置数据)将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
- **Web 浏览器快照**(`pnpm run test:web`;必需的 Linux PR(Pull Request)门禁):Chromium 将回放后的浏览器输出与 `apps/web/tests/snapshots/` 比较。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md))。`test:web` 会[先构建](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)以交付插件 CSS。
签入仓库的会话格式 JSONL 使用规范打包行布局,无密钥快照门禁会通过 `session` header 发现每一份此类 fixture;[临时迁移器](../scripts/migrate-packed-session-fixtures.ts)会改写旧版 fixture 布局。
@@ -32,7 +32,7 @@ e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身
- 产品可见的插件必须有一个非单元的真实组合测试。手动构建的 `ctx.plugin(...)` 套件不够:通过 Loader 和 app/process 启动仅用于测试的 `cordis.yml`,只 mock 外部服务或非确定性输入,断言模型可见的请求/日志、持久状态或用户可见输出。不要把 opt-in 选项混入交付默认值。
- 一个守卫只有在回归真的能让它失败时才有效。对于没有 `inject` 的插件(bundle/组合插件),Loader 冒烟测试在默认导出替换必需的具名导出时仍然绿着——需要添加显式的 `expect('default' in mod).toBe(false)` 加 `unwrapExports` 往返断言,并证明它有效:引入回归、观察变红、回退。
-- 「真实入口路径」指已发布的产物:包的 `bin` 所运行的是构建后的 `lib/bin.js`,并由普通 `node` 执行,从而暴露 tsx 会掩盖的失败(等待稳定时的竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口(worker-thread 的同级文件 `lib/worker.cjs`),也适用于多个 bundle 共享的单例模块(`packages/scaffold/server/tests/built-scope-carrier.e2e.ts`)。保持构建产物冒烟测试绿色(`packages/examples/*/tests/built-bin.e2e.ts`、`packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零状态退出。
+- 「真实入口路径」指已发布的产物:包的 `bin` 所运行的是构建后的 `lib/bin.js`,并由普通 `node` 执行,从而暴露 tsx 会掩盖的失败(等待稳定时的竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口(worker-thread 的同级文件 `lib/worker.cjs`),也适用于多个 bundle 共享的单例模块(`packages/sdk/server/tests/built-scope-carrier.e2e.ts`)。保持构建产物冒烟测试绿色(`packages/examples/*/tests/built-bin.e2e.ts`、`packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零状态退出。
## 测试解析:仅限源码
diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml
index 567f95d408..2acc82ba4a 100644
--- a/docs/tool-catalog.i18n.yaml
+++ b/docs/tool-catalog.i18n.yaml
@@ -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 docs/tool-catalog.md
-tool-catalog.md: dbab9ce2f389dbfe40e7d753ced995a8a384be17
-tool-catalog.zh.md: e99f8bc78923e616265427c1e0361c832cc0930f
+tool-catalog.md: eeccb974b86fddf782f0970e3fb3b63805a007b6
+tool-catalog.zh.md: 485f5e819647c860c0dbe021a4914a49badcec2a
diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md
index dbab9ce2f3..eeccb974b8 100644
--- a/docs/tool-catalog.md
+++ b/docs/tool-catalog.md
@@ -3,11 +3,11 @@
# Tool Schema Catalog
-Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the [subsystem pages](subsystems/core.md) (the types plus each page's generated `cordis-surface` wiring region) — this page is the *tools* the agent is offered.
+Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the [subsystem pages](subsystems/core.md) (the types plus each page's generated Cordis API region) — this page is the *tools* the agent is offered.
This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md).
-Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config, except where a Config field is REQUIRED with no default — there the generator must choose, and the per-package note records which branch this page shows. The registered tool NAME can be a load-time config (e.g. `tool-subagent`'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog's packages-only scope.
+Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config, except where a Config field is REQUIRED with no default — there the generator must choose, and the per-package note records which branch this page shows. The registered tool NAME can be a load-time config (e.g. `tool-subagent`'s `toolName`), so a deployment may expose a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog's packages-only scope.
## Tool Package Map
@@ -19,11 +19,11 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userInteraction (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-interaction seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. |
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.bash`, `ctx.systemPrompt`, `ctx.bashEnv`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. |
-| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`, `ctx.bash`, `ctx.systemPrompt`, `ctx.bashEnv`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus the sandbox surface — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. |
+| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`, `ctx.bash`, `ctx.systemPrompt`, `ctx.bashEnv`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. |
| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`, `cordis_mount`, `cordis_unmount` | `ctx.tools` | `tool/call`, `tool/result`, `process-local temporary Plugin lifecycle` | - | Not in any shipped tree (a deliberate opt-in — temporary Plugin code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins created by cordis_mount may register ADDITIONAL model-visible tools until unmounted or DSH restarts; a full changed request header logs those tool-set changes. |
| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.pty`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. |
-| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`, `ctx.fs` | `tool/call`, `fs/observed after view presence/absence, edit absence, or successful mutation`, `tool/result` | - | Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal surface. |
-| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after read presence/absence or successful mutation`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. |
+| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`, `ctx.fs` | `tool/call`, `fs/observed after view presence/absence, edit absence, or successful mutation`, `tool/result` | - | Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. |
+| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `read_image`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt`, `ctx.attachments (read_image registration)`, `ctx.llm + an image-capable route (read_image execution)` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after read presence/absence or successful file operation`, `durable attachment (read_image)`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input. |
| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.subprocess`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background tasks) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. |
| `@deepseek-ai/dsh-tool-pty` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.pty`, `ctx.systemPrompt`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot bash/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. |
| `@deepseek-ai/dsh-tool-goal` | `create_goal`, `get_goal`, `update_goal` | `ctx.tools`, `ctx.agents`, `ctx.goals`, `ctx.systemPrompt`, `a calling Agent in an authorized open turn` | `tool/call`, `goal/change for mutations`, `tool/result` | - | create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds. |
@@ -31,10 +31,10 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflows`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. |
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - |
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. |
-| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`. |
+| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance's description and `run_in_background` parameter follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable`, while `subagent_fork` stays `one-shot` — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`. |
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). |
-| `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`, `a live continuable in-process child Agent` | `tool/call`, `tool/result`, `a user-role message in the direct parent session` | - | Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The parent-facing `send_message` tool is installed independently. |
-| `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task control surface: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`. |
+| `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`, `ctx.systemPrompt`, `a live continuable in-process child Agent` | `tool/call`, `tool/result`, `a user-role message in the direct parent session` | - | Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently. |
+| `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.tasks.start()`. |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. |
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflows`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |
@@ -248,7 +248,7 @@ Execute a PowerShell command (`pwsh -Command`) and return its stdout/stderr. Eac
Source: [`packages/bash/tool-pwsh/src/index.ts`](../packages/bash/tool-pwsh/src/index.ts)
-The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus the sandbox surface — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables.
+The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables.
## `@deepseek-ai/dsh-tool-cordis`
@@ -284,7 +284,7 @@ Source: [`packages/self-modification/tool-cordis/src/index.ts`](../packages/self
### `cordis_mount`
-Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.
+Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.
```json
{
@@ -417,7 +417,7 @@ Notes for using the `str_replace` command:
Source: [`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool-str-replace-editor/src/index.ts)
-Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal surface.
+Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API.
## `@deepseek-ai/dsh-tool-fs`
@@ -485,6 +485,27 @@ Read a UTF-8 text file and return line-numbered content.
Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
+### `read_image`
+
+Read a PNG/JPEG/WebP/GIF file and return the image itself. Requires the current model to accept image input.
+
+```json
+{
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to the image file, resolved by the filesystem backend."
+ }
+ },
+ "required": [
+ "file_path"
+ ]
+}
+```
+
+Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
+
### `write`
Create or fully replace a UTF-8 text file.
@@ -511,7 +532,7 @@ Create or fully replace a UTF-8 text file.
Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
-The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.
+The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input.
## `@deepseek-ai/dsh-tool-fs-search`
@@ -1189,7 +1210,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its
Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts)
-The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.
+The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance's description and `run_in_background` parameter follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable`, while `subagent_fork` stays `one-shot` — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.
## `@deepseek-ai/dsh-tool-subagent-control`
@@ -1216,7 +1237,7 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub
### `list_agents`
-List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.
+List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.
```json
{
@@ -1268,7 +1289,7 @@ The globally named control tools over continuable background subagents: provider
### `report`
-Report selected content to the agent that started you. Call this zero or more times for progress, findings, or a final answer. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.
+Report selected content to the agent that started you. Call this once before you finish, with a self-contained final result, and earlier for progress or findings that change what that agent does next. That agent shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so finishing your work is not itself a result. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.
```json
{
@@ -1276,7 +1297,7 @@ Report selected content to the agent that started you. Call this zero or more ti
"properties": {
"output": {
"type": "string",
- "description": "Self-contained content for your parent; it does not see your private work."
+ "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
}
},
"required": [
@@ -1287,7 +1308,7 @@ Report selected content to the agent that started you. Call this zero or more ti
Source: [`packages/subagent/tool-subagent-report/src/index.ts`](../packages/subagent/tool-subagent-report/src/index.ts)
-Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The parent-facing `send_message` tool is installed independently.
+Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently.
## `@deepseek-ai/dsh-tool-tasks`
@@ -1358,7 +1379,7 @@ Read a background task. Stream tasks return only output since the previous read;
Source: [`packages/tasks/tool-tasks/src/index.ts`](../packages/tasks/tool-tasks/src/index.ts)
-The kind-agnostic background-task control surface: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`.
+The kind-agnostic background-task controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.tasks.start()`.
## `@deepseek-ai/dsh-tool-todo`
diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md
index e99f8bc789..485f5e8196 100644
--- a/docs/tool-catalog.zh.md
+++ b/docs/tool-catalog.zh.md
@@ -25,7 +25,7 @@
| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`、`cordis_mount`、`cordis_unmount` | `ctx.tools` | `tool/call`、`tool/result`、`process-local temporary Plugin lifecycle` | - | 不在任何随产品发布的树中,需要有意选择启用;临时 Plugin 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。由 cordis_mount 创建的插件在卸载或 DSH 重启之前可以注册**额外的**模型可见工具;发生这类工具集变更时,系统会记录完整且有变动的请求头。 |
| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.pty`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 |
| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`、`ctx.fs` | `tool/call`、`fs/observed after view presence/absence, edit absence, or successful mutation`、`tool/result` | - | 基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 |
-| `@deepseek-ai/dsh-tool-fs` | `edit`、`read`、`write` | `ctx.tools`、`ctx.fs`、`ctx.systemPrompt` | `tool/call`、`fs/write-intent or fs/edit-intent for mutations`、`fs/observed after read presence/absence or successful mutation`、`tool/result` | - | 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。无论是否加载策略插件,上述工具 schema 都完全相同。 |
+| `@deepseek-ai/dsh-tool-fs` | `edit`、`read`、`read_image`、`write` | `ctx.tools`、`ctx.fs`、`ctx.systemPrompt`、`ctx.attachments (read_image registration)`、`ctx.llm + an image-capable route (read_image execution)` | `tool/call`、`fs/write-intent or fs/edit-intent for mutations`、`fs/observed after read presence/absence or successful file operation`、`durable attachment (read_image)`、`tool/result` | - | 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时 `read_image` 不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图像输入,否则拒绝。 |
| `@deepseek-ai/dsh-tool-fs-search` | `glob`、`grep` | `ctx.tools`、`ctx.subprocess`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 |
| `@deepseek-ai/dsh-tool-pty` | `terminal_close`、`terminal_list`、`terminal_open`、`terminal_read`、`terminal_send`、`terminal_signal` | `ctx.tools`、`ctx.pty`、`ctx.systemPrompt`、`ctx.tasks at call time for run_in_background` | `tool/call`、`tool/result` | - | 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.tasks`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 |
| `@deepseek-ai/dsh-tool-goal` | `create_goal`、`get_goal`、`update_goal` | `ctx.tools`、`ctx.agents`、`ctx.goals`、`ctx.systemPrompt`、`a calling Agent in an authorized open turn` | `tool/call`、`goal/change for mutations`、`tool/result` | - | create、edit、pause 和 resume 要求直接来自人类的根权限;complete 和 blocked 也接受确切的当前 Goal Round。blocked 的默认下限是 3 个获准的 Round。 |
@@ -33,10 +33,10 @@
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflows`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 |
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - |
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 |
-| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`、`ctx.subagents` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的示例 agent 会为每个 subagent 后端加载一次该包,因此模型还会看到 schema 相同、绑定到 fork 后端的 `subagent_fork`;见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。 |
+| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`、`ctx.subagents` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的组合会为每个 subagent 后端加载一次该包,因此模型还会看到绑定到 fork 后端的 `subagent_fork`。每个实例的描述与 `run_in_background` 参数取决于它自己的 `backgroundMode` 与 `enableRunInBackground`,因此两个随附 schema 并不相同:`subagent` 为 `continuable`,而 `subagent_fork` 保持 `one-shot`;见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。 |
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 |
-| `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`、`a live continuable in-process child Agent` | `tool/call`、`tool/result`、`a user-role message in the direct parent session` | - | 按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。面向父级的 `send_message` 工具单独安装。 |
-| `@deepseek-ai/dsh-tool-tasks` | `task_kill`、`task_list`、`task_output` | `ctx.tools`、`ctx.tasks`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制接口:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制接口,从而启用生产方的 `ctx.tasks.start()`。 |
+| `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`、`ctx.systemPrompt`、`a live continuable in-process child Agent` | `tool/call`、`tool/result`、`a user-role message in the direct parent session` | - | 按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。 |
+| `@deepseek-ai/dsh-tool-tasks` | `task_kill`、`task_list`、`task_output` | `ctx.tools`、`ctx.tasks`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.tasks.start()`。 |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 |
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`、`ctx.workflows`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 |
@@ -286,7 +286,7 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
### `cordis_mount`
-在当前 DSH 进程中挂载临时 Cordis Plugin。它创建的是内存中的运行时 Plugin,而不是已安装或已配置的 Plugin。该插件会在后续轮次中保持活动,直到执行 cordis_unmount、工具集卸载或 DSH 重启。它不会创建文件、安装包、修改 cordis.yml 或个人/项目配置、在重启后保留,也不会自动转为永久插件。若要保留,请让 Agent 通过常规开发工作流实现普通的本地、项目或仓库 Plugin。它可能影响同一进程中的其他会话;沙箱不是安全边界,注入的服务会访问真实运行时。`code` 会立即作为异步 JavaScript 函数的函数体在隔离沙箱中运行,并且**必须** `return` 一个插件。支持两种形式:函数形式 `return (ctx) => { … }`,它不声明 inject,因此可以注册工具、监听事件和提供服务,但访问**任何**服务(例如 ctx.bash)都会抛出异常;仅在不需要服务时使用。对象形式 `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }`,它声明依赖,Cordis 只在服务存在后激活插件;**优先使用**这种形式。你只能访问 inject 中列出的服务:即使未声明的服务存在,访问它也会抛出异常,因为如果提供方被卸载,未声明的依赖将无法清理。代码调用服务**之前**,请读取 cordis_inspect 的 what:"api";它会列出方法签名以及参数/返回值的类型形状,不要猜测字段类型,例如 bash 运行的 stdout 是对象而非字符串。在 `apply` 内,请使用标准 Cordis API:通过 `ctx.on(event, listener)` 观察事件(见 cordis_inspect 的 what:"events"),或调用 `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` 为自己提供新工具;该工具会在你的**下一步骤**可调用。工具参数:每个键**就是**一个属性,即 { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? };每个直接 DSL 对象都声明 additionalProperties: true|false,而 oneOf: [schema, schema, ...] 会取代 type,表示恰好匹配一个成员的联合。也接受原始 JSON Schema { type: 'object', properties, required?: […] } 包装层,其中对象默认开放。工具的 `execute` **必须**返回 `output.schema` 声明的无损 JSON 值;`output.render(args, value)` 单独返回 Native/模型内容块。临时 Plugin 可以**组合**:一个 Plugin 可以通过 `ctx.provide('name', value)` 提供服务,另一个则可声明 `inject: ['name']` 来消费它;消费方会在提供方出现前保持等待,提供方卸载后重新回到等待状态。在 `apply` 中注册的一切都会由 cordis_unmount 自动清理。沙箱全局对象:`console`(带 `[cordis:]` 标签,写入 harness 终端)、`harness.defineTool`、`harness.registerTool`、`btoa`、`atob`、`TextEncoder`、`TextDecoder`。Node API 已**禁用**:文件系统/网络/定时工作必须通过 Cordis 服务完成,绝不能使用 Node 内置能力;`require`、`setTimeout`/`setInterval` 和 `fetch` 会抛出重定向错误,`process` 和 `Buffer` 未定义。应改用 inject: ['fs'] + ctx.fs 处理文件、inject: ['web'] + ctx.web 处理 HTTP、inject: ['bash'] + ctx.bash 处理进程、inject: ['timer'] + ctx.setTimeout/ctx.setInterval 处理定时(这些是 fiber effect,卸载时自动清理);cordis_inspect 的 what:"api" 会展示**当前**运行时提供的能力。请编写**纯** JavaScript,不要使用 TypeScript(不得使用 `as` 或类型注解)。注意事项:(1) waterfall(瀑布式事件)事件(例如 tools/pre-execute)会向监听器传入最后一个 `next` 回调,该回调**必须**被调用;不调用 `next()` 就返回会**短路**此次调用。除非你有意拦截,否则请优先使用普通通知事件。(2) 切勿等待只能在当前轮次之后解析的内容;你的代码运行在该轮次的工具调用**内部**,否则会死锁。(3) 你的 `ctx` 是受限门面:可以注册工具、观察事件、提供/消费服务和使用定时器,但不会提供框架内部能力(ctx.root、ctx.fiber、ctx.extend、ctx.plugin 等)。不过,它并非安全边界:你注入的服务(例如 ctx.bash)会访问真实运行时。
+在当前 DSH 进程中挂载临时 Cordis Plugin。它创建的是内存中的运行时 Plugin,而不是已安装或已配置的 Plugin。该插件会在后续轮次中保持活动,直到执行 cordis_unmount、工具集卸载或 DSH 重启。它不会创建文件、安装包、修改 cordis.yml 或个人/项目配置、在重启后保留,也不会自动转为永久插件。若要保留,请让 Agent 通过常规开发工作流实现 SDK Plugin 或可安装的 profile bundle。它可能影响同一进程中的其他会话;沙箱不是安全边界,注入的服务会访问真实运行时。`code` 会立即作为异步 JavaScript 函数的函数体在隔离沙箱中运行,并且**必须** `return` 一个插件。支持两种形式:函数形式 `return (ctx) => { … }`,它不声明 inject,因此可以注册工具、监听事件和提供服务,但访问**任何**服务(例如 ctx.bash)都会抛出异常;仅在不需要服务时使用。对象形式 `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }`,它声明依赖,Cordis 只在服务存在后激活插件;**优先使用**这种形式。你只能访问 inject 中列出的服务:即使未声明的服务存在,访问它也会抛出异常,因为如果提供方被卸载,未声明的依赖将无法清理。代码调用服务**之前**,请读取 cordis_inspect 的 what:"api";它会列出方法签名以及参数/返回值的类型形状,不要猜测字段类型,例如 bash 运行的 stdout 是对象而非字符串。在 `apply` 内,请使用标准 Cordis API:通过 `ctx.on(event, listener)` 观察事件(见 cordis_inspect 的 what:"events"),或调用 `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` 为自己提供新工具;该工具会在你的**下一步骤**可调用。工具参数:每个键**就是**一个属性,即 { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? };每个直接 DSL 对象都声明 additionalProperties: true|false,而 oneOf: [schema, schema, ...] 会取代 type,表示恰好匹配一个成员的联合。也接受原始 JSON Schema { type: 'object', properties, required?: […] } 包装层,其中对象默认开放。工具的 `execute` **必须**返回 `output.schema` 声明的无损 JSON 值;`output.render(args, value)` 单独返回 Native/模型内容块。临时 Plugin 可以**组合**:一个 Plugin 可以通过 `ctx.provide('name', value)` 提供服务,另一个则可声明 `inject: ['name']` 来消费它;消费方会在提供方出现前保持等待,提供方卸载后重新回到等待状态。在 `apply` 中注册的一切都会由 cordis_unmount 自动清理。沙箱全局对象:`console`(带 `[cordis:]` 标签,写入 harness 终端)、`harness.defineTool`、`harness.registerTool`、`btoa`、`atob`、`TextEncoder`、`TextDecoder`。Node API 已**禁用**:文件系统/网络/定时工作必须通过 Cordis 服务完成,绝不能使用 Node 内置能力;`require`、`setTimeout`/`setInterval` 和 `fetch` 会抛出重定向错误,`process` 和 `Buffer` 未定义。应改用 inject: ['fs'] + ctx.fs 处理文件、inject: ['web'] + ctx.web 处理 HTTP、inject: ['bash'] + ctx.bash 处理进程、inject: ['timer'] + ctx.setTimeout/ctx.setInterval 处理定时(这些是 fiber effect,卸载时自动清理);cordis_inspect 的 what:"api" 会展示**当前**运行时提供的能力。请编写**纯** JavaScript,不要使用 TypeScript(不得使用 `as` 或类型注解)。注意事项:(1) waterfall(瀑布式事件)事件(例如 tools/pre-execute)会向监听器传入最后一个 `next` 回调,该回调**必须**被调用;不调用 `next()` 就返回会**短路**此次调用。除非你有意拦截,否则请优先使用普通通知事件。(2) 切勿等待只能在当前轮次之后解析的内容;你的代码运行在该轮次的工具调用**内部**,否则会死锁。(3) 你的 `ctx` 是受限门面:可以注册工具、观察事件、提供/消费服务和使用定时器,但不会提供框架内部能力(ctx.root、ctx.fiber、ctx.extend、ctx.plugin 等)。不过,它并非安全边界:你注入的服务(例如 ctx.bash)会访问真实运行时。
```json
{
@@ -489,6 +489,27 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
来源:[`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
+### `read_image`
+
+读取 PNG/JPEG/WebP/GIF 文件并返回图像本身。要求当前模型接受图像输入。
+
+```json
+{
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to the image file, resolved by the filesystem backend."
+ }
+ },
+ "required": [
+ "file_path"
+ ]
+}
+```
+
+来源:[`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
+
### `write`
创建或完全替换 UTF-8 文本文件。
@@ -515,7 +536,7 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
来源:[`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
-先读后写/编辑策略由 `@deepseek-ai/dsh-fs-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。无论是否加载策略插件,上述工具 schema 都完全相同。
+先读后写/编辑策略由 `@deepseek-ai/dsh-fs-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时 `read_image` 不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图像输入,否则拒绝。
## `@deepseek-ai/dsh-tool-fs-search`
@@ -1193,7 +1214,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts)
-注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的示例 agent 会为每个 subagent 后端加载一次该包,因此模型还会看到 schema 相同、绑定到 fork 后端的 `subagent_fork`;见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。
+注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的组合会为每个 subagent 后端加载一次该包,因此模型还会看到绑定到 fork 后端的 `subagent_fork`。每个实例的描述与 `run_in_background` 参数取决于它自己的 `backgroundMode` 与 `enableRunInBackground`,因此两个随附 schema 并不相同:`subagent` 为 `continuable`,而 `subagent_fork` 保持 `one-shot`;见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。
## `@deepseek-ai/dsh-tool-subagent-control`
@@ -1220,7 +1241,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
### `list_agents`
-按持久 id 和标签列出你的可继续后台 subagent。状态来自实时注册表:running 表示 agent 此刻正在工作;idle 表示已加载但处于轮次之间,可能正在等待它启动的 agent;complete 表示它只存在于存储中。无论处于哪种状态,直接子级都仍可作为 `send_message` 的目标。该快照并非投递承诺;`send_message` 会执行权威检查,仍可能失败。无法读取的子级会作为诊断信息报告,而不会被静默丢弃。`descendants` 作用域会按稳定的前序顺序遍历你下方的整棵树,并为每个条目标注其持久的直接父会话 id 和深度。只有深度为 1 的条目可以使用 `send_message`;更深的条目只能作为 `interrupt_agent` 的候选目标。
+按持久 id 和标签列出你的可继续后台 subagent。用它回忆你启动过哪些 subagent,而不是轮询完成情况——subagent 完成时你会被告知。状态来自实时注册表:running 表示 agent 此刻正在工作;idle 表示已加载但处于轮次之间,可能正在等待它启动的 agent;ready 表示它只存在于存储中——可恢复而非终态,也不表示有结果等待收集;`send_message` 会在同一对话上开启新的轮次,且无论处于哪种状态,直接子级都仍可作为 `send_message` 的目标。该快照并非投递承诺;`send_message` 会执行权威检查,仍可能失败。无法读取的子级会作为诊断信息报告,而不会被静默丢弃。`descendants` 作用域会按稳定的前序顺序遍历你下方的整棵树,并为每个条目标注其持久的直接父会话 id 和深度。只有深度为 1 的条目可以使用 `send_message`;更深的条目只能作为 `interrupt_agent` 的候选目标。
```json
{
@@ -1272,7 +1293,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
### `report`
-向启动你的 agent 报告选定内容。你可以调用 0 次或多次,以报告进度、发现或最终答案。报告不会结束你的轮次或完成你的工作,且只有直接父级会收到。失败的调用仍可能已经送达,因此不要盲目重复。
+向启动你的 agent 报告选定内容。在你结束前调用一次,给出自包含的最终结果;当进度或发现会改变该 agent 接下来的行动时,也可以更早调用。该 agent 与你共享工作区,但不会自动收到你的 transcript(文本记录)、工具输出或推理,因此完成你的工作本身并不等于交出结果。报告不会结束你的轮次或完成你的工作,且只有直接父级会收到。失败的调用仍可能已经送达,因此不要盲目重复。
```json
{
@@ -1280,7 +1301,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
"properties": {
"output": {
"type": "string",
- "description": "Self-contained content for your parent; it does not see your private work."
+ "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
}
},
"required": [
@@ -1291,7 +1312,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
来源:[`packages/subagent/tool-subagent-report/src/index.ts`](../packages/subagent/tool-subagent-report/src/index.ts)
-按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。面向父级的 `send_message` 工具单独安装。
+按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。
## `@deepseek-ai/dsh-tool-tasks`
@@ -1362,7 +1383,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
来源:[`packages/tasks/tool-tasks/src/index.ts`](../packages/tasks/tool-tasks/src/index.ts)
-与任务种类无关的后台任务控制接口:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制接口,从而启用生产方的 `ctx.tasks.start()`。
+与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.tasks.start()`。
## `@deepseek-ai/dsh-tool-todo`
diff --git a/docs/tool-execution-pipeline.i18n.yaml b/docs/tool-execution-pipeline.i18n.yaml
index b629e4029e..bd8de3ba60 100644
--- a/docs/tool-execution-pipeline.i18n.yaml
+++ b/docs/tool-execution-pipeline.i18n.yaml
@@ -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 docs/tool-execution-pipeline.md
-tool-execution-pipeline.md: 6c925a404d7a161838e72ce4b03b7f2cad29d313
+tool-execution-pipeline.md: d04d2e4e5093fee92f8921f0eb0112c960a81bb8
tool-execution-pipeline.zh.md: 15627023d3be6ac2b3aae70c2ef01ef9f1077d3e
diff --git a/docs/tool-execution-pipeline.md b/docs/tool-execution-pipeline.md
index 6c925a404d..d04d2e4e50 100644
--- a/docs/tool-execution-pipeline.md
+++ b/docs/tool-execution-pipeline.md
@@ -57,6 +57,6 @@ flowchart TD
allResults --> context
```
-Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.
+Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.
Maintenance mode: curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs.
diff --git a/docs/user/develop/basic/config.i18n.yaml b/docs/user/develop/basic/config.i18n.yaml
index 2887d77209..b367fbb82e 100644
--- a/docs/user/develop/basic/config.i18n.yaml
+++ b/docs/user/develop/basic/config.i18n.yaml
@@ -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 docs/user/develop/basic/config.md
-config.md: 02998c32415b5ba7acf82700034cabc1f7314f33
-config.zh.md: 161af5d6703b4cc77d63443a5a80846593d1c7fd
+config.md: 21ba39fd7de1795e9139aff3e2b11743eedd4833
+config.zh.md: a882c4d59b0ac8e8ec27a5b32da5376b534a7f62
diff --git a/docs/user/develop/basic/config.md b/docs/user/develop/basic/config.md
index 02998c3241..21ba39fd7d 100644
--- a/docs/user/develop/basic/config.md
+++ b/docs/user/develop/basic/config.md
@@ -9,8 +9,8 @@ Accept configuration supplied through `cordis.yml`.
Export a `Config` type and a same-named Schemastery schema. Put defaults directly on the schema fields:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'my-plugin'
@@ -49,8 +49,8 @@ When loading the plugin, Cordis uses the exported schema to validate configurati
Use Schemastery to express stricter validation:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'validated-plugin'
diff --git a/docs/user/develop/basic/config.zh.md b/docs/user/develop/basic/config.zh.md
index 161af5d670..a882c4d59b 100644
--- a/docs/user/develop/basic/config.zh.md
+++ b/docs/user/develop/basic/config.zh.md
@@ -9,8 +9,8 @@
在插件中导出一个 `Config` 类型和同名的 Schemastery schema;默认值直接写在 schema 中:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'my-plugin'
@@ -49,8 +49,8 @@ export function apply(ctx: Context, config: Config) {
对于需要严格校验的场景,使用 Schemastery 定义 schema:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'validated-plugin'
diff --git a/docs/user/develop/basic/index.i18n.yaml b/docs/user/develop/basic/index.i18n.yaml
index 0d89e9622e..6684d18070 100644
--- a/docs/user/develop/basic/index.i18n.yaml
+++ b/docs/user/develop/basic/index.i18n.yaml
@@ -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 docs/user/develop/basic/index.md
-index.md: 7fe66bb19ddb978a4b5a96768b62151b97bca0ec
-index.zh.md: 59f4e5b58b6cf1fbc15de8fafb5f4b0db2e220d6
+index.md: e57a42b42690bd92450cc26876c13a1622bb80cc
+index.zh.md: 240623341618acd6501f2897ae2844fde0a5b73b
diff --git a/docs/user/develop/basic/index.md b/docs/user/develop/basic/index.md
index 7fe66bb19d..e57a42b426 100644
--- a/docs/user/develop/basic/index.md
+++ b/docs/user/develop/basic/index.md
@@ -17,7 +17,7 @@ mkdir -p scratch-plugin/src
In Harness, a plugin is a TypeScript module that exports an `apply` function. The framework calls `apply` when loading the plugin and passes a `ctx` context object through which the plugin registers capabilities:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
@@ -33,7 +33,7 @@ That is the complete configuration.
Create `scratch-plugin/src/my-plugin.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
@@ -56,7 +56,7 @@ Create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugi
Start the Web UI with that overlay:
```sh
-pnpm run dsh web --patch ./scratch-plugin/cordis.yml
+pnpm dsh web --patch ./scratch-plugin/cordis.yml
```
Open `http://127.0.0.1:3080`. The terminal prints `[hello-plugin] plugin loaded!` during startup.
@@ -68,7 +68,7 @@ Anything registered through `ctx`—event listeners, tools, or timers—is clean
For a resource that needs explicit cleanup, such as a network connection, use `ctx.effect()` to provide its disposer:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
@@ -87,7 +87,7 @@ export function apply(ctx: Context) {
If the plugin consumes another service such as `tools` or `llm`, declare it in `inject`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
@@ -107,7 +107,7 @@ In addition to a function module, a plugin can use object or class form.
### Object form
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export default {
name: 'my-plugin',
@@ -121,7 +121,7 @@ export default {
### Class form
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
diff --git a/docs/user/develop/basic/index.zh.md b/docs/user/develop/basic/index.zh.md
index 59f4e5b58b..2406233416 100644
--- a/docs/user/develop/basic/index.zh.md
+++ b/docs/user/develop/basic/index.zh.md
@@ -17,7 +17,7 @@ mkdir -p scratch-plugin/src
在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
@@ -33,7 +33,7 @@ export function apply(ctx: Context) {
创建 `scratch-plugin/src/my-plugin.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
@@ -56,7 +56,7 @@ export function apply(ctx: Context) {
使用该覆盖层启动 Web UI:
```sh
-pnpm run dsh web --patch ./scratch-plugin/cordis.yml
+pnpm dsh web --patch ./scratch-plugin/cordis.yml
```
打开 `http://127.0.0.1:3080`。启动期间,终端会打印 `[hello-plugin] plugin loaded!`。
@@ -68,7 +68,7 @@ pnpm run dsh web --patch ./scratch-plugin/cordis.yml
如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
@@ -87,7 +87,7 @@ export function apply(ctx: Context) {
如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
@@ -107,7 +107,7 @@ export function apply(ctx: Context) {
### 对象形式
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export default {
name: 'my-plugin',
@@ -121,7 +121,7 @@ export default {
### 类形式
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
diff --git a/docs/user/develop/basic/publish.i18n.yaml b/docs/user/develop/basic/publish.i18n.yaml
index d849ac4ae0..91dba947bb 100644
--- a/docs/user/develop/basic/publish.i18n.yaml
+++ b/docs/user/develop/basic/publish.i18n.yaml
@@ -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 docs/user/develop/basic/publish.md
-publish.md: 7657654b1467c14b22e0eb6372c2bc4e77db2f38
-publish.zh.md: 7af2ae3a06cc74597d5cbd6fddd46fbab069e287
+publish.md: 8437c7ea5c4cb966f9f3d68977949c78986ec9a5
+publish.zh.md: 4409dbfda060a84b316029d87ec985209cfa286a
diff --git a/docs/user/develop/basic/publish.md b/docs/user/develop/basic/publish.md
index 7657654b14..8437c7ea5c 100644
--- a/docs/user/develop/basic/publish.md
+++ b/docs/user/develop/basic/publish.md
@@ -98,7 +98,8 @@ The effective configuration composes over an empty root by applying, in order:
2. The profile's own `cordis.patch.yml`.
3. The home-level `$DSH_HOME/cordis.patch.yml` — machine-local preferences shared by every profile.
4. Each `--patch ` overlay, in argv order.
-5. Launcher flag patches (for example `dsh web --port`).
+
+App arguments are not another patch layer. A surface bundle can resolve them through an ordinary app-owned service, described below.
Later layers win per row, and a patch replaces a row's entire `config` value rather than deep-merging keys. Two consequences for bundle authors:
@@ -107,6 +108,29 @@ Later layers win per row, and a patch replaces a row's entire `config` value rat
In-box bundle names always resolve from the dsh installation itself; pnpm manages only out-of-tree packages, so your bundle can rely on `@deepseek-ai/dsh-base` being present and current.
+## Give a surface bundle its own command line
+
+A bundle that defines a runnable app mounts an ordinary provider plugin:
+
+```yaml
+- id: hello-startup
+ name: 'dsh-hello-plugin/startup'
+```
+
+The plugin exports `inject = ['cmdlineArgs']`, calls `parseCmdline` from [`@deepseek-ai/dsh-cmdline`](../../../../packages/boot/cmdline/README.md) with its own commander program, and provides the returned value as its app-owned service. The launcher hands every plugin the same immutable arguments after launcher flags, so app-specific flags need no launcher change and multiple plugins may parse the snapshot. The Loader row needs no launcher marker or special kind.
+
+Rows configured by those arguments inject the provider's service and read it from their own `!!js` options, with the deployment value beside it as the fallback:
+
+```yaml
+- id: my-app
+ name: '@example/my-app'
+ inject: [myAppStartup]
+ config:
+ port: !!js ctx.myAppStartup.port ?? 8080
+```
+
+On `--help`, the provider publishes no service, so those rows never activate. Loader mounts the composition once, waits for each row's ordinary injections, and only then evaluates that row's `!!js` config against its injected context.
+
## Installing from GitHub: the build-script catch
Publishing to a registry is not required — users can install straight from a git host:
diff --git a/docs/user/develop/basic/publish.zh.md b/docs/user/develop/basic/publish.zh.md
index 7af2ae3a06..4409dbfda0 100644
--- a/docs/user/develop/basic/publish.zh.md
+++ b/docs/user/develop/basic/publish.zh.md
@@ -2,14 +2,14 @@
[English](publish.md) | 中文
-前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**,用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。请先完成[插件配置](./config.md)。
+前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。请先完成[插件配置](./config.md)。
-## 两个概念,两种 manifest(元数据清单)
+## 两个概念,两种 manifest
-安装机制建立在两个概念之上。二者都由一份 `package.json` 描述,但它们在 `dsh` 键下携带的 manifest 种类不同,回答的问题也不同:
+安装机制建立在两个概念之上。二者都由一份 `package.json` 描述,但它们在 `dsh` 键下携带的 manifest(元数据清单)种类不同,回答的问题也不同:
-- **组合包**是附带一个配置层的 npm 包。它的 manifest 声明 `dsh.bundle`,回答的是「这个包贡献什么?」:一个插入或覆盖插件行的 patch 文件。
-- **profile** 是位于 `$DSH_HOME/profiles/` 下、描述一份可启动组合的目录。它的 manifest 声明 `dsh.profile`,回答的是「这套配置由哪些组合包按什么顺序组成?」。
+- **组合包**是附带一个配置层的 npm 包。它的 manifest 声明 `dsh.bundle`,回答的是"这个包贡献什么?":一个插入或覆盖插件行的 patch 文件。
+- **profile** 是位于 `$DSH_HOME/profiles/` 下、描述一份可启动组合的目录。它的 manifest 声明 `dsh.profile`,回答的是"这套配置由哪些组合包按什么顺序组成?"。
组合包是你编写并分发的东西;profile 是用户用 `dsh --profile ` 启动的东西。没有东西同时是两者。
@@ -98,7 +98,8 @@ dsh --profile demo
2. profile 自己的 `cordis.patch.yml`。
3. home 级的 `$DSH_HOME/cordis.patch.yml`——各 profile 共享的机器本地偏好。
4. 每个 `--patch ` overlay,按 argv 顺序。
-5. 启动器 flag patch(例如 `dsh web --port`)。
+
+应用参数不是另一层 patch。表层组合包可以通过下文所述的普通应用自有服务解析它们。
后应用的层按行胜出,且 patch 会替换目标行的整个 `config` 值,而不是深度合并各键。这给组合包作者带来两个推论:
@@ -107,6 +108,29 @@ dsh --profile demo
内置组合包名称始终从 dsh 安装目录本身解析;pnpm 只管理树外的包,所以你的组合包可以放心依赖 `@deepseek-ai/dsh-base` 存在且与安装保持一致。
+## 让表层组合包持有自己的命令行
+
+定义了可运行应用的组合包挂载一个普通提供方插件:
+
+```yaml
+- id: hello-startup
+ name: 'dsh-hello-plugin/startup'
+```
+
+该插件导出 `inject = ['cmdlineArgs']`,使用自己的 commander program 调用 [`@deepseek-ai/dsh-cmdline`](../../../../packages/boot/cmdline/README.md) 中的 `parseCmdline`,再把返回值作为应用自有服务提供出去。启动器把自身 flag 之后的同一份不可变参数交给每个插件,因此添加应用专属 flag 无需修改启动器,多个插件也可以解析该快照。Loader 行不需要启动器标记或特殊类型。
+
+受这些参数配置的行会注入提供方服务,并在自己的 `!!js` 选项中读取它,同时把部署取值写在旁边作为回退:
+
+```yaml
+- id: my-app
+ name: '@example/my-app'
+ inject: [myAppStartup]
+ config:
+ port: !!js ctx.myAppStartup.port ?? 8080
+```
+
+遇到 `--help` 时,提供方不会发布该服务,所以这些行不会激活。Loader 只挂载一次组合,等待每一行的普通注入,再基于其已注入的上下文求值该行的 `!!js` 配置。
+
## 从 GitHub 安装:构建脚本这道坎
发布到注册表不是必须的——用户可以直接从 git 托管安装:
@@ -127,7 +151,7 @@ dsh plugin --profile demo add github:you/hello-plugin
然后重新执行 `add`。
-请如实看待这项授权:**允许该包的代码在安装时于你的机器上执行**,且不在 agent(智能体)运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit(`github:you/hello-plugin#`),让后续推送无法悄悄改变实际运行的内容。
+请如实看待这项授权:**允许该包的代码在安装时于你的机器上执行**,且不在 agent 运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit(`github:you/hello-plugin#`),让后续推送无法悄悄改变实际运行的内容。
如果不想让用户做这项授权,就改为分发构建产物——以下两种形式都不需要任何构建权限:
diff --git a/docs/user/develop/basic/tool.i18n.yaml b/docs/user/develop/basic/tool.i18n.yaml
index 594e2c2872..2fdba218d0 100644
--- a/docs/user/develop/basic/tool.i18n.yaml
+++ b/docs/user/develop/basic/tool.i18n.yaml
@@ -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 docs/user/develop/basic/tool.md
-tool.md: ba2f3b1302ba31735d67be264f498a0395394d06
-tool.zh.md: 676f8fc996d752a05d94b55d4522e61e3b3e2161
+tool.md: a149f6876c573c9ece4b066ef6c211f69b0b03a8
+tool.zh.md: 1f9a1172bcde33a1d7319492f15686c47de01174
diff --git a/docs/user/develop/basic/tool.md b/docs/user/develop/basic/tool.md
index ba2f3b1302..a149f6876c 100644
--- a/docs/user/develop/basic/tool.md
+++ b/docs/user/develop/basic/tool.md
@@ -9,7 +9,7 @@ This tutorial adds a `greet` tool to the Web UI. Complete [Your first plugin](./
Replace `scratch-plugin/src/my-plugin.ts` with:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
@@ -40,7 +40,7 @@ export function apply(ctx: Context) {
Restart the development command if it is not running:
```sh
-pnpm run dsh web --patch ./scratch-plugin/cordis.yml
+pnpm dsh web --patch ./scratch-plugin/cordis.yml
```
Open `http://127.0.0.1:3080` and ask: `Use the greet tool to greet Ada.` The model can call `greet` and receives `Hello, Ada!` as the tool result.
diff --git a/docs/user/develop/basic/tool.zh.md b/docs/user/develop/basic/tool.zh.md
index 676f8fc996..1f9a1172bc 100644
--- a/docs/user/develop/basic/tool.zh.md
+++ b/docs/user/develop/basic/tool.zh.md
@@ -9,7 +9,7 @@
将 `scratch-plugin/src/my-plugin.ts` 替换为:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
@@ -40,7 +40,7 @@ export function apply(ctx: Context) {
如果开发命令未在运行,请重新启动:
```sh
-pnpm run dsh web --patch ./scratch-plugin/cordis.yml
+pnpm dsh web --patch ./scratch-plugin/cordis.yml
```
打开 `http://127.0.0.1:3080`,然后输入:`Use the greet tool to greet Ada.` 模型可以调用 `greet`,并收到 `Hello, Ada!` 这一工具结果。
diff --git a/docs/user/develop/framework/events.i18n.yaml b/docs/user/develop/framework/events.i18n.yaml
index f39b04145d..ebc4dc7833 100644
--- a/docs/user/develop/framework/events.i18n.yaml
+++ b/docs/user/develop/framework/events.i18n.yaml
@@ -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 docs/user/develop/framework/events.md
-events.md: 8a8c076d9c7b40d73182db074c4f494fded8c6dd
-events.zh.md: 9649d89d575a1b05fa524bd460043da71dc9ae43
+events.md: 4b5f9ee215186398ee5aee7a438f792f9b5a3639
+events.zh.md: b48c8020803239d3c9636f81052d3b70afa315f2
diff --git a/docs/user/develop/framework/events.md b/docs/user/develop/framework/events.md
index 8a8c076d9c..4b5f9ee215 100644
--- a/docs/user/develop/framework/events.md
+++ b/docs/user/develop/framework/events.md
@@ -85,9 +85,9 @@ A waterfall listener **must call `next()`**. Omitting it short-circuits the pipe
Harness uses TypeScript declaration merging for type-safe events:
```ts
-import 'cordis'
+import '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
@@ -121,7 +121,7 @@ export function apply(ctx: Context) {
This plugin logs tool calls and results:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
diff --git a/docs/user/develop/framework/events.zh.md b/docs/user/develop/framework/events.zh.md
index 9649d89d57..b48c802080 100644
--- a/docs/user/develop/framework/events.zh.md
+++ b/docs/user/develop/framework/events.zh.md
@@ -85,9 +85,9 @@ waterfall 监听器**必须调用 `next()`**。不调用 `next` 会短路整个
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
```ts
-import 'cordis'
+import '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
@@ -121,7 +121,7 @@ export function apply(ctx: Context) {
这个插件记录工具调用和工具结果:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
diff --git a/docs/user/develop/framework/index.i18n.yaml b/docs/user/develop/framework/index.i18n.yaml
index d06be13bdd..1c8dc3dae4 100644
--- a/docs/user/develop/framework/index.i18n.yaml
+++ b/docs/user/develop/framework/index.i18n.yaml
@@ -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 docs/user/develop/framework/index.md
-index.md: 79e925b54509da41535735527e283850384257ec
-index.zh.md: 962677dc468c9cc233a51d50758247e028d9c3ed
+index.md: 85701ce281d92da0c805b39291179df73eb65f51
+index.zh.md: 871aa55ef81a7dcbfe3cbde5986244220ee32f98
diff --git a/docs/user/develop/framework/index.md b/docs/user/develop/framework/index.md
index 79e925b545..85701ce281 100644
--- a/docs/user/develop/framework/index.md
+++ b/docs/user/develop/framework/index.md
@@ -80,7 +80,7 @@ export function apply(ctx: Context) {
To stop a plugin instance early:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
declare const ctx: Context
declare function myPlugin(ctx: Context): void
@@ -98,7 +98,7 @@ await fiber.dispose()
## Hot replacement (HMR)
-With `@cordisjs/plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers:
+With `@deepseek-ai/cordis-plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers:
1. Unload the old plugin and clean up its registrations.
2. Load the new code.
diff --git a/docs/user/develop/framework/index.zh.md b/docs/user/develop/framework/index.zh.md
index 962677dc46..871aa55ef8 100644
--- a/docs/user/develop/framework/index.zh.md
+++ b/docs/user/develop/framework/index.zh.md
@@ -80,7 +80,7 @@ export function apply(ctx: Context) {
当你需要提前终止一个插件实例:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
declare const ctx: Context
declare function myPlugin(ctx: Context): void
@@ -98,7 +98,7 @@ await fiber.dispose()
## HMR(热模块替换)
-通过 `cordis.yml` 加载 `@cordisjs/plugin-hmr` 后,修改插件源文件会触发:
+通过 `cordis.yml` 加载 `@deepseek-ai/cordis-plugin-hmr` 后,修改插件源文件会触发:
1. 卸载旧插件(清理所有注册)
2. 重新加载新代码
diff --git a/docs/user/develop/framework/service.i18n.yaml b/docs/user/develop/framework/service.i18n.yaml
index 7cb2f4ff88..29151de351 100644
--- a/docs/user/develop/framework/service.i18n.yaml
+++ b/docs/user/develop/framework/service.i18n.yaml
@@ -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 docs/user/develop/framework/service.md
-service.md: 040b1388cc431c30045e05f7d372ab5885bb3f9d
-service.zh.md: 0786b684c1688440a24cc729288835ad636f8ff7
+service.md: 3358f82ca5391741a7f531b7504de7335959ad03
+service.zh.md: 8fb4beeac43051c0f08483fe61e22c588127c360
diff --git a/docs/user/develop/framework/service.md b/docs/user/develop/framework/service.md
index 040b1388cc..3358f82ca5 100644
--- a/docs/user/develop/framework/service.md
+++ b/docs/user/develop/framework/service.md
@@ -36,7 +36,7 @@ When `apply` runs, every service declared by `inject` is ready. If a service is
### Extend Service
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
export default class MetricsService extends Service {
static inject = ['llm'] // A service may depend on other services.
@@ -67,9 +67,9 @@ export function apply(ctx: Context) {
Use TypeScript declaration merging to type `ctx.metrics`:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService
}
@@ -114,7 +114,7 @@ This prevents a plugin from calling a service that no longer exists.
```yaml
- id: group-a
- name: '@cordisjs/plugin-group'
+ name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
bash: true
@@ -125,7 +125,7 @@ This prevents a plugin from calling a service that no longer exists.
- name: './src/plugin-a.ts'
- id: group-b
- name: '@cordisjs/plugin-group'
+ name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
bash: true
diff --git a/docs/user/develop/framework/service.zh.md b/docs/user/develop/framework/service.zh.md
index 0786b684c1..8fb4beeac4 100644
--- a/docs/user/develop/framework/service.zh.md
+++ b/docs/user/develop/framework/service.zh.md
@@ -36,7 +36,7 @@ export function apply(ctx: Context) {
### 使用 Service 基类
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
export default class MetricsService extends Service {
static inject = ['llm'] // A service may depend on other services.
@@ -67,9 +67,9 @@ export function apply(ctx: Context) {
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService
}
@@ -114,7 +114,7 @@ export function apply(ctx: Context) {
```yaml
- id: group-a
- name: '@cordisjs/plugin-group'
+ name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
bash: true
@@ -125,7 +125,7 @@ export function apply(ctx: Context) {
- name: './src/plugin-a.ts'
- id: group-b
- name: '@cordisjs/plugin-group'
+ name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
bash: true
diff --git a/docs/user/develop/practice/index.i18n.yaml b/docs/user/develop/practice/index.i18n.yaml
index fc15dfeb2f..1bb86a5cbb 100644
--- a/docs/user/develop/practice/index.i18n.yaml
+++ b/docs/user/develop/practice/index.i18n.yaml
@@ -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 docs/user/develop/practice/index.md
-index.md: 1eb33e17ab6c5d0a2b37ff97d5948dfbcba497ca
-index.zh.md: 31afa80407f81f571615b5ed68a9370775f5188f
+index.md: 7ca9f0b1abe472dc90c6d4e56543e43b6d2ec727
+index.zh.md: 216b1cb01b355e411bf1949c59fd3720ae47139e
diff --git a/docs/user/develop/practice/index.md b/docs/user/develop/practice/index.md
index 1eb33e17ab..7ca9f0b1ab 100644
--- a/docs/user/develop/practice/index.md
+++ b/docs/user/develop/practice/index.md
@@ -61,9 +61,9 @@ The [capability-seam reference](../../../capability-seams.md) owns the current b
```ts ignore-check
// packages/my-cap/my-cap/src/index.ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
myCap: MyCapService
}
@@ -91,7 +91,7 @@ export interface MyCapResult {
```ts ignore-check
// packages/my-cap/my-cap-local/src/index.ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
class MyCapLocal extends MyCapService {
@@ -112,7 +112,7 @@ export function apply(ctx: Context) {
```ts ignore-check
// packages/my-cap/tool-my-cap/src/index.ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'tool-my-cap'
diff --git a/docs/user/develop/practice/index.zh.md b/docs/user/develop/practice/index.zh.md
index 31afa80407..216b1cb01b 100644
--- a/docs/user/develop/practice/index.zh.md
+++ b/docs/user/develop/practice/index.zh.md
@@ -61,9 +61,9 @@
```ts ignore-check
// packages/my-cap/my-cap/src/index.ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
myCap: MyCapService
}
@@ -91,7 +91,7 @@ export interface MyCapResult {
```ts ignore-check
// packages/my-cap/my-cap-local/src/index.ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
class MyCapLocal extends MyCapService {
@@ -112,7 +112,7 @@ export function apply(ctx: Context) {
```ts ignore-check
// packages/my-cap/tool-my-cap/src/index.ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'tool-my-cap'
diff --git a/docs/user/develop/practice/llm-adapter.i18n.yaml b/docs/user/develop/practice/llm-adapter.i18n.yaml
index a7a745faaf..c8487899ec 100644
--- a/docs/user/develop/practice/llm-adapter.i18n.yaml
+++ b/docs/user/develop/practice/llm-adapter.i18n.yaml
@@ -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 docs/user/develop/practice/llm-adapter.md
-llm-adapter.md: 7445688530c1ba61e5c065f9f5e49db6498da5b1
-llm-adapter.zh.md: c726735ff2679584d1c061ef8acddc8981dadd26
+llm-adapter.md: aba4a6d0c8ee42e78ca5a804d9a0dd9b31c1e240
+llm-adapter.zh.md: dff9eef464599823d6cd99e83d668485109b2ec0
diff --git a/docs/user/develop/practice/llm-adapter.md b/docs/user/develop/practice/llm-adapter.md
index 7445688530..aba4a6d0c8 100644
--- a/docs/user/develop/practice/llm-adapter.md
+++ b/docs/user/develop/practice/llm-adapter.md
@@ -11,8 +11,8 @@ An LLM adapter extends `LlmAdapter` and implements `stream()`, translating Harne
## Minimal implementation
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
class MyAdapter extends LlmAdapter {
diff --git a/docs/user/develop/practice/llm-adapter.zh.md b/docs/user/develop/practice/llm-adapter.zh.md
index c726735ff2..dff9eef464 100644
--- a/docs/user/develop/practice/llm-adapter.zh.md
+++ b/docs/user/develop/practice/llm-adapter.zh.md
@@ -11,8 +11,8 @@ LLM 适配器是一个继承 `LlmAdapter` 并实现 `stream()` 方法的类,
## 最小实现
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
class MyAdapter extends LlmAdapter {
diff --git a/docs/user/guide/config.i18n.yaml b/docs/user/guide/config.i18n.yaml
index 7feb2c7f07..00a8867092 100644
--- a/docs/user/guide/config.i18n.yaml
+++ b/docs/user/guide/config.i18n.yaml
@@ -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 docs/user/guide/config.md
-config.md: cd778065801ae58a46703ae3447f835f80abf062
-config.zh.md: 6f6d37bfe8f7ad29c154d65c1763279655006435
+config.md: 1d3ad5ce36d4b360ba5156b6be28a6caae4a23d4
+config.zh.md: 7f8bfaa77066f2976a5667e3ac402814a7afdf96
diff --git a/docs/user/guide/config.md b/docs/user/guide/config.md
index cd77806580..1d3ad5ce36 100644
--- a/docs/user/guide/config.md
+++ b/docs/user/guide/config.md
@@ -18,6 +18,10 @@ A minimal configuration is a list of plugin entries:
```yaml
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
+ config:
+ apiKey: !!js process.env.DEEPSEEK_API_KEY
+ models:
+ - deepseek-v4-flash
- id: bash
name: '@deepseek-ai/dsh-bash-local'
@@ -47,16 +51,17 @@ Cordis starts sibling entries concurrently. A plugin declares required services
## CLI patch layers
-`dsh --profile ` composes the profile's bundle patch layers (its manifest's `dsh.profile.bundles` list, in order) over an empty root, then the profile's own `~/.dsh/profiles//cordis.patch.yml`, then each `--patch ` overlay, then CLI-flag patches. Later layers win per row.
+`dsh --profile ` composes the profile's bundle patch layers (its manifest's `dsh.profile.bundles` list, in order) over an empty root, then the profile's own `~/.dsh/profiles//cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and each `--patch ` overlay. Later layers win per row. App flags are not another patch layer: an ordinary bundle plugin injects `cmdlineArgs` and provides parsed values as its own service, while rows that inject and retain a `!!js` read of that service give the invocation value precedence.
-A patch replaces a row's entire `config` value; it does not deep-merge keys. For example, patching `llm-deepseek` with only `config: { thinking: disabled }` also removes that row's configured `apiKeyEnv` and `baseURL`, so restate every key the row must retain.
+A patch replaces a row's entire `config` value; it does not deep-merge keys. For example, patching `llm-deepseek` with only `config: { thinking: disabled }` also removes that row's configured `apiKey` and `baseURL`, so restate every key the row must retain.
## JavaScript values and environment variables
-The Cordis loader evaluates runtime expressions tagged with `!!js` for non-secret runtime values. Bundled LLM adapters carry credential references such as `apiKeyEnv`; the value belongs in an environment layer or `$DSH_HOME/.credentials.yaml`, not Cordis configuration.
+The Cordis loader evaluates runtime expressions tagged with `!!js`. Keep API keys and other secrets in the gitignored `.env` file at the repository root, never in committed configuration.
```yaml
config:
+ apiKey: !!js process.env.DEEPSEEK_API_KEY
cwd: !!js process.cwd()
```
diff --git a/docs/user/guide/config.zh.md b/docs/user/guide/config.zh.md
index 6f6d37bfe8..7f8bfaa770 100644
--- a/docs/user/guide/config.zh.md
+++ b/docs/user/guide/config.zh.md
@@ -18,6 +18,10 @@ Harness 使用 `cordis.yml` 描述 agent(智能体)加载哪些插件以及
```yaml
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
+ config:
+ apiKey: !!js process.env.DEEPSEEK_API_KEY
+ models:
+ - deepseek-v4-flash
- id: bash
name: '@deepseek-ai/dsh-bash-local'
@@ -47,16 +51,17 @@ Cordis 会并发启动同级配置项。插件通过 `inject` 声明必需服务
## CLI 补丁层
-`dsh --profile ` 按该 profile 的 manifest(元数据清单)中 `dsh.profile.bundles` 列表的顺序,在空根之上组合各组合包补丁层,随后依次应用该 profile 自己的 `~/.dsh/profiles//cordis.patch.yml`、home 级 `$DSH_HOME/cordis.patch.yml`、每个 `--patch ` overlay,最后是 CLI(命令行界面)标志补丁。同一行以较后的层为准。
+`dsh --profile ` 按该 profile 的 manifest(元数据清单)中 `dsh.profile.bundles` 列表的顺序,在空根之上组合各组合包补丁层,随后依次应用该 profile 自己的 `~/.dsh/profiles//cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 与每个 `--patch ` overlay。同一行以较后的层为准。应用 flag 并不是另一层 patch:组合包中的普通插件注入 `cmdlineArgs`,再把解析值作为自身服务提供;注入该服务并保留其 `!!js` 读取的行会让本次调用的取值优先。
-补丁会替换目标行的整个 `config` 值,而不是深度合并各个键。例如,只用 `config: { thinking: disabled }` 修补 `llm-deepseek`,也会移除该行原有的 `apiKeyEnv` 与 `baseURL`;因此必须重新写出该行需要保留的全部键。
+补丁会替换目标行的整个 `config` 值,而不是深度合并各个键。例如,只用 `config: { thinking: disabled }` 修补 `llm-deepseek`,也会移除该行原有的 `apiKey` 与 `baseURL`;因此必须重新写出该行需要保留的全部键。
## JavaScript 值和环境变量
-Cordis loader 会求值以 `!!js` 标记的运行时表达式,用于非机密的运行时值。仓库内置的 LLM(大语言模型)适配器携带 `apiKeyEnv` 等凭据引用;对应的值应放在环境层或 `$DSH_HOME/.credentials.yaml`,而不是 Cordis 配置中。
+Cordis loader 使用 `!!js` 标签读取运行时表达式。API key 等凭据应放在仓库根目录、已被 Git 忽略的 `.env` 中,不能提交到配置文件。
```yaml
config:
+ apiKey: !!js process.env.DEEPSEEK_API_KEY
cwd: !!js process.cwd()
```
@@ -64,4 +69,4 @@ config:
## 精确配置参考
-每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../architecture.md)和[能力 seam](../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../examples/README.md)中最接近的例子。
+每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../architecture.md)和[能力接口](../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../examples/README.md)中最接近的例子。
diff --git a/docs/user/guide/providers.i18n.yaml b/docs/user/guide/providers.i18n.yaml
index c95a0db6c0..9da7fd1113 100644
--- a/docs/user/guide/providers.i18n.yaml
+++ b/docs/user/guide/providers.i18n.yaml
@@ -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 docs/user/guide/providers.md
-providers.md: 4667e54161e77a62d454f3f78a8164d5c79b78c8
-providers.zh.md: 15e0ccc826a5c285c71d4776793f8048c11f6254
+providers.md: 0e7ed11d1b09a8361d75b576a400978ac66d08a7
+providers.zh.md: 060bf3dc41b773e89cd0d78de921c3a20cfc6076
diff --git a/docs/user/guide/providers.md b/docs/user/guide/providers.md
index 4667e54161..0e7ed11d1b 100644
--- a/docs/user/guide/providers.md
+++ b/docs/user/guide/providers.md
@@ -15,7 +15,7 @@ Adding a provider therefore rarely means editing `cordis.yml` — writing settin
## Configure from the web UI
-Start `pnpm run dsh web` and open **Settings → Models**.
+Start `pnpm dsh web` and open **Settings → Models**.

@@ -29,6 +29,8 @@ That holds for providers that authenticate with an API key. The catalog also car

+Every field but the Provider ID stays editable afterwards: **Edit** on the row reopens the same fields, with the display name and the protocol under **Customized settings** beside the base URL. Clearing the display name falls back to the Provider ID. The Provider ID itself is fixed: it names the route in requests, in `agent-default-model`, and in every session already logged, and it is the stem of the credential reference the page can never read back — so renaming a route means declaring a new provider and deleting the old one.
+
**Let the endpoint report its models.** Expand **Model catalog** and choose **Fetch available models**: the interrogation asks the endpoint **the form currently shows** — including a base URL edited but not yet saved and a key typed but not yet stored — and offers what it reports as candidates to pick from. A route the installed catalog describes is answered from that catalog with no network call. Adopting a candidate only writes rows into the draft; nothing is stored until you save.
Keys are write-only: the page only ever holds a redacted descriptor, never the literal secret. A key you enter is stored in `$DSH_HOME/.credentials.yaml`, and the profile records only the variable name that references it.
diff --git a/docs/user/guide/providers.zh.md b/docs/user/guide/providers.zh.md
index 15e0ccc826..060bf3dc41 100644
--- a/docs/user/guide/providers.zh.md
+++ b/docs/user/guide/providers.zh.md
@@ -15,7 +15,7 @@ Harness 出厂自带 DeepSeek,同时预装了一个通用的多提供方适配
## 在 Web 界面里配置
-启动 `pnpm run dsh web`,打开**设置 → 模型**。
+启动 `pnpm dsh web`,打开**设置 → 模型**。

@@ -29,6 +29,8 @@ Harness 出厂自带 DeepSeek,同时预装了一个通用的多提供方适配

+除 Provider ID 外的每个字段之后都还能改:行上的**编辑**会重新打开这些字段,显示名称和协议在「自定义设置」里、紧挨着 API 地址;显示名称清空即退回 Provider ID。Provider ID 本身固定不可改:它在请求里、在 `agent-default-model` 里、在每一条已记录的会话里点名这条路由,同时还是凭据引用的词干,而页面永远读不回凭据值——因此重命名一条路由等于声明一个新提供方再把旧的删掉。
+
**让端点自己报模型。** 展开**模型目录**后点**获取可用模型**,会按你**当前表单里**的地址与密钥去问端点(地址改了但没保存、密钥刚输入还没存下,都算数),把它报告的模型列成候选让你勾选。内置目录里的路由直接由目录作答,不联网。采纳只是把行写进草稿,最终还是你点保存才落盘。
密钥是只写的:页面拿到的永远是脱敏描述符,不是明文。写入的密钥存进 `$DSH_HOME/.credentials.yaml`,profile 里只记录引用它的变量名。
diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml
new file mode 100644
index 0000000000..eaf2543f90
--- /dev/null
+++ b/docs/user/guide/python-sdk.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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 docs/user/guide/python-sdk.md
+python-sdk.md: 0713de6f96c110624d5f5a8f2ccf56dd4ce01096
+python-sdk.zh.md: f8fa877b407a43c643fda405e1059fb487182761
diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md
new file mode 100644
index 0000000000..0713de6f96
--- /dev/null
+++ b/docs/user/guide/python-sdk.md
@@ -0,0 +1,130 @@
+# Get started with the Python SDK
+
+English | [中文](python-sdk.zh.md)
+
+This tutorial installs the Python SDK, runs a checked-in Cordis composition without the Web UI, and uses the same API in your own program. It uses the compact [`minimal.cordis.yml`](../../../examples/jsonrpc-agent/minimal.cordis.yml) configuration as a complete example with a fixed system prompt, tool catalog, persistent-shell behavior, and compaction policy.
+
+## Prerequisites
+
+- Python 3.10 or newer
+- Linux x64, Linux arm64, or macOS arm64
+- A DeepSeek-compatible API endpoint and credential
+- An isolated workspace that the agent may modify
+
+## Install the SDK
+
+Choose either the public package or a source build. Both install the `deepseek-harness-sdk` distribution and expose the `deepseek_harness` Python module.
+
+### Install from PyPI
+
+Create a virtual environment and install the SDK with its same-version bundled runtime:
+
+```sh
+python -m venv .venv
+. .venv/bin/activate
+python -m pip install deepseek-harness-sdk
+```
+
+### Build from source
+
+A source build additionally requires Git, Node.js ^22.19 or >= 24, Corepack-enabled pnpm 11, and `uv`. The following commands build the runtime for the current supported host platform, build both wheels, and install them into the active virtual environment:
+
+```sh
+git clone https://github.com/deepseek-ai/deepseek-harness.git deepseek-harness
+cd deepseek-harness
+python -m pip install uv==0.11.23
+corepack enable
+pnpm install
+
+case "$(uname -s):$(uname -m)" in
+ Linux:x86_64) runtime_platform=linux-x64 ;;
+ Linux:aarch64|Linux:arm64) runtime_platform=linux-arm64 ;;
+ Darwin:arm64) runtime_platform=macos-arm64 ;;
+ *) echo "unsupported platform" >&2; exit 1 ;;
+esac
+
+pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets="node24-$runtime_platform"
+version="$(node -p "require('./package.json').version")"
+python scripts/build-python-release.py --package sdk --output-dir dist-python
+python scripts/build-python-release.py \
+ --package runtime \
+ --platform "$runtime_platform" \
+ --runtime-exe "dist-exe/dsh-jsonrpc-agent-pkg-$runtime_platform" \
+ --output-dir dist-python
+python -m pip install --find-links dist-python "deepseek-harness-sdk==$version"
+```
+
+The runtime wheel contains the JSON-RPC executable and every plugin used by the complete [`minimal.cordis.yml`](../../../examples/jsonrpc-agent/minimal.cordis.yml), so neither installation path needs Node.js after installation.
+
+## Run the checked-in example
+
+Set the credential in the environment. Set `DEEPSEEK_BASE_URL` as well when the model is served by an OpenAI-compatible proxy rather than the default DeepSeek endpoint.
+
+```sh
+export DEEPSEEK_API_KEY=sk-your-key-here
+# export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
+```
+
+Run one task from the repository checkout:
+
+```sh
+python examples/jsonrpc-agent/minimal.py \
+ --workspace /absolute/path/to/workspace \
+ --session-root /absolute/path/to/sessions \
+ --session-id example-001 \
+ "Inspect the repository and fix the failing tests."
+```
+
+The script prints the final assistant response. The session root receives a JSONL session log containing the assembled model request and every tool call.
+
+## Use the SDK in your own program
+
+The example is a thin wrapper around this SDK call:
+
+```python
+from pathlib import Path
+
+from deepseek_harness import DeepSeekHarness
+
+config = Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve()
+workspace = Path("/absolute/path/to/workspace").resolve()
+sessions = Path("/absolute/path/to/sessions").resolve()
+
+with DeepSeekHarness(
+ provider="deepseek-official",
+ model="deepseek-v4-flash",
+ max_tokens=49_152,
+ cwd=str(workspace),
+ session_root=str(sessions),
+ cordis=str(config),
+) as harness:
+ result = harness.run(
+ "Inspect the repository and fix the failing tests.",
+ session_id="example-001",
+ )
+
+print(result.final_response)
+```
+
+`DeepSeekHarness` starts the bundled JSON-RPC runtime lazily and reuses it until the context manager exits. Reusing the same harness and session id across calls also preserves the session-owned Bash process, including its working directory, exported variables, and shell functions.
+
+## Understand the example configuration
+
+| Surface | Fixed value |
+|---|---|
+| System prompt | `You are a helpful software engineer assistant.` |
+| Model-facing tools | Persistent `bash` and `str_replace_editor` only |
+| Bash timeout | 300 seconds |
+| Editor output limit | 16,000 characters |
+| Compaction | Trigger ratio `0.8`, retain `20,480` tokens, summary cap `8,192` tokens, one retry |
+| Session persistence | Uncompressed JSONL under `DSH_SESSION_ROOT` |
+
+The configuration omits harness identity, workspace prompt text, skills, one-shot Bash, task tools, and every other model-facing plugin. Filesystem policy facts are logged as runtime user context rather than appended to the system prompt. The editor requires absolute paths as an unconditional current contract, so the obsolete `requireAbsolutePath` option is absent.
+
+## Choose workspace and session IDs
+
+`cwd` selects the workspace available to the agent, while `session_root` stores session logs and state. Use a fresh session id for an independent task; reuse an id only when the next call should continue the same conversation and persistent shell state.
+
+The composition uses `danger-full-access`. Run it only inside a disposable checkout or container: Bash and the editor can modify any path allowed to the runtime process. The persistent PTY backend requires a POSIX terminal substrate and is not a Windows agent surface.
+
+For the complete SDK lifecycle and result contract, see the [Python SDK reference](../../../python/sdk/README.md). For Cordis composition syntax, see [Configuration](./config.md).
diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md
new file mode 100644
index 0000000000..f8fa877b40
--- /dev/null
+++ b/docs/user/guide/python-sdk.zh.md
@@ -0,0 +1,130 @@
+# Python SDK 快速上手
+
+[English](python-sdk.md) | 中文
+
+本教程介绍如何安装 Python SDK、在不使用 Web UI 的情况下运行仓库内置 Cordis 组合,以及如何在自己的程序中调用同一套 API。教程使用精简且完整的 [`minimal.cordis.yml`](../../../examples/jsonrpc-agent/minimal.cordis.yml) 作为示例,其中固定了系统提示词、工具目录、持久 shell 行为和压缩(compaction)策略。
+
+## 前置要求
+
+- Python 3.10 或更高版本
+- Linux x64、Linux arm64 或 macOS arm64
+- DeepSeek 兼容的 API 端点与凭据
+- agent 可以修改的隔离 workspace
+
+## 安装 SDK
+
+可以选择安装公开包或从源码构建。两种方式都会安装 `deepseek-harness-sdk` 分发包,并提供 `deepseek_harness` Python 模块。
+
+### 从 PyPI 安装
+
+请创建虚拟环境,并安装 SDK 及其同版本内置运行时:
+
+```sh
+python -m venv .venv
+. .venv/bin/activate
+python -m pip install deepseek-harness-sdk
+```
+
+### 从源码构建
+
+从源码构建还需要 Git、Node.js ^22.19 或 >= 24、通过 Corepack 启用的 pnpm 11,以及 `uv`。以下命令为当前受支持的宿主平台构建运行时和两个 wheel 包,并将它们安装进当前虚拟环境:
+
+```sh
+git clone https://github.com/deepseek-ai/deepseek-harness.git deepseek-harness
+cd deepseek-harness
+python -m pip install uv==0.11.23
+corepack enable
+pnpm install
+
+case "$(uname -s):$(uname -m)" in
+ Linux:x86_64) runtime_platform=linux-x64 ;;
+ Linux:aarch64|Linux:arm64) runtime_platform=linux-arm64 ;;
+ Darwin:arm64) runtime_platform=macos-arm64 ;;
+ *) echo "unsupported platform" >&2; exit 1 ;;
+esac
+
+pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets="node24-$runtime_platform"
+version="$(node -p "require('./package.json').version")"
+python scripts/build-python-release.py --package sdk --output-dir dist-python
+python scripts/build-python-release.py \
+ --package runtime \
+ --platform "$runtime_platform" \
+ --runtime-exe "dist-exe/dsh-jsonrpc-agent-pkg-$runtime_platform" \
+ --output-dir dist-python
+python -m pip install --find-links dist-python "deepseek-harness-sdk==$version"
+```
+
+运行时 wheel 包含 JSON-RPC 可执行文件,以及完整 [`minimal.cordis.yml`](../../../examples/jsonrpc-agent/minimal.cordis.yml) 使用的每个插件,因此两种安装方式完成后都不再需要 Node.js。
+
+## 运行仓库内置示例
+
+请在环境中设置凭据。如果模型不是由默认 DeepSeek 端点提供,而是通过 OpenAI 兼容代理提供,还需要设置 `DEEPSEEK_BASE_URL`。
+
+```sh
+export DEEPSEEK_API_KEY=sk-your-key-here
+# export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
+```
+
+从仓库 checkout 运行一个任务:
+
+```sh
+python examples/jsonrpc-agent/minimal.py \
+ --workspace /absolute/path/to/workspace \
+ --session-root /absolute/path/to/sessions \
+ --session-id example-001 \
+ "Inspect the repository and fix the failing tests."
+```
+
+脚本会打印 assistant 的最终回复。会话根目录会收到 JSONL 会话日志,其中包含组装后的模型请求与每次工具调用。
+
+## 在自己的程序中使用 SDK
+
+该示例是以下 SDK 调用的轻量包装层:
+
+```python
+from pathlib import Path
+
+from deepseek_harness import DeepSeekHarness
+
+config = Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve()
+workspace = Path("/absolute/path/to/workspace").resolve()
+sessions = Path("/absolute/path/to/sessions").resolve()
+
+with DeepSeekHarness(
+ provider="deepseek-official",
+ model="deepseek-v4-flash",
+ max_tokens=49_152,
+ cwd=str(workspace),
+ session_root=str(sessions),
+ cordis=str(config),
+) as harness:
+ result = harness.run(
+ "Inspect the repository and fix the failing tests.",
+ session_id="example-001",
+ )
+
+print(result.final_response)
+```
+
+`DeepSeekHarness` 会延迟启动内置 JSON-RPC 运行时,并持续复用,直至退出上下文管理器。在多次调用中复用同一个 harness 和 session id,还会保留该会话拥有的 Bash 进程,包括其工作目录、已导出的变量与 shell 函数。
+
+## 了解示例配置
+
+| 方面 | 固定值 |
+|---|---|
+| 系统提示词 | `You are a helpful software engineer assistant.` |
+| 面向模型的工具 | 仅持久 `bash` 与 `str_replace_editor` |
+| Bash 超时 | 300 秒 |
+| 编辑器输出上限 | 16,000 个字符 |
+| 压缩 | 触发比例 `0.8`、保留 `20,480` 个 token、摘要上限 `8,192` 个 token、重试 1 次 |
+| 会话持久化 | `DSH_SESSION_ROOT` 下未压缩的 JSONL |
+
+该配置省略了 harness 身份、workspace 提示词文本、skill(技能)、一次性 Bash、任务工具和其他所有面向模型的插件。文件系统策略事实记录为运行时用户上下文,而不会追加到系统提示词中。编辑器无条件要求绝对路径,因此配置中没有已经废弃的 `requireAbsolutePath` 选项。
+
+## 选择 workspace 与 session id
+
+`cwd` 用于选择 agent 可访问的 workspace,`session_root` 用于保存会话日志和状态。独立任务应使用新的 session id;只有下一次调用需要延续同一段对话和持久 shell 状态时,才复用原有 id。
+
+该组合使用 `danger-full-access`。只能在可丢弃的 checkout 或容器内运行:Bash 与编辑器可以修改运行时进程有权访问的任何路径。持久 PTY 后端需要 POSIX 终端环境,因此该模式不适用于 Windows agent。
+
+完整的 SDK 生命周期与结果约定见 [Python SDK 参考](../../../python/sdk/README.md)。Cordis 组合语法见[配置](./config.md)。
diff --git a/docs/user/guide/quickstart.i18n.yaml b/docs/user/guide/quickstart.i18n.yaml
index dc9b7cb25b..0cca002d4f 100644
--- a/docs/user/guide/quickstart.i18n.yaml
+++ b/docs/user/guide/quickstart.i18n.yaml
@@ -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 docs/user/guide/quickstart.md
-quickstart.md: ce196641f205324334533c025b4ac1dc791f857d
-quickstart.zh.md: 3a5d6d0748ec0c7ec83c74570d0fad1e8d66a97c
+quickstart.md: e93e5a430f0cb345728581cd6fa3175ffd20b7d1
+quickstart.zh.md: 69cde830bb802ef19cc1204685395b957a0e02e3
diff --git a/docs/user/guide/quickstart.md b/docs/user/guide/quickstart.md
index ce196641f2..e93e5a430f 100644
--- a/docs/user/guide/quickstart.md
+++ b/docs/user/guide/quickstart.md
@@ -19,10 +19,9 @@ pnpm -v
## Step 1: install and configure the API key
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
-pnpm run build
```
Create the gitignored repository-root `.env`:
@@ -36,27 +35,28 @@ DEEPSEEK_API_KEY=sk-your-key-here
Run a non-interactive task and print its final answer:
```sh
-pnpm run dsh run "summarize the architecture of this workspace"
+pnpm dsh --profile headless "summarize the architecture of this workspace"
```
-`dsh run` creates and persists a fresh session, prints the final assistant answer, and exits. It starts no Web server or listening port, and a successful run leaves stderr empty.
+`dsh --profile headless` creates and persists a fresh session, prints the final assistant answer, and exits. It starts no Web server or listening port, and a successful run leaves stderr empty.
## Step 3: use the Web UI
Start the browser interface:
```sh
-pnpm run dsh web
+pnpm dsh web
```
Open `http://127.0.0.1:3080`. The agent can read and write files, run commands, delegate subtasks, and track a plan. Try: `Create hello.js in the current directory, print "Hello from Harness!", and run it`.
## What happened
-`dsh run` boots the `headless` profile: [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) and [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) compose over an empty root, then the runner drives the core Agent and Session services directly. `dsh web` instead composes `dsh-base` with [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml), which owns the Host, HTTP, and browser layers. Both read the same default DeepSeek model route from `dsh-base`.
+`dsh --profile headless` boots the `headless` profile: [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) and [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) compose over an empty root, then the runner drives the core Agent and Session services directly. `dsh web` instead composes `dsh-base` with [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml), which owns the Host, HTTP, and browser layers. Both read the same default DeepSeek model route from `dsh-base`.
## Next steps
+- [Get started with the Python SDK](./python-sdk.md) — install the SDK and run a complete Cordis configuration without the Web UI
- [Configure models](./providers.md) — reach providers beyond DeepSeek, and custom gateways
- [Configuration](./config.md) — understand the `cordis.yml` format
- [Develop a plugin](../develop/basic/) — build your own tool or backend
diff --git a/docs/user/guide/quickstart.zh.md b/docs/user/guide/quickstart.zh.md
index 3a5d6d0748..69cde830bb 100644
--- a/docs/user/guide/quickstart.zh.md
+++ b/docs/user/guide/quickstart.zh.md
@@ -19,10 +19,9 @@ pnpm -v
## 第一步:安装并配置 API 密钥
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
-pnpm run build
```
在仓库根目录创建已被 Git 忽略的 `.env`:
@@ -36,27 +35,28 @@ DEEPSEEK_API_KEY=sk-your-key-here
运行一个非交互式任务并打印最终回答:
```sh
-pnpm run dsh run "summarize the architecture of this workspace"
+pnpm dsh --profile headless "summarize the architecture of this workspace"
```
-`dsh run` 创建并持久化一个新会话,打印最终助手回答,然后退出。它不会启动 Web 服务器或监听端口;成功运行时 stderr 为空。
+`dsh --profile headless` 创建并持久化一个新会话,打印最终助手回答,然后退出。它不会启动 Web 服务器或监听端口;成功运行时 stderr 为空。
## 第三步:使用 Web UI
启动浏览器界面:
```sh
-pnpm run dsh web
+pnpm dsh web
```
打开 `http://127.0.0.1:3080`。agent 可以读写文件、运行命令、分配子任务和跟踪计划。可以尝试:`Create hello.js in the current directory, print "Hello from Harness!", and run it`。
## 运行原理
-`dsh run` 启动 `headless` profile:[`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) 和 [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) 在空根之上组合,随后 runner 直接驱动 core Agent 与 Session 服务。`dsh web` 则由 `dsh-base` 与 [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 组合,后者拥有 Host、HTTP 与浏览器层。二者都从 `dsh-base` 读取同一个默认 DeepSeek 模型路由。
+`dsh --profile headless` 启动 `headless` profile:[`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) 和 [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) 在空根之上组合,随后 runner 直接驱动 core Agent 与 Session 服务。`dsh web` 则由 `dsh-base` 与 [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 组合,后者拥有 Host、HTTP 与浏览器层。二者都从 `dsh-base` 读取同一个默认 DeepSeek 模型路由。
## 下一步
+- [Python SDK 快速上手](./python-sdk.md) — 安装 SDK,并在不使用 Web UI 的情况下运行完整 Cordis 配置
- [配置模型](./providers.md) — 接入 DeepSeek 之外的提供方与自定义网关
- [配置文件](./config.md) — 了解 `cordis.yml` 的格式
- [开发插件](../develop/basic/) — 编写自己的工具或后端
diff --git a/examples/acp-agent/advanced.cordis.snapshot.yml b/examples/acp-agent/advanced.cordis.snapshot.yml
index c89fdaf2a6..19d4a79fca 100644
--- a/examples/acp-agent/advanced.cordis.snapshot.yml
+++ b/examples/acp-agent/advanced.cordis.snapshot.yml
@@ -1,6 +1,6 @@
# Replay counterpart to advanced.cordis.yml; only the live model is replaced.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/advanced.cordis.yml b/examples/acp-agent/advanced.cordis.yml
index aa20e1558d..1273d4f059 100644
--- a/examples/acp-agent/advanced.cordis.yml
+++ b/examples/acp-agent/advanced.cordis.yml
@@ -1,7 +1,7 @@
# Add Code Mode and Cordis tools to the base spawn/workflow stack, exercising
# all four boundaries in one ACP snapshot.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/both-mode.cordis.snapshot.yml b/examples/acp-agent/both-mode.cordis.snapshot.yml
index 84a286649a..8f2c92d62e 100644
--- a/examples/acp-agent/both-mode.cordis.snapshot.yml
+++ b/examples/acp-agent/both-mode.cordis.snapshot.yml
@@ -2,7 +2,7 @@
# swap. Include patches cannot target entries behind a nested include, so this file
# applies both overlays directly to `cordis.yml`.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/both-mode.cordis.yml b/examples/acp-agent/both-mode.cordis.yml
index d793e616f9..58e465176c 100644
--- a/examples/acp-agent/both-mode.cordis.yml
+++ b/examples/acp-agent/both-mode.cordis.yml
@@ -3,7 +3,7 @@
# this overlay for snapshot recording and the sibling overlay for replay. A config
# patch replaces the whole app config, so unchanged base fields are restated below.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/child-question.cordis.snapshot.yml b/examples/acp-agent/child-question.cordis.snapshot.yml
index 4eb0c5bfb4..d9d87e8db9 100644
--- a/examples/acp-agent/child-question.cordis.snapshot.yml
+++ b/examples/acp-agent/child-question.cordis.snapshot.yml
@@ -2,7 +2,7 @@
# seam, model-facing tool, and tripwire provider while replacing DeepSeek with
# per-session replay.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/child-question.cordis.yml b/examples/acp-agent/child-question.cordis.yml
index 803d21c35d..8de7fe135b 100644
--- a/examples/acp-agent/child-question.cordis.yml
+++ b/examples/acp-agent/child-question.cordis.yml
@@ -1,7 +1,7 @@
# Snapshot-only human-interaction composition. The provider is a tripwire: the
# runtime-owned child must be rejected by the seam before any UI wait begins.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml b/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml
index 684ac2b27d..650d950e69 100644
--- a/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml
+++ b/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml
@@ -1,7 +1,7 @@
# Keyless replay counterpart of code-mode-workspace-context.cordis.yml. It adds
# Code Mode to the default filesystem suite and swaps in replay.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/code-mode-workspace-context.cordis.yml b/examples/acp-agent/code-mode-workspace-context.cordis.yml
index a724a86961..02ca7d40de 100644
--- a/examples/acp-agent/code-mode-workspace-context.cordis.yml
+++ b/examples/acp-agent/code-mode-workspace-context.cordis.yml
@@ -1,7 +1,7 @@
# Code Mode workspace-context snapshot recording overlay. The default filesystem
# tools trigger nested instruction discovery after a read.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/code-mode.cordis.snapshot.yml b/examples/acp-agent/code-mode.cordis.snapshot.yml
index 992a442343..7c114d81f3 100644
--- a/examples/acp-agent/code-mode.cordis.snapshot.yml
+++ b/examples/acp-agent/code-mode.cordis.snapshot.yml
@@ -2,7 +2,7 @@
# swap. Include patches cannot target entries behind a nested include, so this file
# applies both overlays directly to `cordis.yml`.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/code-mode.cordis.yml b/examples/acp-agent/code-mode.cordis.yml
index ec31a1fc87..8ad70610b9 100644
--- a/examples/acp-agent/code-mode.cordis.yml
+++ b/examples/acp-agent/code-mode.cordis.yml
@@ -4,7 +4,7 @@
# replay overlay for `DSH_SNAPSHOT=replay`. A config patch replaces the whole app
# config, so unchanged base fields are restated below.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/cordis-tools.cordis.yml b/examples/acp-agent/cordis-tools.cordis.yml
index c6e3e84457..87ba11858e 100644
--- a/examples/acp-agent/cordis-tools.cordis.yml
+++ b/examples/acp-agent/cordis-tools.cordis.yml
@@ -1,7 +1,7 @@
# Add the self-referential Cordis tools without changing the base ACP tool
# presentation mode.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/cordis.snapshot.yml b/examples/acp-agent/cordis.snapshot.yml
index 5bc771a0fc..46eff1e869 100644
--- a/examples/acp-agent/cordis.snapshot.yml
+++ b/examples/acp-agent/cordis.snapshot.yml
@@ -9,7 +9,7 @@
# `DSH_SNAPSHOT_OVERRIDE` from the harness. The one-shot patch applies at include
# load time, and stdout remains reserved for ACP JSON-RPC.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml
index 59e7b78278..a73d6d36e4 100644
--- a/examples/acp-agent/cordis.yml
+++ b/examples/acp-agent/cordis.yml
@@ -121,12 +121,16 @@
backgroundMode: continuable
maxDepth: 1
+# Fork stays one-shot because a continuable child's `report` tool and prompt
+# section precede the inherited history a fork reuses; `run_in_background` is off
+# because this example mounts no task service. See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md.
- id: tool-subagent-fork
name: '@deepseek-ai/dsh-tool-subagent'
config:
provider: fork
toolName: subagent_fork
- backgroundMode: continuable
+ backgroundMode: one-shot
+ enableRunInBackground: false
maxDepth: 1
diff --git a/examples/acp-agent/depth-two.cordis.snapshot.yml b/examples/acp-agent/depth-two.cordis.snapshot.yml
index 3e292699d1..e988f9be60 100644
--- a/examples/acp-agent/depth-two.cordis.snapshot.yml
+++ b/examples/acp-agent/depth-two.cordis.snapshot.yml
@@ -1,7 +1,7 @@
# Keyless counterpart to depth-two.cordis.yml: apply the depth patch and replace
# the live adapter with per-session replay.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/depth-two.cordis.yml b/examples/acp-agent/depth-two.cordis.yml
index 1af96e9283..73e02d7ffc 100644
--- a/examples/acp-agent/depth-two.cordis.yml
+++ b/examples/acp-agent/depth-two.cordis.yml
@@ -1,7 +1,7 @@
# Depth-limit snapshot overlay: keep the default composition and allow two
# generations of spawn children before runtime enforcement rejects another.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/fs.cordis.snapshot.yml b/examples/acp-agent/fs.cordis.snapshot.yml
index 0cab77bb36..ee922dba07 100644
--- a/examples/acp-agent/fs.cordis.snapshot.yml
+++ b/examples/acp-agent/fs.cordis.snapshot.yml
@@ -5,7 +5,7 @@
# `deepseek-v4-pro`, but the recorded corpus was captured on flash, and a config
# patch replaces the whole app config, so the base fields are restated verbatim.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/fs.cordis.yml b/examples/acp-agent/fs.cordis.yml
index 0d667255c8..c68e361302 100644
--- a/examples/acp-agent/fs.cordis.yml
+++ b/examples/acp-agent/fs.cordis.yml
@@ -2,7 +2,7 @@
# the base cordis.yml, so this overlay adds only the local tool-result spill
# storage those scenarios exercise.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/image-text-route.cordis.snapshot.yml b/examples/acp-agent/image-text-route.cordis.snapshot.yml
new file mode 100644
index 0000000000..bd1c0e01ea
--- /dev/null
+++ b/examples/acp-agent/image-text-route.cordis.snapshot.yml
@@ -0,0 +1,39 @@
+# Keyless replay for the read-image refusal scenario: identical to the
+# image.cordis.snapshot.yml overlay except the replay catalog leaves flash
+# text-only, so the strict read_image gate refuses and no image ever enters
+# the durable log.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+ - id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-flash
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: none
+ workspaceContext:
+ maxBytes: 65536
+ persona: |
+ You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+ Verify your work by running the code or tests. Keep answers brief and factual.
+ - insert:
+ - id: attachment-local
+ name: '@deepseek-ai/dsh-attachment-local'
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+ inputModalities: [text]
+ - id: deepseek-v4-pro
+ inputModalities: [text]
diff --git a/examples/acp-agent/image-text-route.cordis.yml b/examples/acp-agent/image-text-route.cordis.yml
new file mode 100644
index 0000000000..bbb5b9b4c0
--- /dev/null
+++ b/examples/acp-agent/image-text-route.cordis.yml
@@ -0,0 +1,27 @@
+# Text-route image overlay: the attachment store registers read_image, but the
+# strict execution gate refuses on a route that does not declare image input,
+# so a text-only deployment keeps its durable history text-clean. The app
+# config is restated to re-pin `deepseek-v4-flash` (base ships pro; the
+# authored fixture and the pinned header class are flash), because a config
+# patch replaces the whole app config.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-flash
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: none
+ workspaceContext:
+ maxBytes: 65536
+ persona: |
+ You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+ Verify your work by running the code or tests. Keep answers brief and factual.
+ - insert:
+ - id: attachment-local
+ name: '@deepseek-ai/dsh-attachment-local'
diff --git a/examples/acp-agent/image.cordis.snapshot.yml b/examples/acp-agent/image.cordis.snapshot.yml
new file mode 100644
index 0000000000..f056f775d3
--- /dev/null
+++ b/examples/acp-agent/image.cordis.snapshot.yml
@@ -0,0 +1,41 @@
+# Keyless replay for the read-image success scenario. Include patches cannot
+# target entries behind a nested include, so this restates the replay overlay
+# directly over the base cordis.yml (the fs.cordis.snapshot.yml pattern) and
+# re-pins the recorded flash model. The replay catalog declares image input on
+# flash, so the strict read_image gate accepts the route and the tool result
+# carries the durable image block; the live DeepSeek route cannot record this.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+ - id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-flash
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: none
+ workspaceContext:
+ maxBytes: 65536
+ persona: |
+ You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+ Verify your work by running the code or tests. Keep answers brief and factual.
+ - insert:
+ - id: attachment-local
+ name: '@deepseek-ai/dsh-attachment-local'
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+ inputModalities: [text, image]
+ - id: deepseek-v4-pro
+ inputModalities: [text]
diff --git a/examples/acp-agent/image.cordis.yml b/examples/acp-agent/image.cordis.yml
new file mode 100644
index 0000000000..a12cef1e85
--- /dev/null
+++ b/examples/acp-agent/image.cordis.yml
@@ -0,0 +1,27 @@
+# Image-scenario overlay: adds the durable attachment store the read_image tool
+# commits through. The store resolves its root from $DSH_HOME, which the
+# snapshot harness scopes per run, so the overlay itself carries no paths. The
+# app config is restated to re-pin `deepseek-v4-flash` (base ships pro; the
+# authored fixture and the pinned header class are flash), because a config
+# patch replaces the whole app config.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-flash
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: none
+ workspaceContext:
+ maxBytes: 65536
+ persona: |
+ You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+ Verify your work by running the code or tests. Keep answers brief and factual.
+ - insert:
+ - id: attachment-local
+ name: '@deepseek-ai/dsh-attachment-local'
diff --git a/examples/acp-agent/partial-landlock.cordis.snapshot.yml b/examples/acp-agent/partial-landlock.cordis.snapshot.yml
index af834b885d..ce48d20ac7 100644
--- a/examples/acp-agent/partial-landlock.cordis.snapshot.yml
+++ b/examples/acp-agent/partial-landlock.cordis.snapshot.yml
@@ -1,7 +1,7 @@
# Keyless runner-classification composition: replay authored model turns and
# replace the shipping provider with a deterministic process-launch stand-in.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/partial-landlock.cordis.yml b/examples/acp-agent/partial-landlock.cordis.yml
index 7a958bb6de..2272c657d1 100644
--- a/examples/acp-agent/partial-landlock.cordis.yml
+++ b/examples/acp-agent/partial-landlock.cordis.yml
@@ -1,7 +1,7 @@
# Live counterpart for the runner-classification snapshot overlay. It replaces
# only the sandbox provider; authored scenarios are skipped in record mode.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/product-subagent-both.cordis.snapshot.yml b/examples/acp-agent/product-subagent-both.cordis.snapshot.yml
new file mode 100644
index 0000000000..73d22eae27
--- /dev/null
+++ b/examples/acp-agent/product-subagent-both.cordis.snapshot.yml
@@ -0,0 +1,38 @@
+# Keyless twin of product-subagent-both.cordis.yml: preserve both product
+# tools while replacing only the external model adapter.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+ - insert:
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+ - id: deepseek-v4-pro
+ - id: subagent-codex
+ name: '@deepseek-ai/dsh-subagent-codex'
+ - id: subagent-claude-code
+ name: '@deepseek-ai/dsh-subagent-claude-code'
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
+ - id: tool-subagent-claude-code
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: claude-code
+ toolName: subagent_claude_code
+ enableRunInBackground: false
+ maxDepth: provider-managed
diff --git a/examples/acp-agent/product-subagent-both.cordis.yml b/examples/acp-agent/product-subagent-both.cordis.yml
new file mode 100644
index 0000000000..ce9d054058
--- /dev/null
+++ b/examples/acp-agent/product-subagent-both.cordis.yml
@@ -0,0 +1,27 @@
+# Add both native product providers and the same independent foreground tool
+# rows an Agent Preset may contribute. Loading the composition starts neither
+# product; the scenario pins both model-visible schemas.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - insert:
+ - id: subagent-codex
+ name: '@deepseek-ai/dsh-subagent-codex'
+ - id: subagent-claude-code
+ name: '@deepseek-ai/dsh-subagent-claude-code'
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
+ - id: tool-subagent-claude-code
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: claude-code
+ toolName: subagent_claude_code
+ enableRunInBackground: false
+ maxDepth: provider-managed
diff --git a/examples/acp-agent/product-subagent-codex.cordis.snapshot.yml b/examples/acp-agent/product-subagent-codex.cordis.snapshot.yml
new file mode 100644
index 0000000000..8b8cd8604c
--- /dev/null
+++ b/examples/acp-agent/product-subagent-codex.cordis.snapshot.yml
@@ -0,0 +1,29 @@
+# Keyless twin of product-subagent-codex.cordis.yml: keep the same product
+# provider/tool composition and replace only the external model adapter.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+ - insert:
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+ - id: deepseek-v4-pro
+ - id: subagent-codex
+ name: '@deepseek-ai/dsh-subagent-codex'
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
diff --git a/examples/acp-agent/product-subagent-codex.cordis.yml b/examples/acp-agent/product-subagent-codex.cordis.yml
new file mode 100644
index 0000000000..55f9985b7d
--- /dev/null
+++ b/examples/acp-agent/product-subagent-codex.cordis.yml
@@ -0,0 +1,18 @@
+# Add the native Codex product provider and its preset-shaped foreground tool to
+# the real ACP composition. The model is told not to call it; the scenario pins
+# the assembled request schema without starting Codex.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - insert:
+ - id: subagent-codex
+ name: '@deepseek-ai/dsh-subagent-codex'
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
diff --git a/examples/acp-agent/pty.cordis.snapshot.yml b/examples/acp-agent/pty.cordis.snapshot.yml
index c918f74c56..75e9f313b6 100644
--- a/examples/acp-agent/pty.cordis.snapshot.yml
+++ b/examples/acp-agent/pty.cordis.snapshot.yml
@@ -1,6 +1,6 @@
# Keyless replay counterpart to pty.cordis.yml.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/pty.cordis.yml b/examples/acp-agent/pty.cordis.yml
index 5e911b29d0..0ff9c1d4b5 100644
--- a/examples/acp-agent/pty.cordis.yml
+++ b/examples/acp-agent/pty.cordis.yml
@@ -1,7 +1,7 @@
# Opt-in persistent PTY composition for the PTY snapshot scenario. The base
# deployment already owns the shared sandbox provider and policy.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/retry.cordis.snapshot.yml b/examples/acp-agent/retry.cordis.snapshot.yml
index 72370f1a70..c69b08fe64 100644
--- a/examples/acp-agent/retry.cordis.snapshot.yml
+++ b/examples/acp-agent/retry.cordis.snapshot.yml
@@ -3,7 +3,7 @@
# 1 ms zero-jitter retry policy as the live sibling. The app patch still
# restates its whole config for raw JSONL persistence and the recorded model.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/retry.cordis.yml b/examples/acp-agent/retry.cordis.yml
index 087faa271d..2da66a3e44 100644
--- a/examples/acp-agent/retry.cordis.yml
+++ b/examples/acp-agent/retry.cordis.yml
@@ -6,7 +6,7 @@
# adapter fields around `retryPolicy`, while the app patch re-pins the recorded
# flash model and restates its base fields.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/session-query.cordis.snapshot.yml b/examples/acp-agent/session-query.cordis.snapshot.yml
index 1edadf8374..33029a9c7a 100644
--- a/examples/acp-agent/session-query.cordis.snapshot.yml
+++ b/examples/acp-agent/session-query.cordis.snapshot.yml
@@ -1,7 +1,7 @@
# Keyless counterpart to session-query.cordis.yml: the nested snapshot overlay
# supplies replay plus deterministic private spill storage and its byte limit.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./fs.cordis.snapshot.yml
patches:
diff --git a/examples/acp-agent/session-query.cordis.yml b/examples/acp-agent/session-query.cordis.yml
index e5e45025df..03ed085fa2 100644
--- a/examples/acp-agent/session-query.cordis.yml
+++ b/examples/acp-agent/session-query.cordis.yml
@@ -1,7 +1,7 @@
# Explicit session-query tool opt-in for the dedicated spill scenario. The
# nested filesystem overlay supplies private spill storage and its byte limit.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./fs.cordis.yml
patches:
diff --git a/examples/acp-agent/session-sandbox-root.cordis.snapshot.yml b/examples/acp-agent/session-sandbox-root.cordis.snapshot.yml
index 02b1d303d4..55627c17fa 100644
--- a/examples/acp-agent/session-sandbox-root.cordis.snapshot.yml
+++ b/examples/acp-agent/session-sandbox-root.cordis.snapshot.yml
@@ -3,7 +3,7 @@
# and the deliberately distinct sandbox fallback are applied together to the
# live tree.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/session-sandbox-root.cordis.yml b/examples/acp-agent/session-sandbox-root.cordis.yml
index f27732fd68..fd2d712882 100644
--- a/examples/acp-agent/session-sandbox-root.cordis.yml
+++ b/examples/acp-agent/session-sandbox-root.cordis.yml
@@ -3,7 +3,7 @@
# /tmp. A workspace-write mutation can therefore succeed only when the calling
# session's cwd replaces the process-level fallback root.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/session-title.cordis.snapshot.yml b/examples/acp-agent/session-title.cordis.snapshot.yml
index b10e82cfcc..4debc76a02 100644
--- a/examples/acp-agent/session-title.cordis.snapshot.yml
+++ b/examples/acp-agent/session-title.cordis.snapshot.yml
@@ -2,7 +2,7 @@
# the auxiliary route consumes replay.override.json with pacing so its accepted
# title commits only after the main turn has closed.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/session-title.cordis.yml b/examples/acp-agent/session-title.cordis.yml
index 0819b79d98..e18a6eeafd 100644
--- a/examples/acp-agent/session-title.cordis.yml
+++ b/examples/acp-agent/session-title.cordis.yml
@@ -2,7 +2,7 @@
# the ordinary DeepSeek route while the ACP app and every other capability stay
# identical to the base example.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/subagent-continuable-inheritance.cordis.snapshot.yml b/examples/acp-agent/subagent-continuable-inheritance.cordis.snapshot.yml
new file mode 100644
index 0000000000..43822afbc3
--- /dev/null
+++ b/examples/acp-agent/subagent-continuable-inheritance.cordis.snapshot.yml
@@ -0,0 +1,46 @@
+# Keyless counterpart to subagent-continuable-inheritance.cordis.yml: replace
+# the live adapter with replay and switch the root session to read-only at
+# creation.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+ - id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-flash
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: none
+ workspaceContext:
+ maxBytes: 65536
+ persona: |
+ You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+ Verify your work by running the code or tests. Keep answers brief and factual.
+ - id: sandbox
+ name: '@deepseek-ai/dsh-sandbox-local'
+ config:
+ runnerCommand:
+ - bash
+ - -c
+ - while [ "$1" != "--" ]; do shift; done; shift; exec "$@"
+ - passthrough-runner
+ runnerFailureSignatures:
+ - 'passthrough-runner: profile rejected'
+ - insert:
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+ - id: deepseek-v4-pro
+ - id: parent-sandbox-override
+ name: './tests/fixtures/parent-sandbox-override.ts'
diff --git a/examples/acp-agent/subagent-continuable-inheritance.cordis.yml b/examples/acp-agent/subagent-continuable-inheritance.cordis.yml
new file mode 100644
index 0000000000..227982e4bf
--- /dev/null
+++ b/examples/acp-agent/subagent-continuable-inheritance.cordis.yml
@@ -0,0 +1,11 @@
+# Policy-inheritance overlay: the root session is switched to read-only at
+# creation (the UI Access switch equivalent), so a continuable background
+# child must inherit that override instead of the deployment default.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - insert:
+ - id: parent-sandbox-override
+ name: './tests/fixtures/parent-sandbox-override.ts'
diff --git a/examples/acp-agent/subagent-durability-failure.cordis.snapshot.yml b/examples/acp-agent/subagent-durability-failure.cordis.snapshot.yml
index 7ce0733e53..2fc7c8a369 100644
--- a/examples/acp-agent/subagent-durability-failure.cordis.snapshot.yml
+++ b/examples/acp-agent/subagent-durability-failure.cordis.snapshot.yml
@@ -1,7 +1,7 @@
# Keyless counterpart to subagent-durability-failure.cordis.yml: replace the
# live adapter with replay and fail the provider-owned final child checkpoint.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/subagent-durability-failure.cordis.yml b/examples/acp-agent/subagent-durability-failure.cordis.yml
index c033c323dc..ff5603093e 100644
--- a/examples/acp-agent/subagent-durability-failure.cordis.yml
+++ b/examples/acp-agent/subagent-durability-failure.cordis.yml
@@ -1,7 +1,7 @@
# Snapshot-only durability-failure overlay. The child turn's ordinary flush
# succeeds; the provider-owned final confirmation fails deterministically.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/subagent-report-quiet.cordis.snapshot.yml b/examples/acp-agent/subagent-report-quiet.cordis.snapshot.yml
new file mode 100644
index 0000000000..4d28b399f0
--- /dev/null
+++ b/examples/acp-agent/subagent-report-quiet.cordis.snapshot.yml
@@ -0,0 +1,51 @@
+# Keyless counterpart to subagent-report-quiet.cordis.yml: replace the live
+# adapter with replay, keep report delivery quiet, and fence the child behind
+# the end of its parent's spawn turn so settlement opens the next turn.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+ - id: acp-agent
+ name: '@deepseek-ai/dsh-acp-demo'
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-flash
+ persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+ persistenceCompression: none
+ workspaceContext:
+ maxBytes: 65536
+ persona: |
+ You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+ Verify your work by running the code or tests. Keep answers brief and factual.
+ - id: sandbox
+ name: '@deepseek-ai/dsh-sandbox-local'
+ config:
+ runnerCommand:
+ - bash
+ - -c
+ - while [ "$1" != "--" ]; do shift; done; shift; exec "$@"
+ - passthrough-runner
+ runnerFailureSignatures:
+ - 'passthrough-runner: profile rejected'
+ - id: tool-subagent-report
+ name: '@deepseek-ai/dsh-tool-subagent-report'
+ config:
+ reportDelivery: quiet
+ - insert:
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+ - id: deepseek-v4-pro
+
+- id: report-fence
+ name: './tests/fixtures/subagent-report-fence.ts'
diff --git a/examples/acp-agent/subagent-report-quiet.cordis.yml b/examples/acp-agent/subagent-report-quiet.cordis.yml
new file mode 100644
index 0000000000..cb1c2b6d34
--- /dev/null
+++ b/examples/acp-agent/subagent-report-quiet.cordis.yml
@@ -0,0 +1,17 @@
+# Snapshot-only overlay pinning quiet report delivery. The shipped default wakes
+# the parent on every accepted report, and the runtime's settlement notice wakes
+# it again when the child's Activation ends; two independent wakes have no single
+# authored order. Quiet delivery leaves settlement as the only wake, while the
+# fixture below holds the child until the parent's spawn turn has closed.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: tool-subagent-report
+ name: '@deepseek-ai/dsh-tool-subagent-report'
+ config:
+ reportDelivery: quiet
+
+- id: report-fence
+ name: './tests/fixtures/subagent-report-fence.ts'
diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts
index 2a3de5d6b8..0cddc9a504 100644
--- a/examples/acp-agent/tests/acp.snapshot.ts
+++ b/examples/acp-agent/tests/acp.snapshot.ts
@@ -38,20 +38,30 @@ const WORKSPACE_CONTEXT_CONFIG = fileURLToPath(new URL('../workspace-context.cor
const ADVANCED_CONFIG = fileURLToPath(new URL('../advanced.cordis.yml', import.meta.url))
const FS_CONFIG = fileURLToPath(new URL('../fs.cordis.yml', import.meta.url))
const SESSION_QUERY_CONFIG = fileURLToPath(new URL('../session-query.cordis.yml', import.meta.url))
+const IMAGE_CONFIG = fileURLToPath(new URL('../image.cordis.yml', import.meta.url))
+const IMAGE_TEXT_ROUTE_CONFIG = fileURLToPath(new URL('../image-text-route.cordis.yml', import.meta.url))
const PTY_CONFIG = fileURLToPath(new URL('../pty.cordis.yml', import.meta.url))
const DEPTH_TWO_CONFIG = fileURLToPath(new URL('../depth-two.cordis.yml', import.meta.url))
const CHILD_QUESTION_CONFIG = fileURLToPath(new URL('../child-question.cordis.yml', import.meta.url))
const SESSION_SANDBOX_ROOT_CONFIG = fileURLToPath(new URL('../session-sandbox-root.cordis.yml', import.meta.url))
const RETRY_CONFIG = fileURLToPath(new URL('../retry.cordis.yml', import.meta.url))
const SESSION_TITLE_CONFIG = fileURLToPath(new URL('../session-title.cordis.yml', import.meta.url))
+const SUBAGENT_REPORT_QUIET_CONFIG = fileURLToPath(
+ new URL('../subagent-report-quiet.cordis.yml', import.meta.url),
+)
const SUBAGENT_DURABILITY_FAILURE_CONFIG = fileURLToPath(
new URL('../subagent-durability-failure.cordis.yml', import.meta.url),
)
+const SUBAGENT_CONTINUABLE_INHERITANCE_CONFIG = fileURLToPath(
+ new URL('../subagent-continuable-inheritance.cordis.yml', import.meta.url),
+)
const LSP_CONFIG = fileURLToPath(new URL('./lsp.cordis.yml', import.meta.url))
const WEB_CONFIG = fileURLToPath(new URL('../web.cordis.yml', import.meta.url))
const FS_SEARCH_CONFIG = fileURLToPath(new URL('./fs-search.cordis.yml', import.meta.url))
const PARTIAL_LANDLOCK_CONFIG = fileURLToPath(new URL('../partial-landlock.cordis.yml', import.meta.url))
const PWSH_CONFIG = fileURLToPath(new URL('./pwsh.cordis.yml', import.meta.url))
+const PRODUCT_SUBAGENT_CODEX_CONFIG = fileURLToPath(new URL('../product-subagent-codex.cordis.yml', import.meta.url))
+const PRODUCT_SUBAGENT_BOTH_CONFIG = fileURLToPath(new URL('../product-subagent-both.cordis.yml', import.meta.url))
const FS_DIFF_BOUND_CONFIG = fileURLToPath(new URL('./fs-diff-bound.cordis.yml', import.meta.url))
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
const PACKED_CHUNKS_SOURCE = 'hook-cc-pretool-deny'
@@ -123,6 +133,27 @@ const SCENARIOS: Scenario[] = [
// text-turn is the default header pin and owns the prompt and tool-schema
// sidecars reused by alternate classes with identical component sequences.
{ name: 'text-turn', hasModelTurn: true, recorded: true, pinsHeader: true },
+ // Product-subagent scenarios are authored schema-isolation fixtures: they
+ // reuse the stable text-turn transcript so only Loader-composed headers and
+ // tool sidecars vary. Model output and usage are not evidence here, so record
+ // mode must not replace them with live-API output.
+ {
+ name: 'product-subagent-codex',
+ hasModelTurn: true,
+ recorded: false,
+ pinsHeader: true,
+ headerClass: 'product-subagent-codex',
+ configPath: PRODUCT_SUBAGENT_CODEX_CONFIG,
+ },
+ {
+ name: 'product-subagent-both',
+ hasModelTurn: true,
+ recorded: false,
+ pinsHeader: true,
+ headerClass: 'product-subagent-both',
+ systemPromptSource: 'product-subagent-codex',
+ configPath: PRODUCT_SUBAGENT_BOTH_CONFIG,
+ },
{
name: 'session-title-after-turn',
hasModelTurn: true,
@@ -154,6 +185,30 @@ const SCENARIOS: Scenario[] = [
configPath: SESSION_QUERY_CONFIG,
posixOnly: true,
},
+ // Authored keyless replays through the assembled app: the replay catalog
+ // declares flash image-capable (success) or text-only (refusal), and the
+ // real read_image tool executes against the workspace fixture and the real
+ // attachment store. Both boot the same composed header (the tool registers
+ // with the attachment store, independent of route), so they share one class.
+ {
+ name: 'read-image',
+ hasModelTurn: true,
+ recorded: false,
+ pinsHeader: true,
+ headerClass: 'image',
+ // The overlay adds no prompt section (read_image carries no guidance), so
+ // the composed system prompt is byte-identical to the default class; only
+ // the tool-schema sidecar is class-specific.
+ systemPromptSource: 'text-turn',
+ configPath: IMAGE_CONFIG,
+ },
+ {
+ name: 'read-image-text-route',
+ hasModelTurn: true,
+ recorded: false,
+ headerClass: 'image',
+ configPath: IMAGE_TEXT_ROUTE_CONFIG,
+ },
{
name: 'pty-tools',
hasModelTurn: true,
@@ -315,7 +370,17 @@ const SCENARIOS: Scenario[] = [
// Windows bash process-tree kill is deferred with the Bash execution domain.
{ name: 'cancel-tool-calls', hasModelTurn: true, recorded: false, overridden: true, posixOnly: true },
{ name: 'subagent-spawn', hasModelTurn: true, recorded: true },
+ // Keyless authored scenario: the child ends at max-tokens with an empty
+ // usage-only assistant/message after earlier text and a tool call. The
+ // parent's tool result must retain that assistant output and stop reason.
+ { name: 'subagent-max-tokens-partial', hasModelTurn: true, recorded: false },
{ name: 'subagent-multi', hasModelTurn: true, recorded: true },
+ // Authored keyless replay: one assistant message carries two subagent calls
+ // and the parent log pins call/call/result/result instead of the serial
+ // interleaving. The twin delegations must stay identical: replay binds child
+ // scripts and harvest order nondeterministically across concurrent children
+ // (XXX(concurrent-subagents) in dsh-llm-replay).
+ { name: 'subagent-parallel', hasModelTurn: true, recorded: false },
{ name: 'subagent-fork', hasModelTurn: true, recorded: true },
{ name: 'subagent-mixed', hasModelTurn: true, recorded: true },
// Authored continuable-subagent transcript: a background delegation returns
@@ -323,14 +388,36 @@ const SCENARIOS: Scenario[] = [
// turns on that same child (the parent is never woken with their output),
// send_message to an unknown subagent id fails without delivering, and the
// child's retained handle is disposed child-first at teardown despite a
- // failed final durability confirmation.
+ // failed final durability confirmation. That failed confirmation is also what
+ // the settlement notice must report: the child's last turn claimed the third
+ // message and then died on its durability checkpoint without entering a step,
+ // so the notice opening the parent's second turn says the child FAILED and the
+ // parent must not read the earlier answer as final. The scenario's fixture
+ // fences the child behind the parent's spawn turn so that notice can only
+ // arrive at an idle parent.
{
name: 'subagent-continuable',
hasModelTurn: true,
recorded: false,
pinsChildToolSchemas: [1],
+ pinsChildSystemPrompts: [1],
configPath: SUBAGENT_DURABILITY_FAILURE_CONFIG,
},
+ // Authored policy-inheritance transcript: the root session is switched to
+ // read-only at creation (the UI Access switch equivalent), and the
+ // continuable background child's log carries that override as a
+ // `sandbox/mode` `source: 'delegation'` event, so the child's runtime
+ // context states the inherited policy instead of the deployment default.
+ // The input also waits for the manager-owned settlement turn, keeping that
+ // delivery from racing transcript harvest.
+ {
+ name: 'subagent-continuable-inheritance',
+ hasModelTurn: true,
+ recorded: false,
+ pinsChildToolSchemas: [1],
+ pinsChildSystemPrompts: [1],
+ configPath: SUBAGENT_CONTINUABLE_INHERITANCE_CONFIG,
+ },
// The in-process child is published before its first follow-up fails. The
// foreground tool retains both that run-result failure and an independent
// published-handle disposal failure.
@@ -343,13 +430,18 @@ const SCENARIOS: Scenario[] = [
configPath: SUBAGENT_DURABILITY_FAILURE_CONFIG,
},
// Authored child-to-parent transcript: the child calls its scope-local
- // `report`, quiet delivery reaches the idle parent without waking it, and a
- // later parent turn consumes the logged report.
+ // `report`, and the runtime's unconditional settlement notice then wakes the
+ // parked parent into one ordinary turn that claims both. The overlay pins
+ // quiet report delivery because two independent wakes have no orderable
+ // transcript; the shipped waking default is covered by package tests.
{
name: 'subagent-report',
hasModelTurn: true,
recorded: false,
+ overridden: false,
+ configPath: SUBAGENT_REPORT_QUIET_CONFIG,
pinsChildToolSchemas: [1],
+ pinsChildSystemPrompts: [1],
},
// Authored durable-catalog transcript: the snapshot-only lifecycle marker
// fences the second parent turn behind the child's Activation end, so
@@ -362,6 +454,7 @@ const SCENARIOS: Scenario[] = [
hasModelTurn: true,
recorded: false,
pinsChildToolSchemas: [1],
+ pinsChildSystemPrompts: [1],
},
{
name: 'subagent-depth-two-rejection',
diff --git a/examples/acp-agent/tests/fixtures/child-question-tripwire.ts b/examples/acp-agent/tests/fixtures/child-question-tripwire.ts
index 7eb15ac551..7495e68819 100644
--- a/examples/acp-agent/tests/fixtures/child-question-tripwire.ts
+++ b/examples/acp-agent/tests/fixtures/child-question-tripwire.ts
@@ -1,4 +1,4 @@
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import '@deepseek-ai/dsh-user-interaction'
/** Snapshot-only provider whose invocation means the child guard failed. */
diff --git a/examples/acp-agent/tests/fixtures/parent-sandbox-override.ts b/examples/acp-agent/tests/fixtures/parent-sandbox-override.ts
new file mode 100644
index 0000000000..262bd8a579
--- /dev/null
+++ b/examples/acp-agent/tests/fixtures/parent-sandbox-override.ts
@@ -0,0 +1,19 @@
+import type { Context } from '@deepseek-ai/cordis'
+import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
+import type {} from '@deepseek-ai/dsh-agent'
+
+export const name = 'parent-sandbox-override'
+
+/**
+ * Snapshot-only overlay: switch each ROOT session to `read-only` at creation —
+ * the UI "Access" switch equivalent (one runtime `sandbox/mode` event on the
+ * session log) — so the scenario proves a continuable background child
+ * inherits the parent's explicit override as a `source: 'delegation'` event
+ * instead of falling back to the deployment default.
+ */
+export function apply(ctx: Context): void {
+ ctx.on('agent/created', ({ agent }) => {
+ if (agent.session.header.parentSession !== undefined) return
+ setSandboxMode(agent.session, 'read-only')
+ })
+}
diff --git a/examples/acp-agent/tests/fixtures/subagent-durability-failure.ts b/examples/acp-agent/tests/fixtures/subagent-durability-failure.ts
index d3ffa4e8a7..9ebc0d6ac1 100644
--- a/examples/acp-agent/tests/fixtures/subagent-durability-failure.ts
+++ b/examples/acp-agent/tests/fixtures/subagent-durability-failure.ts
@@ -1,4 +1,4 @@
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { SessionId } from '@deepseek-ai/dsh-session'
export const name = 'subagent-durability-failure'
@@ -31,6 +31,8 @@ const FAILED_CHECKPOINT_TURN = 3
/** Fail the child checkpoint and stabilize the authored follow-up failure ordering. */
export function apply(ctx: Context): void {
const followupsAccepted = Promise.withResolvers()
+ const parentTurnClosed = Promise.withResolvers()
+ let parentClosed = false
const publishedFailure = process.env.DSH_SUBAGENT_PUBLISHED_FAILURE === '1'
const persistence = ctx.sessionPersistence
const load = persistence.load.bind(persistence)
@@ -62,8 +64,22 @@ export function apply(ctx: Context): void {
agents.create = create
persistence.load = load
followupsAccepted.resolve(undefined)
+ parentTurnClosed.resolve(undefined)
}, 'subagent snapshot ordering')
+ // The manager's settlement notice races whatever the parent is doing when the
+ // child's Activation ends, and this transcript pins it as the parent's own
+ // later turn. Hold the child's steps until the parent's spawn turn closes, so
+ // the notice can only arrive at an idle parent. The parent's turn never awaits
+ // child model work — its own fences need inbox acceptance only — so the child
+ // cannot deadlock it.
+ ctx.on('session/event', (session, event) => {
+ if (session.header.parentSession !== undefined || event.type !== 'turn/end') return
+ if (event.data.turn !== 1) return
+ parentClosed = true
+ parentTurnClosed.resolve(undefined)
+ })
+
// Remap the placeholder child id in a follow-up to the live child. The child
// id the model "knows" is authored into the transcript, while the running
// child is minted with a random id, so without this the follow-ups would
@@ -91,7 +107,12 @@ export function apply(ctx: Context): void {
if (accepted >= 3) followupsAccepted.resolve(undefined)
})
ctx.on('agent/pre-step', async ({ agent }, next) => {
- if (agent.session.header.parentSession !== undefined) await followupsAccepted.promise
+ if (agent.session.header.parentSession === undefined) return next()
+ await followupsAccepted.promise
+ // The published-failure variant's child never reaches a step (its follow-up
+ // throws), and its parent turn awaits that child, so only the continuable
+ // scenario takes the settlement fence.
+ if (!publishedFailure && !parentClosed) await parentTurnClosed.promise
return next()
})
diff --git a/examples/acp-agent/tests/fixtures/subagent-report-fence.ts b/examples/acp-agent/tests/fixtures/subagent-report-fence.ts
new file mode 100644
index 0000000000..754343e4ec
--- /dev/null
+++ b/examples/acp-agent/tests/fixtures/subagent-report-fence.ts
@@ -0,0 +1,41 @@
+/**
+ * Loader fixture that holds the report child until its parent's spawn turn ends.
+ * @module subagent-report-fence
+ */
+
+import type { Context } from '@deepseek-ai/cordis'
+import type {} from '@deepseek-ai/dsh-agent-loop'
+
+/** Fixture plugin name. */
+export const name = 'subagent-report-fence'
+
+/**
+ * Keep replay scheduling from folding settlement into the parent's first turn.
+ * @param ctx - assembled ACP-agent context.
+ */
+export function apply(ctx: Context): void {
+ const childReady = Promise.withResolvers()
+ const parentStopped = Promise.withResolvers()
+ let hasStopped = false
+
+ ctx.effect(() => {
+ const disposeSession = ctx.root.on('session/event', (session, event) => {
+ if (session.header.parentSession !== undefined || event.type !== 'turn/end' || event.data.turn !== 1) return
+ hasStopped = true
+ parentStopped.resolve(undefined)
+ })
+ const disposeStep = ctx.root.on('agent/pre-step', async ({ agent, turn, step }, next) => {
+ if (agent.session.header.parentSession !== undefined) {
+ childReady.resolve(undefined)
+ if (!hasStopped) await parentStopped.promise
+ } else if (turn === 1 && step === 2) {
+ await childReady.promise
+ }
+ return next()
+ })
+ return () => {
+ disposeStep()
+ disposeSession()
+ }
+ }, 'subagent-report-fence.listeners')
+}
diff --git a/examples/acp-agent/tests/fixtures/subagent-settlement-marker.ts b/examples/acp-agent/tests/fixtures/subagent-settlement-marker.ts
index e81bde95c4..8e4ad0a0e9 100644
--- a/examples/acp-agent/tests/fixtures/subagent-settlement-marker.ts
+++ b/examples/acp-agent/tests/fixtures/subagent-settlement-marker.ts
@@ -1,6 +1,6 @@
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-subagent'
export const name = 'subagent-settlement-marker'
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts
index 9d3857ffc8..f98c007125 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts
+++ b/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts
@@ -1,4 +1,4 @@
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts
index e2dc946dfe..781138d9ff 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts
+++ b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts
@@ -1,6 +1,6 @@
/** Parent adapter that fails if the composition-only Loader test starts a turn. */
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
diff --git a/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts b/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts
index 6c1cd46fbf..e1819e430f 100644
--- a/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts
+++ b/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts
@@ -1,4 +1,4 @@
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent'
import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compact'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
diff --git a/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml b/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml
index a216a34cfd..4cc6ba1f3e 100644
--- a/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml
+++ b/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml
@@ -3,7 +3,7 @@
# entries behind a nested include; the acp-agent restatement keeps the recorded
# deepseek-v4-flash model and raw JSONL persistence for the harness's harvest.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ../cordis.yml
patches:
diff --git a/examples/acp-agent/tests/fs-diff-bound.cordis.yml b/examples/acp-agent/tests/fs-diff-bound.cordis.yml
index 9be25cdb25..c82a7ce63a 100644
--- a/examples/acp-agent/tests/fs-diff-bound.cordis.yml
+++ b/examples/acp-agent/tests/fs-diff-bound.cordis.yml
@@ -5,7 +5,7 @@
# verbatim, and the acp-agent restatement re-pins `deepseek-v4-flash` to match
# the recorded corpus and its pinned request headers.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ../cordis.yml
patches:
diff --git a/examples/acp-agent/tests/lsp.cordis.snapshot.yml b/examples/acp-agent/tests/lsp.cordis.snapshot.yml
index dc672376b5..574d8f646f 100644
--- a/examples/acp-agent/tests/lsp.cordis.snapshot.yml
+++ b/examples/acp-agent/tests/lsp.cordis.snapshot.yml
@@ -1,6 +1,6 @@
# Keyless replay keeps the LSP composition intact and replaces only the model adapter.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ../cordis.yml
patches:
diff --git a/examples/acp-agent/tests/lsp.cordis.yml b/examples/acp-agent/tests/lsp.cordis.yml
index 49c9099d65..b9600d5d09 100644
--- a/examples/acp-agent/tests/lsp.cordis.yml
+++ b/examples/acp-agent/tests/lsp.cordis.yml
@@ -1,7 +1,7 @@
# Exercise the model-facing LSP tool through the shipped ACP app and Loader entry path.
# The scenario workspace supplies the deterministic stdio server used by this test composition.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ../cordis.yml
patches:
diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
index 7908f0e71b..0556c0fe80 100644
--- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
@@ -1,19 +1,20 @@
{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498801881,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"}]}}
-{"type":"turn/start","seq":1,"time":1785821418076,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821418076,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821418091,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
-{"type":"step/start","seq":4,"time":1785730458555,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730458555,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730458555,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"f9a2d1b6-8f23-43a5-8702-d413fed40990"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730458555,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730458555,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730458555,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":11,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}}
-{"type":"assistant/chunk","seq":12,"time":1785498801905,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}}
-{"type":"assistant/chunk","seq":13,"time":1785730458561,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","seq":14,"time":1785730458561,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":15,"time":1785730458561,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"b9c977ca-2c1a-4a5e-8397-e0b9381a9943"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
-{"type":"step/end","seq":16,"time":1785730458561,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":17,"time":1785730458561,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357538290,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357538290,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"}]}}
+{"type":"turn/start","seq":2,"time":1786357538290,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357538290,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357538308,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
+{"type":"step/start","seq":5,"time":1786357538310,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730458555,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357538310,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"1f81ee1b-b08e-4fba-a075-426e41dd03d4"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357538310,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730458555,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730458555,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":12,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}}
+{"type":"assistant/chunk","seq":13,"time":1785498801905,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}}
+{"type":"assistant/chunk","seq":14,"time":1785730458561,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":15,"time":1785730458561,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":16,"time":1785730458561,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"b9c977ca-2c1a-4a5e-8397-e0b9381a9943"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
+{"type":"step/end","seq":17,"time":1785730458561,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":18,"time":1785730458561,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
index adf877c24b..b0ef18eb2b 100644
--- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
+++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
@@ -1,19 +1,20 @@
{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498802039,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"}]}}
-{"type":"turn/start","seq":1,"time":1785821418251,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821418251,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821418270,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
-{"type":"step/start","seq":4,"time":1785730458703,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730458703,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730458703,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"dfbcd587-db47-4c3d-bbe9-8c031b215fc3"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730458703,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730458703,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730458703,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":11,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}}
-{"type":"assistant/chunk","seq":12,"time":1785498802068,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}}
-{"type":"assistant/chunk","seq":13,"time":1785730458709,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","seq":14,"time":1785730458709,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":15,"time":1785730458709,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5c33b525-4844-4272-b6f2-e036356d0e22"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
-{"type":"step/end","seq":16,"time":1785730458709,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":17,"time":1785730458709,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357538450,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357538450,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"}]}}
+{"type":"turn/start","seq":2,"time":1786357538450,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357538450,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357538469,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
+{"type":"step/start","seq":5,"time":1786357538470,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730458703,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357538471,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"dbf0fe20-f0e0-4700-b0fc-d192f5e7b1ca"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357538471,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730458703,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730458703,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":12,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}}
+{"type":"assistant/chunk","seq":13,"time":1785498802068,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}}
+{"type":"assistant/chunk","seq":14,"time":1785730458709,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":15,"time":1785730458709,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":16,"time":1785730458709,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5c33b525-4844-4272-b6f2-e036356d0e22"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
+{"type":"step/end","seq":17,"time":1785730458709,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":18,"time":1785730458709,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md
index 2014dc54e3..91ecf6c888 100644
--- a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md
+++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md
@@ -60,7 +60,7 @@ interface ToolArgsMap {
/** Exact service key or event name whose original JSDoc to include; valid only with what:"api" or what:"events". */
name?: string;
} & Record;
- /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */
+ /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */
cordis_mount: {
/** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */
code: string;
@@ -99,7 +99,7 @@ interface ToolArgsMap {
/** The agent id of the running agent to interrupt. */
agent_id: string;
} & Record;
- /** List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only. */
+ /** List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only. */
list_agents: {
/** children (default) lists direct children only; descendants walks the complete tree below you. */
scope?: "children" | "descendants";
@@ -132,23 +132,21 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record;
- /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */
+ /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
prompt: string;
- /** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */
+ /** Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message. */
run_in_background?: boolean;
} & Record;
- /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */
+ /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. */
subagent_fork: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */
prompt: string;
- /** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */
- run_in_background?: boolean;
} & Record;
/** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */
task_kill: {
@@ -319,7 +317,7 @@ interface ToolOutputMap {
kind: "child";
id: string;
label: string;
- status: "running" | "idle" | "complete";
+ status: "running" | "idle" | "ready";
parent?: string;
depth?: number;
} | {
diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json
index 403d3fbc6c..339fac4f73 100644
--- a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json
@@ -72,7 +72,7 @@
},
{
"name": "cordis_mount",
- "description": "Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.",
+ "description": "Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.",
"parameters": {
"type": "object",
"properties": {
@@ -190,7 +190,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -309,7 +309,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -323,7 +323,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -334,7 +334,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -345,10 +345,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json
index d5cbdc2ccc..597408965a 100644
--- a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -252,7 +252,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -266,7 +266,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -277,7 +277,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -288,10 +288,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md
index cb1b047e93..94e7ee4afb 100644
--- a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md
+++ b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md
@@ -82,7 +82,7 @@ interface ToolArgsMap {
/** The agent id of the running agent to interrupt. */
agent_id: string;
} & Record;
- /** List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only. */
+ /** List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only. */
list_agents: {
/** children (default) lists direct children only; descendants walks the complete tree below you. */
scope?: "children" | "descendants";
@@ -115,23 +115,21 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record;
- /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */
+ /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
prompt: string;
- /** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */
+ /** Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message. */
run_in_background?: boolean;
} & Record;
- /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */
+ /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. */
subagent_fork: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */
prompt: string;
- /** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */
- run_in_background?: boolean;
} & Record;
/** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */
task_kill: {
@@ -290,7 +288,7 @@ interface ToolOutputMap {
kind: "child";
id: string;
label: string;
- status: "running" | "idle" | "complete";
+ status: "running" | "idle" | "ready";
parent?: string;
depth?: number;
} | {
diff --git a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
index 6fa938c18e..2e6c552322 100644
--- a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
@@ -15,7 +15,7 @@
{"type":"assistant/chunk","seq":13,"time":1785730459883,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":14,"time":1785730459883,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"6b62bed7-113a-4d2e-a6aa-b935a1063ee2"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1785730459883,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}}
-{"type":"tool/result","seq":16,"time":1785730459904,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"inspect-tools-api"},"content":[{"type":"tool-result","toolCallId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Present this agent's tools in `mode` instead of the deployment default.\n *\n * Scoped only, and one declaration per agent: this is how an agent preset\n * composes a Code Mode agent beside native ones in the same process, and a\n * process-global override would be the `mode` config field instead.\n * @param mode - the presentation this agent's model sees.\n * @returns the exact disposer that restores the deployment default.\n */\n presentAs(mode: ToolPresentationMode): () => void\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly inbox: Inbox;\n readonly status: AgentStatus;\n readonly ctx: Context;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n runMaintenance(task: (signal: AbortSignal) => Promise): Promise;\n send(message: UserMessage, target: InboxTarget, wakeup: boolean): void;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n } | {\n readonly kind: 'hook';\n readonly reason: string;\n } | {\n readonly kind: 'disposed';\n };\n export interface AgentOptions {\n provider?: string;\n model?: string;\n maxTokens?: number;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AssistantMessage extends Message {\n readonly role: 'assistant';\n readonly source: ModelMessageSource;\n }\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type AttachmentId = Branded<'AttachmentId'>;\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean | undefined;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'image': ImageBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export type ContextFormed = {\n readonly form?: never;\n } | {\n readonly form: 'instructions';\n } | {\n readonly form: 'catalog';\n } | {\n readonly form: 'snapshot';\n readonly sections: readonly ContextSnapshotSection[];\n } | {\n readonly form: 'notice';\n readonly summary: string;\n } | {\n readonly form: 'relay';\n } | {\n readonly form: 'recall';\n };\n export interface ContextSnapshotSection {\n readonly name: string;\n readonly text: string;\n }\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n system?: string;\n tools?: ToolSchema[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export interface ImageAttachmentRef {\n attachmentId: AttachmentId;\n mediaType: ImageMediaType;\n bytes: number;\n width: number;\n height: number;\n name?: string;\n }\n export interface ImageBlock {\n type: 'image';\n attachment: ImageAttachmentRef;\n }\n export type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';\n export class Inbox {\n constructor(private readonly session: Session, private readonly notifications: InboxNotifications);\n get nextTurn(): readonly UserMessage[];\n get nextStep(): readonly UserMessage[];\n get hasPending(): boolean;\n clear(): void;\n claim(target: InboxTarget, turn: number): UserMessage[];\n append(target: InboxTarget, message: UserMessage): void;\n prepend(target: InboxTarget, message: UserMessage): void;\n replace(messageId: MessageId, newMessage: UserMessage): boolean;\n remove(messageId: MessageId): boolean;\n splice(target: InboxTarget, start: number, deleteCount: number, inserted: UserMessage[]): UserMessage[];\n }\n export interface InboxNotifications {\n inserted(message: UserMessage): void;\n discarded(message: UserMessage): void;\n claimed(message: UserMessage, turn: number): void;\n }\n export type InboxTarget = 'next-turn' | 'next-step';\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmCallConfigAdapterDefaults {\n reasoningEffort?: true;\n maxTokens?: true;\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n readonly id: MessageId;\n readonly role: 'system' | 'user' | 'assistant';\n readonly content: ContentBlock[];\n readonly source: MessageSource;\n }\n export type MessageId = Branded<'MessageId'>;\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n } & ContextFormed;\n model: ModelMessageSource;\n tool: ToolMessageSource;\n }\n export interface ModelMessageSource extends AssistantProvenance {\n kind: 'model';\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReadFileLine {\n number: number;\n text: string;\n }\n export interface ReadResultView {\n card: 'read';\n title?: string;\n path: string;\n offset: number;\n lines: ReadFileLine[];\n totalLines: number;\n lang?: string;\n content?: ContentBlock[];\n }\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type ReasoningEffortId = Branded<'ReasoningEffortId'>;\n export interface RequestContext {\n provider: string;\n model: string;\n contextWindow?: number;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SearchFileMatches {\n path: string;\n matches: SearchLineMatch[];\n }\n export interface SearchLineMatch {\n lineNumber: number;\n line: string;\n }\n export interface SearchMatchesResultView {\n card: 'search';\n shape: 'matches';\n title?: string;\n files: SearchFileMatches[];\n truncated: boolean;\n total: number;\n }\n export interface SearchPathsResultView {\n card: 'search';\n shape: 'paths';\n title?: string;\n paths: string[];\n truncated: boolean;\n total: number;\n }\n export type SearchResultView = SearchMatchesResultView | SearchPathsResultView;\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n readonly firstLiveSeq: number;\n static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;\n static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessage;\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n message: AssistantMessage;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n 'request/context': RequestContext;\n 'session/end-seed': Record;\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly origin?: 'subagent';\n readonly delegationDepth?: number;\n readonly agentPreset?: string;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly rootCallId: CallId;\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly rootCallId?: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolMessageSource {\n kind: 'tool';\n callId: CallId;\n }\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export type ToolPresentationMode = 'native' | 'code' | 'both';\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export interface ToolResultMessage extends Message {\n readonly role: 'user';\n readonly content: [\n ToolResultBlock\n ];\n readonly source: ToolMessageSource;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | SearchResultView | ReadResultView | WebResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessage): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndCancelCause = AgentCancelCause | {\n readonly kind: 'legacy';\n };\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n reason: TurnEndCancelCause;\n };\n blocked: {\n kind: 'blocked';\n };\n error: {\n kind: 'error';\n error: LlmFailure;\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export interface UserMessage extends Message {\n readonly role: 'user';\n }\n export interface WebFetchResultView {\n card: 'web';\n kind: 'fetch';\n title?: string;\n url: string;\n statusCode: number;\n truncated: boolean;\n }\n export type WebResultView = WebSearchResultView | WebFetchResultView;\n export interface WebSearchResultView {\n card: 'web';\n kind: 'search';\n title?: string;\n sources: WebSource[];\n answer?: string;\n truncated: boolean;\n }\n export interface WebSource {\n url: string;\n title?: string;\n snippet?: string;\n publishedAt?: string;\n }"}],"isError":false}],"role":"user","id":"d422878c-c566-461b-9b6b-a61d241022ea"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
+{"type":"tool/result","seq":16,"time":1785730459904,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"inspect-tools-api"},"content":[{"type":"tool-result","toolCallId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Present the calling scope's tools in `mode` instead of the deployment\n * default. Nearest scope on the chain wins, so a preset's standing\n * declaration covers every agent joined under it.\n *\n * Scoped only, and one declaration per scope: this is how an agent preset\n * composes Code Mode agents beside native ones in the same process, and a\n * process-global override would be the `mode` config field instead.\n * @param mode - the presentation the covered agents' models see.\n * @returns the exact disposer that restores the deployment default.\n */\n presentAs(mode: ToolPresentationMode): () => void\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-tool mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly inbox: Inbox;\n readonly status: AgentStatus;\n readonly ctx: Context;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n runMaintenance(task: (signal: AbortSignal) => Promise): Promise;\n send(message: UserMessage, target: InboxTarget, wakeup: boolean): void;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n } | {\n readonly kind: 'hook';\n readonly reason: string;\n } | {\n readonly kind: 'disposed';\n };\n export interface AgentOptions {\n provider?: string;\n model?: string;\n maxTokens?: number;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AssistantMessage extends Message {\n readonly role: 'assistant';\n readonly source: ModelMessageSource;\n }\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type AttachmentId = Branded<'AttachmentId'>;\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean | undefined;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'image': ImageBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export type ContextFormed = {\n readonly form?: never;\n } | {\n readonly form: 'instructions';\n } | {\n readonly form: 'catalog';\n } | {\n readonly form: 'snapshot';\n readonly sections: readonly ContextSnapshotSection[];\n } | {\n readonly form: 'notice';\n readonly summary: string;\n } | {\n readonly form: 'relay';\n } | {\n readonly form: 'recall';\n };\n export interface ContextSnapshotSection {\n readonly name: string;\n readonly text: string;\n }\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n system?: string;\n tools?: ToolSchema[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export interface ImageAttachmentRef {\n attachmentId: AttachmentId;\n mediaType: ImageMediaType;\n bytes: number;\n width: number;\n height: number;\n name?: string;\n }\n export interface ImageBlock {\n type: 'image';\n attachment: ImageAttachmentRef;\n }\n export type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';\n export class Inbox {\n constructor(private readonly session: Session, private readonly notifications: InboxNotifications);\n get nextTurn(): readonly UserMessage[];\n get nextStep(): readonly UserMessage[];\n get hasPending(): boolean;\n clear(): void;\n claim(target: InboxTarget, turn: number): UserMessage[];\n append(target: InboxTarget, message: UserMessage): void;\n prepend(target: InboxTarget, message: UserMessage): void;\n replace(messageId: MessageId, newMessage: UserMessage): boolean;\n remove(messageId: MessageId): boolean;\n splice(target: InboxTarget, start: number, deleteCount: number, inserted: UserMessage[]): UserMessage[];\n }\n export interface InboxNotifications {\n inserted(message: UserMessage): void;\n discarded(message: UserMessage): void;\n claimed(message: UserMessage, turn: number): void;\n }\n export type InboxTarget = 'next-turn' | 'next-step';\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmCallConfigAdapterDefaults {\n reasoningEffort?: true;\n maxTokens?: true;\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n readonly id: MessageId;\n readonly role: 'system' | 'user' | 'assistant';\n readonly content: ContentBlock[];\n readonly source: MessageSource;\n }\n export type MessageId = Branded<'MessageId'>;\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n } & ContextFormed;\n model: ModelMessageSource;\n tool: ToolMessageSource;\n }\n export interface ModelMessageSource extends AssistantProvenance {\n kind: 'model';\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReadFileLine {\n number: number;\n text: string;\n }\n export interface ReadResultView {\n card: 'read';\n title?: string;\n path: string;\n offset: number;\n lines: ReadFileLine[];\n totalLines: number;\n lang?: string;\n content?: ContentBlock[];\n }\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type ReasoningEffortId = Branded<'ReasoningEffortId'>;\n export interface RequestContext {\n provider: string;\n model: string;\n contextWindow?: number;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SearchFileMatches {\n path: string;\n matches: SearchLineMatch[];\n }\n export interface SearchLineMatch {\n lineNumber: number;\n line: string;\n }\n export interface SearchMatchesResultView {\n card: 'search';\n shape: 'matches';\n title?: string;\n files: SearchFileMatches[];\n truncated: boolean;\n total: number;\n }\n export interface SearchPathsResultView {\n card: 'search';\n shape: 'paths';\n title?: string;\n paths: string[];\n truncated: boolean;\n total: number;\n }\n export type SearchResultView = SearchMatchesResultView | SearchPathsResultView;\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n readonly firstLiveSeq: number;\n static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;\n static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n ignorable?: true;\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessage;\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n message: AssistantMessage;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n 'request/context': RequestContext;\n 'session/end-seed': Record;\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly origin?: 'subagent';\n readonly delegationDepth?: number;\n readonly agentPreset?: string;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly rootCallId: CallId;\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly rootCallId?: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolMessageSource {\n kind: 'tool';\n callId: CallId;\n }\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export type ToolPresentationMode = 'native' | 'code' | 'both';\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export interface ToolResultMessage extends Message {\n readonly role: 'user';\n readonly content: [\n ToolResultBlock\n ];\n readonly source: ToolMessageSource;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | SearchResultView | ReadResultView | WebResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessage): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndCancelCause = AgentCancelCause | {\n readonly kind: 'legacy';\n };\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n reason: TurnEndCancelCause;\n };\n blocked: {\n kind: 'blocked';\n };\n error: {\n kind: 'error';\n error: LlmFailure;\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export interface UserMessage extends Message {\n readonly role: 'user';\n }\n export interface WebFetchResultView {\n card: 'web';\n kind: 'fetch';\n title?: string;\n url: string;\n statusCode: number;\n truncated: boolean;\n }\n export type WebResultView = WebSearchResultView | WebFetchResultView;\n export interface WebSearchResultView {\n card: 'web';\n kind: 'search';\n title?: string;\n sources: WebSource[];\n answer?: string;\n truncated: boolean;\n }\n export interface WebSource {\n url: string;\n title?: string;\n snippet?: string;\n publishedAt?: string;\n }"}],"isError":false}],"role":"user","id":"eb20999e-deb2-4abe-8517-14de8a6ca238"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1785730459904,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1785730459916,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1784449176734,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json
index 335be3672b..d1cf4a4ef6 100644
--- a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -268,7 +268,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -282,7 +282,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -293,7 +293,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -304,10 +304,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-both/input.json b/examples/acp-agent/tests/snapshots/product-subagent-both/input.json
new file mode 100644
index 0000000000..5fe0259a4e
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-both/input.json
@@ -0,0 +1,7 @@
+{
+ "steps": [
+ { "op": "initialize" },
+ { "op": "newSession" },
+ { "op": "prompt", "text": "Reply with exactly the word: PONG. Do not use any tools." }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-both/session.jsonl b/examples/acp-agent/tests/snapshots/product-subagent-both/session.jsonl
new file mode 100644
index 0000000000..84ac27e70e
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-both/session.jsonl
@@ -0,0 +1,22 @@
+{"type":"session","version":0,"id":"539aa64c-7f37-40ff-abd8-ed45b717be1b","createdAt":1783600629539,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"agent/inbox/spliced","seq":0,"time":1785498761270,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3e25dc34-48e0-4738-8401-1a8d181d37e5"}]}}
+{"type":"turn/start","seq":1,"time":1785821359466,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":2,"time":1785821359466,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":3,"time":1783600629542,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1785498761313,"data":{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3e25dc34-48e0-4738-8401-1a8d181d37e5"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730415287,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4b8d9730-0b7b-4e14-8a30-3d852f808f0e"},"surfaceOp":"append"}
+{"type":"session/title","seq":6,"time":1785730415287,"data":{"title":"Reply with exactly the word:","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":7,"time":1785498761318,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":8,"time":1785730415288,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}}
+{"type":"assistant/chunk","seq":9,"time":1783600630822,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":10,"time0":1783600630852,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,33,1,40,0,0,0,0,0,18,0,36,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","P","ONG","\""," and"," not"," use"," any"," tools","."]}}
+{"type":"assistant/chunk","seq":30,"time":1783600631006,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":31,"time":1783600631008,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"P"}}}
+{"type":"assistant/chunk","seq":32,"time":1783600631009,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ONG"}}}
+{"type":"assistant/chunk","seq":33,"time":1783600631009,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"PONG\" and not use any tools."}}}}
+{"type":"assistant/chunk","seq":34,"time":1783600631009,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PONG"}}}}
+{"type":"assistant/chunk","seq":35,"time":1785498761338,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3091,"outputTokens":23,"cacheReadTokens":0,"reasoningTokens":20}}}}
+{"type":"assistant/chunk","seq":36,"time":1785730415297,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":37,"time":1785730415298,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"PONG\" and not use any tools."},{"type":"text","text":"PONG"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"f1418376-f303-4017-acd7-92899c841c8a"},"usage":{"inputTokens":3091,"outputTokens":23,"cacheReadTokens":0,"reasoningTokens":20}},"sourceEventSeqs":[9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"surfaceOp":"append"}
+{"type":"step/end","seq":38,"time":1785730415298,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":39,"time":1785730415298,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-both/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/product-subagent-both/stdout.expected.jsonl
new file mode 100644
index 0000000000..acfccdd778
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-both/stdout.expected.jsonl
@@ -0,0 +1,4 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"PONG"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json
new file mode 100644
index 0000000000..6274c67e7f
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json
@@ -0,0 +1,565 @@
+{
+ "initial": [
+ {
+ "name": "bash",
+ "description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "The bash command to execute."
+ },
+ "description": {
+ "type": "string",
+ "description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
+ },
+ "timeoutMs": {
+ "type": "number",
+ "description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
+ },
+ "workdir": {
+ "type": "string",
+ "description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
+ }
+ },
+ "required": [
+ "command",
+ "description"
+ ]
+ }
+ },
+ {
+ "name": "create_goal",
+ "description": "Create one persisted same-session completion goal when the current direct human request is a long-running objective that should continue across autonomous goal rounds. You may infer that intent without requiring the user to say \"create a goal\". Do not use this for trivial single-turn work. Execution rejects non-human and subagent authority.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The concrete completion objective inferred from the direct human request."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer limit on automatic continuation rounds."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "edit",
+ "description": "Edit an existing UTF-8 text file by replacing literal text.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to edit, resolved by the filesystem backend."
+ },
+ "old_string": {
+ "type": "string",
+ "description": "Literal text to replace. Must match exactly."
+ },
+ "new_string": {
+ "type": "string",
+ "description": "Literal replacement text. Use an empty string to delete the match."
+ },
+ "replace_all": {
+ "type": "boolean",
+ "description": "Replace all matches. Defaults to false; when false, old_string must appear exactly once."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "old_string",
+ "new_string"
+ ]
+ }
+ },
+ {
+ "name": "get_goal",
+ "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "interrupt_agent",
+ "description": "Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "agent_id": {
+ "type": "string",
+ "description": "The agent id of the running agent to interrupt."
+ }
+ },
+ "required": [
+ "agent_id"
+ ]
+ }
+ },
+ {
+ "name": "list_agents",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "scope": {
+ "type": "string",
+ "description": "children (default) lists direct children only; descendants walks the complete tree below you.",
+ "enum": [
+ "children",
+ "descendants"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "name": "ralph",
+ "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The immutable completion objective for every fresh Ralph round."
+ },
+ "maxRounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer round cap, bounded by the deployment ceiling."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "read",
+ "description": "Read a UTF-8 text file and return line-numbered content.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to read, resolved by the filesystem backend."
+ },
+ "offset": {
+ "type": "number",
+ "description": "1-based first line to return. Defaults to 1."
+ },
+ "limit": {
+ "type": "number",
+ "description": "Maximum number of lines to return. Defaults to 2000."
+ }
+ },
+ "required": [
+ "file_path"
+ ]
+ }
+ },
+ {
+ "name": "send_message",
+ "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "subagent_id": {
+ "type": "string",
+ "description": "The subagent id returned when the background subagent was started."
+ },
+ "message": {
+ "type": "string",
+ "description": "The message to deliver to the subagent."
+ }
+ },
+ "required": [
+ "subagent_id",
+ "message"
+ ]
+ }
+ },
+ {
+ "name": "skill",
+ "description": "Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The exact skill name from the available skills list."
+ }
+ },
+ "required": [
+ "name"
+ ]
+ }
+ },
+ {
+ "name": "subagent",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "subagent_claude_code",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "subagent_codex",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "subagent_fork",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "task_kill",
+ "description": "Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "reason": {
+ "type": "string",
+ "description": "Optional short reason, recorded in the log and forwarded to the task."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "task_list",
+ "description": "List your background tasks (running and finished) with their ids, kinds, and statuses.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "task_output",
+ "description": "Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "wait": {
+ "type": "boolean",
+ "description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."
+ },
+ "timeout_ms": {
+ "type": "number",
+ "description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "todo_write",
+ "description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "todos": {
+ "type": "array",
+ "description": "The COMPLETE task list, replacing any previous list.",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "What the task is — a short imperative line."
+ },
+ "status": {
+ "type": "string",
+ "description": "pending (not started) | in_progress (now) | completed (done).",
+ "enum": [
+ "pending",
+ "in_progress",
+ "completed"
+ ]
+ }
+ },
+ "required": [
+ "content",
+ "status"
+ ]
+ }
+ }
+ },
+ "required": [
+ "todos"
+ ]
+ }
+ },
+ {
+ "name": "update_goal",
+ "description": "Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "goal_id": {
+ "type": "string",
+ "description": "Exact id returned by get_goal."
+ },
+ "revision": {
+ "type": "number",
+ "description": "Exact positive revision returned by get_goal."
+ },
+ "action": {
+ "type": "string",
+ "description": "edit | pause | resume | complete | blocked",
+ "enum": [
+ "edit",
+ "pause",
+ "resume",
+ "complete",
+ "blocked"
+ ]
+ },
+ "objective": {
+ "type": "string",
+ "description": "Replacement objective; valid only with action edit."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Replacement cap; valid only with action edit."
+ },
+ "blocked_reason": {
+ "type": "string",
+ "description": "Concrete blocking condition; required only with action blocked."
+ }
+ },
+ "required": [
+ "goal_id",
+ "revision",
+ "action"
+ ]
+ }
+ },
+ {
+ "name": "workflow",
+ "description": "Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."
+ },
+ "meta": {
+ "type": "object",
+ "description": "The workflow identity block (plain JSON — never code).",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Short kebab-case workflow name."
+ },
+ "description": {
+ "type": "string",
+ "description": "One-line description of what the workflow does."
+ },
+ "whenToUse": {
+ "type": "string",
+ "description": "Optional guidance on when this workflow applies."
+ },
+ "phases": {
+ "type": "array",
+ "description": "Optional phase declarations matched by phase() calls.",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "The phase title phase() calls match by exact string."
+ },
+ "detail": {
+ "type": "string",
+ "description": "Optional one-line description of the phase."
+ },
+ "provider": {
+ "type": "string",
+ "description": "Optional provider override this phase is expected to use."
+ },
+ "model": {
+ "type": "string",
+ "description": "Optional model override this phase is expected to use."
+ }
+ },
+ "required": [
+ "title"
+ ]
+ }
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ]
+ },
+ "args": {
+ "type": "object",
+ "description": "Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).",
+ "additionalProperties": true
+ }
+ },
+ "required": [
+ "script",
+ "meta"
+ ]
+ }
+ },
+ {
+ "name": "write",
+ "description": "Create or fully replace a UTF-8 text file.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to write, resolved by the filesystem backend."
+ },
+ "content": {
+ "type": "string",
+ "description": "Full UTF-8 text content to write."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "content"
+ ]
+ }
+ }
+ ],
+ "changes": []
+}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-codex/input.json b/examples/acp-agent/tests/snapshots/product-subagent-codex/input.json
new file mode 100644
index 0000000000..5fe0259a4e
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-codex/input.json
@@ -0,0 +1,7 @@
+{
+ "steps": [
+ { "op": "initialize" },
+ { "op": "newSession" },
+ { "op": "prompt", "text": "Reply with exactly the word: PONG. Do not use any tools." }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-codex/session.jsonl b/examples/acp-agent/tests/snapshots/product-subagent-codex/session.jsonl
new file mode 100644
index 0000000000..bd47dfa24f
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-codex/session.jsonl
@@ -0,0 +1,22 @@
+{"type":"session","version":0,"id":"539aa64c-7f37-40ff-abd8-ed45b717be1b","createdAt":1783600629539,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"agent/inbox/spliced","seq":0,"time":1785498761270,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3e25dc34-48e0-4738-8401-1a8d181d37e5"}]}}
+{"type":"turn/start","seq":1,"time":1785821359466,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":2,"time":1785821359466,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":3,"time":1783600629542,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1785498761313,"data":{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3e25dc34-48e0-4738-8401-1a8d181d37e5"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730415287,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4b8d9730-0b7b-4e14-8a30-3d852f808f0e"},"surfaceOp":"append"}
+{"type":"session/title","seq":6,"time":1785730415287,"data":{"title":"Reply with exactly the word:","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":7,"time":1785498761318,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":8,"time":1785730415288,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}}
+{"type":"assistant/chunk","seq":9,"time":1783600630822,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":10,"time0":1783600630852,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,33,1,40,0,0,0,0,0,18,0,36,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","P","ONG","\""," and"," not"," use"," any"," tools","."]}}
+{"type":"assistant/chunk","seq":30,"time":1783600631006,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":31,"time":1783600631008,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"P"}}}
+{"type":"assistant/chunk","seq":32,"time":1783600631009,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ONG"}}}
+{"type":"assistant/chunk","seq":33,"time":1783600631009,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"PONG\" and not use any tools."}}}}
+{"type":"assistant/chunk","seq":34,"time":1783600631009,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PONG"}}}}
+{"type":"assistant/chunk","seq":35,"time":1785498761338,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3091,"outputTokens":23,"cacheReadTokens":0,"reasoningTokens":20}}}}
+{"type":"assistant/chunk","seq":36,"time":1785730415297,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":37,"time":1785730415298,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"PONG\" and not use any tools."},{"type":"text","text":"PONG"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"c883cf16-01fe-4afc-b37c-d255bb450d21"},"usage":{"inputTokens":3091,"outputTokens":23,"cacheReadTokens":0,"reasoningTokens":20}},"sourceEventSeqs":[9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"surfaceOp":"append"}
+{"type":"step/end","seq":38,"time":1785730415298,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":39,"time":1785730415298,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-codex/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/product-subagent-codex/stdout.expected.jsonl
new file mode 100644
index 0000000000..acfccdd778
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-codex/stdout.expected.jsonl
@@ -0,0 +1,4 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"PONG"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-codex/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/product-subagent-codex/system-prompt.expected.md
new file mode 100644
index 0000000000..a6ffe7d4d7
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-codex/system-prompt.expected.md
@@ -0,0 +1,22 @@
+You are an AI agent powered by the DeepSeek Harness SDK.
+
+You are a coding assistant powered by the deepseek-v4-pro model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+Verify your work by running the code or tests. Keep answers brief and factual.
+
+
+Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.
+
+Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.
+
+Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.
+
+Check the [exit code: N] marker on every bash result; investigate failures before moving on.
+
+Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
+
+Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
+
+Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.
+
+Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json
new file mode 100644
index 0000000000..e7e83d948c
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json
@@ -0,0 +1,544 @@
+{
+ "initial": [
+ {
+ "name": "bash",
+ "description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "The bash command to execute."
+ },
+ "description": {
+ "type": "string",
+ "description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
+ },
+ "timeoutMs": {
+ "type": "number",
+ "description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
+ },
+ "workdir": {
+ "type": "string",
+ "description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
+ }
+ },
+ "required": [
+ "command",
+ "description"
+ ]
+ }
+ },
+ {
+ "name": "create_goal",
+ "description": "Create one persisted same-session completion goal when the current direct human request is a long-running objective that should continue across autonomous goal rounds. You may infer that intent without requiring the user to say \"create a goal\". Do not use this for trivial single-turn work. Execution rejects non-human and subagent authority.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The concrete completion objective inferred from the direct human request."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer limit on automatic continuation rounds."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "edit",
+ "description": "Edit an existing UTF-8 text file by replacing literal text.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to edit, resolved by the filesystem backend."
+ },
+ "old_string": {
+ "type": "string",
+ "description": "Literal text to replace. Must match exactly."
+ },
+ "new_string": {
+ "type": "string",
+ "description": "Literal replacement text. Use an empty string to delete the match."
+ },
+ "replace_all": {
+ "type": "boolean",
+ "description": "Replace all matches. Defaults to false; when false, old_string must appear exactly once."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "old_string",
+ "new_string"
+ ]
+ }
+ },
+ {
+ "name": "get_goal",
+ "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "interrupt_agent",
+ "description": "Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "agent_id": {
+ "type": "string",
+ "description": "The agent id of the running agent to interrupt."
+ }
+ },
+ "required": [
+ "agent_id"
+ ]
+ }
+ },
+ {
+ "name": "list_agents",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "scope": {
+ "type": "string",
+ "description": "children (default) lists direct children only; descendants walks the complete tree below you.",
+ "enum": [
+ "children",
+ "descendants"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "name": "ralph",
+ "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The immutable completion objective for every fresh Ralph round."
+ },
+ "maxRounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer round cap, bounded by the deployment ceiling."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "read",
+ "description": "Read a UTF-8 text file and return line-numbered content.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to read, resolved by the filesystem backend."
+ },
+ "offset": {
+ "type": "number",
+ "description": "1-based first line to return. Defaults to 1."
+ },
+ "limit": {
+ "type": "number",
+ "description": "Maximum number of lines to return. Defaults to 2000."
+ }
+ },
+ "required": [
+ "file_path"
+ ]
+ }
+ },
+ {
+ "name": "send_message",
+ "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "subagent_id": {
+ "type": "string",
+ "description": "The subagent id returned when the background subagent was started."
+ },
+ "message": {
+ "type": "string",
+ "description": "The message to deliver to the subagent."
+ }
+ },
+ "required": [
+ "subagent_id",
+ "message"
+ ]
+ }
+ },
+ {
+ "name": "skill",
+ "description": "Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The exact skill name from the available skills list."
+ }
+ },
+ "required": [
+ "name"
+ ]
+ }
+ },
+ {
+ "name": "subagent",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "subagent_codex",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "subagent_fork",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "task_kill",
+ "description": "Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "reason": {
+ "type": "string",
+ "description": "Optional short reason, recorded in the log and forwarded to the task."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "task_list",
+ "description": "List your background tasks (running and finished) with their ids, kinds, and statuses.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "task_output",
+ "description": "Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "wait": {
+ "type": "boolean",
+ "description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."
+ },
+ "timeout_ms": {
+ "type": "number",
+ "description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "todo_write",
+ "description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "todos": {
+ "type": "array",
+ "description": "The COMPLETE task list, replacing any previous list.",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "What the task is — a short imperative line."
+ },
+ "status": {
+ "type": "string",
+ "description": "pending (not started) | in_progress (now) | completed (done).",
+ "enum": [
+ "pending",
+ "in_progress",
+ "completed"
+ ]
+ }
+ },
+ "required": [
+ "content",
+ "status"
+ ]
+ }
+ }
+ },
+ "required": [
+ "todos"
+ ]
+ }
+ },
+ {
+ "name": "update_goal",
+ "description": "Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "goal_id": {
+ "type": "string",
+ "description": "Exact id returned by get_goal."
+ },
+ "revision": {
+ "type": "number",
+ "description": "Exact positive revision returned by get_goal."
+ },
+ "action": {
+ "type": "string",
+ "description": "edit | pause | resume | complete | blocked",
+ "enum": [
+ "edit",
+ "pause",
+ "resume",
+ "complete",
+ "blocked"
+ ]
+ },
+ "objective": {
+ "type": "string",
+ "description": "Replacement objective; valid only with action edit."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Replacement cap; valid only with action edit."
+ },
+ "blocked_reason": {
+ "type": "string",
+ "description": "Concrete blocking condition; required only with action blocked."
+ }
+ },
+ "required": [
+ "goal_id",
+ "revision",
+ "action"
+ ]
+ }
+ },
+ {
+ "name": "workflow",
+ "description": "Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."
+ },
+ "meta": {
+ "type": "object",
+ "description": "The workflow identity block (plain JSON — never code).",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Short kebab-case workflow name."
+ },
+ "description": {
+ "type": "string",
+ "description": "One-line description of what the workflow does."
+ },
+ "whenToUse": {
+ "type": "string",
+ "description": "Optional guidance on when this workflow applies."
+ },
+ "phases": {
+ "type": "array",
+ "description": "Optional phase declarations matched by phase() calls.",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "The phase title phase() calls match by exact string."
+ },
+ "detail": {
+ "type": "string",
+ "description": "Optional one-line description of the phase."
+ },
+ "provider": {
+ "type": "string",
+ "description": "Optional provider override this phase is expected to use."
+ },
+ "model": {
+ "type": "string",
+ "description": "Optional model override this phase is expected to use."
+ }
+ },
+ "required": [
+ "title"
+ ]
+ }
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ]
+ },
+ "args": {
+ "type": "object",
+ "description": "Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).",
+ "additionalProperties": true
+ }
+ },
+ "required": [
+ "script",
+ "meta"
+ ]
+ }
+ },
+ {
+ "name": "write",
+ "description": "Create or fully replace a UTF-8 text file.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to write, resolved by the filesystem backend."
+ },
+ "content": {
+ "type": "string",
+ "description": "Full UTF-8 text content to write."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "content"
+ ]
+ }
+ }
+ ],
+ "changes": []
+}
diff --git a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json
index 37d7d467be..272fe439d3 100644
--- a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -256,7 +256,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -267,10 +267,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/read-image-text-route/input.json b/examples/acp-agent/tests/snapshots/read-image-text-route/input.json
new file mode 100644
index 0000000000..551bfee00a
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/read-image-text-route/input.json
@@ -0,0 +1,14 @@
+{
+ "steps": [
+ {
+ "op": "initialize"
+ },
+ {
+ "op": "newSession"
+ },
+ {
+ "op": "prompt",
+ "text": "Use read_image on red.png in the current directory. If the tool refuses because the current model is text-only, reply with exactly the single word UNAVAILABLE."
+ }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/read-image-text-route/session.jsonl b/examples/acp-agent/tests/snapshots/read-image-text-route/session.jsonl
new file mode 100644
index 0000000000..3e32a50a32
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/read-image-text-route/session.jsonl
@@ -0,0 +1,26 @@
+{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783951000000,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"agent/inbox/spliced","seq":0,"time":1783951000001,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use read_image on red.png in the current directory. If the tool refuses because the current model is text-only, reply with exactly the single word UNAVAILABLE."}],"source":{"kind":"user"},"role":"user","id":"0a0a0a0a-0000-4000-8000-000000000001"}]}}
+{"type":"turn/start","seq":1,"time":1783951000002,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":2,"time":1783951000002,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":3,"time":1783951000003,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1783951000003,"data":{"content":[{"type":"text","text":"Use read_image on red.png in the current directory. If the tool refuses because the current model is text-only, reply with exactly the single word UNAVAILABLE."}],"source":{"kind":"user"},"role":"user","id":"0a0a0a0a-0000-4000-8000-000000000001"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1786344284632,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"11a08f07-014a-408b-bfc5-634770ce7179"},"surfaceOp":"append"}
+{"type":"session/title","seq":6,"time":1786344284632,"data":{"title":"Use read_image on red.png in","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":7,"time":1786344284632,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":8,"time":1786344284633,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":9,"time":1783951000007,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":10,"time":1786344284637,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"read-image-refused","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}}}}
+{"type":"assistant/chunk","seq":11,"time":1786344284637,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":12,"time":1786344284637,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":13,"time":1786344284638,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"read-image-refused","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"9676ac40-f7a8-4a7b-9326-a45fef18f11e"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12],"surfaceOp":"append"}
+{"type":"tool/call","seq":14,"time":1786344284638,"data":{"turn":1,"step":1,"callId":"read-image-refused","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}}
+{"type":"tool/result","seq":15,"time":1786344284643,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"read-image-refused"},"content":[{"type":"tool-result","toolCallId":"read-image-refused","content":[{"type":"text","text":"Error: cannot read \"red.png\" as an image: model \"deepseek-v4-flash\" does not declare image input; switch to an image-capable model to read images"}],"isError":true}],"role":"user","id":"ee31751e-df5a-458e-8497-8113cf6107ef"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
+{"type":"step/end","seq":16,"time":1786344284643,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":17,"time":1786344284648,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":18,"time":1783951000016,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":19,"time":1786344284652,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"UNAVAILABLE"}}}}
+{"type":"assistant/chunk","seq":20,"time":1786344284652,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":21,"time":1786344284652,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":22,"time":1786344284652,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"UNAVAILABLE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"1c15b391-a95a-4113-9d47-2a1dfc991cf9"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21],"surfaceOp":"append"}
+{"type":"step/end","seq":23,"time":1786344284653,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":24,"time":1786344284653,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/read-image-text-route/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/read-image-text-route/stdout.expected.jsonl
new file mode 100644
index 0000000000..f93f99ce97
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/read-image-text-route/stdout.expected.jsonl
@@ -0,0 +1,4 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"UNAVAILABLE"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/read-image-text-route/workspace/red.png b/examples/acp-agent/tests/snapshots/read-image-text-route/workspace/red.png
new file mode 100644
index 0000000000..62a5f8f47f
Binary files /dev/null and b/examples/acp-agent/tests/snapshots/read-image-text-route/workspace/red.png differ
diff --git a/examples/acp-agent/tests/snapshots/read-image/input.json b/examples/acp-agent/tests/snapshots/read-image/input.json
new file mode 100644
index 0000000000..603d2ad4ac
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/read-image/input.json
@@ -0,0 +1,14 @@
+{
+ "steps": [
+ {
+ "op": "initialize"
+ },
+ {
+ "op": "newSession"
+ },
+ {
+ "op": "prompt",
+ "text": "Use read_image to look at red.png in the current directory, then reply with exactly the single word DONE."
+ }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/read-image/session.jsonl b/examples/acp-agent/tests/snapshots/read-image/session.jsonl
new file mode 100644
index 0000000000..6acde5a0ff
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/read-image/session.jsonl
@@ -0,0 +1,26 @@
+{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783951000000,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"agent/inbox/spliced","seq":0,"time":1783951000001,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use read_image to look at red.png in the current directory, then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"0a0a0a0a-0000-4000-8000-000000000001"}]}}
+{"type":"turn/start","seq":1,"time":1783951000002,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":2,"time":1783951000002,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":3,"time":1783951000003,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1783951000003,"data":{"content":[{"type":"text","text":"Use read_image to look at red.png in the current directory, then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"0a0a0a0a-0000-4000-8000-000000000001"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1786344283033,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"eecd1df6-153c-4a34-b198-42bfc9f9701e"},"surfaceOp":"append"}
+{"type":"session/title","seq":6,"time":1786344283033,"data":{"title":"Use read_image to look at","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":7,"time":1786344283034,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":8,"time":1786344283034,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":9,"time":1783951000007,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":10,"time":1786344283039,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"read-image-call","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}}}}
+{"type":"assistant/chunk","seq":11,"time":1786344283039,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":12,"time":1786344283039,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":13,"time":1786344283039,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"read-image-call","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"41e9fb55-6edb-419d-b76c-554daa5a1c5d"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12],"surfaceOp":"append"}
+{"type":"tool/call","seq":14,"time":1786344283039,"data":{"turn":1,"step":1,"callId":"read-image-call","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}}
+{"type":"tool/result","seq":15,"time":1786344283069,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"read-image-call"},"content":[{"type":"tool-result","toolCallId":"read-image-call","content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"isError":false}],"role":"user","id":"0b5779fc-523e-4275-9a32-8eb5e39f521e"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
+{"type":"step/end","seq":16,"time":1786344283069,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":17,"time":1786344283075,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":18,"time":1783951000016,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":19,"time":1786344283078,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
+{"type":"assistant/chunk","seq":20,"time":1786344283079,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":21,"time":1786344283079,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":22,"time":1786344283079,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"73a87a50-8e0b-42af-8c54-d9b6fbe375f1"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21],"surfaceOp":"append"}
+{"type":"step/end","seq":23,"time":1786344283079,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":24,"time":1786344283079,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/read-image/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/read-image/stdout.expected.jsonl
new file mode 100644
index 0000000000..82ae8907ca
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/read-image/stdout.expected.jsonl
@@ -0,0 +1,4 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/read-image/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/read-image/tool-schemas.expected.json
new file mode 100644
index 0000000000..0b495864d6
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/read-image/tool-schemas.expected.json
@@ -0,0 +1,539 @@
+{
+ "initial": [
+ {
+ "name": "bash",
+ "description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "The bash command to execute."
+ },
+ "description": {
+ "type": "string",
+ "description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
+ },
+ "timeoutMs": {
+ "type": "number",
+ "description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
+ },
+ "workdir": {
+ "type": "string",
+ "description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
+ }
+ },
+ "required": [
+ "command",
+ "description"
+ ]
+ }
+ },
+ {
+ "name": "create_goal",
+ "description": "Create one persisted same-session completion goal when the current direct human request is a long-running objective that should continue across autonomous goal rounds. You may infer that intent without requiring the user to say \"create a goal\". Do not use this for trivial single-turn work. Execution rejects non-human and subagent authority.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The concrete completion objective inferred from the direct human request."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer limit on automatic continuation rounds."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "edit",
+ "description": "Edit an existing UTF-8 text file by replacing literal text.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to edit, resolved by the filesystem backend."
+ },
+ "old_string": {
+ "type": "string",
+ "description": "Literal text to replace. Must match exactly."
+ },
+ "new_string": {
+ "type": "string",
+ "description": "Literal replacement text. Use an empty string to delete the match."
+ },
+ "replace_all": {
+ "type": "boolean",
+ "description": "Replace all matches. Defaults to false; when false, old_string must appear exactly once."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "old_string",
+ "new_string"
+ ]
+ }
+ },
+ {
+ "name": "get_goal",
+ "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "interrupt_agent",
+ "description": "Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "agent_id": {
+ "type": "string",
+ "description": "The agent id of the running agent to interrupt."
+ }
+ },
+ "required": [
+ "agent_id"
+ ]
+ }
+ },
+ {
+ "name": "list_agents",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "scope": {
+ "type": "string",
+ "description": "children (default) lists direct children only; descendants walks the complete tree below you.",
+ "enum": [
+ "children",
+ "descendants"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "name": "ralph",
+ "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The immutable completion objective for every fresh Ralph round."
+ },
+ "maxRounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer round cap, bounded by the deployment ceiling."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "read",
+ "description": "Read a UTF-8 text file and return line-numbered content.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to read, resolved by the filesystem backend."
+ },
+ "offset": {
+ "type": "number",
+ "description": "1-based first line to return. Defaults to 1."
+ },
+ "limit": {
+ "type": "number",
+ "description": "Maximum number of lines to return. Defaults to 2000."
+ }
+ },
+ "required": [
+ "file_path"
+ ]
+ }
+ },
+ {
+ "name": "read_image",
+ "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Requires the current model to accept image input.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to the image file, resolved by the filesystem backend."
+ }
+ },
+ "required": [
+ "file_path"
+ ]
+ }
+ },
+ {
+ "name": "send_message",
+ "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "subagent_id": {
+ "type": "string",
+ "description": "The subagent id returned when the background subagent was started."
+ },
+ "message": {
+ "type": "string",
+ "description": "The message to deliver to the subagent."
+ }
+ },
+ "required": [
+ "subagent_id",
+ "message"
+ ]
+ }
+ },
+ {
+ "name": "skill",
+ "description": "Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The exact skill name from the available skills list."
+ }
+ },
+ "required": [
+ "name"
+ ]
+ }
+ },
+ {
+ "name": "subagent",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "subagent_fork",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "task_kill",
+ "description": "Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "reason": {
+ "type": "string",
+ "description": "Optional short reason, recorded in the log and forwarded to the task."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "task_list",
+ "description": "List your background tasks (running and finished) with their ids, kinds, and statuses.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "task_output",
+ "description": "Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "wait": {
+ "type": "boolean",
+ "description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."
+ },
+ "timeout_ms": {
+ "type": "number",
+ "description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "todo_write",
+ "description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "todos": {
+ "type": "array",
+ "description": "The COMPLETE task list, replacing any previous list.",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "What the task is — a short imperative line."
+ },
+ "status": {
+ "type": "string",
+ "description": "pending (not started) | in_progress (now) | completed (done).",
+ "enum": [
+ "pending",
+ "in_progress",
+ "completed"
+ ]
+ }
+ },
+ "required": [
+ "content",
+ "status"
+ ]
+ }
+ }
+ },
+ "required": [
+ "todos"
+ ]
+ }
+ },
+ {
+ "name": "update_goal",
+ "description": "Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "goal_id": {
+ "type": "string",
+ "description": "Exact id returned by get_goal."
+ },
+ "revision": {
+ "type": "number",
+ "description": "Exact positive revision returned by get_goal."
+ },
+ "action": {
+ "type": "string",
+ "description": "edit | pause | resume | complete | blocked",
+ "enum": [
+ "edit",
+ "pause",
+ "resume",
+ "complete",
+ "blocked"
+ ]
+ },
+ "objective": {
+ "type": "string",
+ "description": "Replacement objective; valid only with action edit."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Replacement cap; valid only with action edit."
+ },
+ "blocked_reason": {
+ "type": "string",
+ "description": "Concrete blocking condition; required only with action blocked."
+ }
+ },
+ "required": [
+ "goal_id",
+ "revision",
+ "action"
+ ]
+ }
+ },
+ {
+ "name": "workflow",
+ "description": "Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."
+ },
+ "meta": {
+ "type": "object",
+ "description": "The workflow identity block (plain JSON — never code).",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Short kebab-case workflow name."
+ },
+ "description": {
+ "type": "string",
+ "description": "One-line description of what the workflow does."
+ },
+ "whenToUse": {
+ "type": "string",
+ "description": "Optional guidance on when this workflow applies."
+ },
+ "phases": {
+ "type": "array",
+ "description": "Optional phase declarations matched by phase() calls.",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "The phase title phase() calls match by exact string."
+ },
+ "detail": {
+ "type": "string",
+ "description": "Optional one-line description of the phase."
+ },
+ "provider": {
+ "type": "string",
+ "description": "Optional provider override this phase is expected to use."
+ },
+ "model": {
+ "type": "string",
+ "description": "Optional model override this phase is expected to use."
+ }
+ },
+ "required": [
+ "title"
+ ]
+ }
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ]
+ },
+ "args": {
+ "type": "object",
+ "description": "Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).",
+ "additionalProperties": true
+ }
+ },
+ "required": [
+ "script",
+ "meta"
+ ]
+ }
+ },
+ {
+ "name": "write",
+ "description": "Create or fully replace a UTF-8 text file.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to write, resolved by the filesystem backend."
+ },
+ "content": {
+ "type": "string",
+ "description": "Full UTF-8 text content to write."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "content"
+ ]
+ }
+ }
+ ],
+ "changes": []
+}
diff --git a/examples/acp-agent/tests/snapshots/read-image/workspace/red.png b/examples/acp-agent/tests/snapshots/read-image/workspace/red.png
new file mode 100644
index 0000000000..62a5f8f47f
Binary files /dev/null and b/examples/acp-agent/tests/snapshots/read-image/workspace/red.png differ
diff --git a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json
index aed34842df..f37db3d88b 100644
--- a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -435,7 +435,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -449,7 +449,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -460,7 +460,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -471,10 +471,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl
index 68527f63ac..857dea88b0 100644
--- a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.1.jsonl
@@ -1,29 +1,30 @@
{"type":"session","version":0,"id":"55555555-5555-4555-8555-555555555555","createdAt":2001,"cwd":"{{cwd}}","parentSession":"44444444-4444-4444-8444-444444444444","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1786173701247,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result."}],"source":{"kind":"user"},"role":"user","id":"106c2785-219e-46e8-8386-497ac6a98f68"}]}}
-{"type":"turn/start","seq":1,"time":1786173701247,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1786173701247,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1786173701270,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check deployment question"}}
-{"type":"step/start","seq":4,"time":1786173701272,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1786173701272,"data":{"content":[{"type":"text","text":"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result."}],"source":{"kind":"user"},"role":"user","id":"106c2785-219e-46e8-8386-497ac6a98f68"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1786173701272,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"fafefa37-7640-4c80-a00a-6a0c3ce46281"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1786173701272,"data":{"title":"Call ask_user_question once to ask","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1786173701272,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1786173701273,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":11,"time":1786173701278,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_child_question","name":"ask_user_question","argumentsDelta":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}}}
-{"type":"assistant/chunk","seq":12,"time":1786173701279,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_child_question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}}}}
-{"type":"assistant/chunk","seq":13,"time":1786173701279,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":14,"time":1786173701279,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":15,"time":1786173701279,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_child_question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"301e1969-74b2-45d8-a764-604b806f1c01"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
-{"type":"tool/call","seq":16,"time":1786173701279,"data":{"turn":1,"step":1,"callId":"call_child_question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}}
-{"type":"tool/result","seq":17,"time":1786173701292,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_child_question"},"content":[{"type":"tool-result","toolCallId":"call_child_question","content":[{"type":"text","text":"Error: human interaction is unavailable while the calling agent is owned by another live agent; include the unresolved question or decision in the child agent's final result"}],"isError":true}],"role":"user","id":"b9fc0a38-47bb-4335-a8e4-c881ed66bbc3"},"error":{"name":"UserInteractionError","code":"DELEGATED_CALLER"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
-{"type":"step/end","seq":18,"time":1786173701292,"data":{"turn":1,"step":1}}
-{"type":"step/start","seq":19,"time":1786173701309,"data":{"turn":1,"step":2}}
-{"type":"assistant/chunk","seq":20,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":21,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"UNRESOLVED: Should deployment use the CUDA fallback?"}}}
-{"type":"assistant/chunk","seq":22,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"UNRESOLVED: Should deployment use the CUDA fallback?"}}}}
-{"type":"assistant/chunk","seq":23,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
-{"type":"assistant/chunk","seq":24,"time":1786173701315,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":25,"time":1786173701315,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"UNRESOLVED: Should deployment use the CUDA fallback?"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5f2ada85-5967-4ed8-9e16-eaff2af847b5"},"usage":{"inputTokens":10,"outputTokens":4}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"}
-{"type":"step/end","seq":26,"time":1786173701315,"data":{"turn":1,"step":2}}
-{"type":"turn/end","seq":27,"time":1786173701315,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357535138,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357535138,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result."}],"source":{"kind":"user"},"role":"user","id":"106c2785-219e-46e8-8386-497ac6a98f68"}]}}
+{"type":"turn/start","seq":2,"time":1786357535138,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357535138,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357535155,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check deployment question"}}
+{"type":"step/start","seq":5,"time":1786357535158,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1786173701272,"data":{"content":[{"type":"text","text":"Call ask_user_question once to ask whether deployment should use the CUDA fallback. If the tool returns an error, include the unresolved question verbatim in your final result."}],"source":{"kind":"user"},"role":"user","id":"106c2785-219e-46e8-8386-497ac6a98f68"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357535158,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"d8734c8a-d956-4e3f-8d28-399adf51a203"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357535158,"data":{"title":"Call ask_user_question once to ask","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1786173701272,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1786173701273,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":12,"time":1786173701278,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_child_question","name":"ask_user_question","argumentsDelta":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}}}
+{"type":"assistant/chunk","seq":13,"time":1786173701279,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_child_question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}}}}
+{"type":"assistant/chunk","seq":14,"time":1786173701279,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":15,"time":1786173701279,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":16,"time":1786173701279,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_child_question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"301e1969-74b2-45d8-a764-604b806f1c01"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
+{"type":"tool/call","seq":17,"time":1786173701279,"data":{"turn":1,"step":1,"callId":"call_child_question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"cuda-fallback\",\"header\":\"Deployment\",\"question\":\"Should deployment use the CUDA fallback?\"}]}"}}
+{"type":"tool/result","seq":18,"time":1786173701292,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_child_question"},"content":[{"type":"tool-result","toolCallId":"call_child_question","content":[{"type":"text","text":"Error: human interaction is unavailable while the calling agent is owned by another live agent; include the unresolved question or decision in the child agent's final result"}],"isError":true}],"role":"user","id":"b9fc0a38-47bb-4335-a8e4-c881ed66bbc3"},"error":{"name":"UserInteractionError","code":"DELEGATED_CALLER"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
+{"type":"step/end","seq":19,"time":1786173701292,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":20,"time":1786173701309,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":21,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":22,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"UNRESOLVED: Should deployment use the CUDA fallback?"}}}
+{"type":"assistant/chunk","seq":23,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"UNRESOLVED: Should deployment use the CUDA fallback?"}}}}
+{"type":"assistant/chunk","seq":24,"time":1786173701314,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
+{"type":"assistant/chunk","seq":25,"time":1786173701315,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":26,"time":1786173701315,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"UNRESOLVED: Should deployment use the CUDA fallback?"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5f2ada85-5967-4ed8-9e16-eaff2af847b5"},"usage":{"inputTokens":10,"outputTokens":4}},"sourceEventSeqs":[21,22,23,24,25],"surfaceOp":"append"}
+{"type":"step/end","seq":27,"time":1786173701315,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":28,"time":1786173701315,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.jsonl
index ed155f158f..92efdfa71a 100644
--- a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/session.jsonl
@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1786173701175,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1786173701216,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1786173701216,"data":{"content":[{"type":"text","text":"Delegate one question check. Ask the child to call ask_user_question once about the CUDA fallback and return any unresolved question in its final result."}],"source":{"kind":"user"},"role":"user","id":"851bea02-2961-471a-84ec-3b068c451db0"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1786173701217,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"8ef74c46-9e80-475c-9093-0e85ba92e346"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1786173701217,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e1f92805-80c9-46b7-94ac-6cdb05d23f86"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1786173701217,"data":{"title":"Delegate one question check. Ask","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1786173701218,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1786173701219,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json
index dc07c990aa..ffd8b4bd7e 100644
--- a/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/subagent-child-question-rejection/tool-schemas.expected.json
@@ -196,7 +196,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -294,7 +294,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -308,7 +308,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -319,7 +319,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -330,10 +330,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/input.json b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/input.json
new file mode 100644
index 0000000000..f07f1d51fc
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/input.json
@@ -0,0 +1,26 @@
+{
+ "steps": [
+ {
+ "op": "initialize"
+ },
+ {
+ "op": "newSession"
+ },
+ {
+ "op": "prompt",
+ "text": "Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Reply with the single word DONE. Do not use the bash tool."
+ },
+ {
+ "op": "waitForSubagentTurnEnd",
+ "child": 1,
+ "minimumTurn": 1
+ },
+ {
+ "op": "waitForTurnStart",
+ "minimumTurn": 2
+ },
+ {
+ "op": "waitForTurnEnd"
+ }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.1.jsonl
new file mode 100644
index 0000000000..bd9557666f
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.1.jsonl
@@ -0,0 +1,22 @@
+{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
+{"type":"subagent/descriptor","seq":0,"time":1786333735890,"data":{"version":2,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
+{"type":"session/end-seed","seq":1,"time":1786333735890,"data":{}}
+{"type":"sandbox/mode","seq":2,"time":1786333735890,"data":{"mode":"read-only","source":"delegation"}}
+{"type":"approval/policy","seq":3,"time":1786357527742,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":4,"time":1786357527743,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"42af19c7-e234-4752-93d4-bd9c943c1fe7"}]}}
+{"type":"turn/start","seq":5,"time":1786357527743,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":6,"time":1786357527743,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":7,"time":1786357527768,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":8,"time":1786333735916,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"42af19c7-e234-4752-93d4-bd9c943c1fe7"},"surfaceOp":"append"}
+{"type":"user/message","seq":9,"time":1786357527769,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"1b760052-ffcb-44d2-aae2-fd73d7c444f1"},"surfaceOp":"append"}
+{"type":"session/title","seq":10,"time":1786357527769,"data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":11,"time":1786333735916,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":12,"time":1786333735916,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":13,"time":1786333735920,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":14,"time":1786333735921,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_OK"}}}
+{"type":"assistant/chunk","seq":15,"time":1786333735921,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_OK"}}}}
+{"type":"assistant/chunk","seq":16,"time":1786333735921,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":17,"time":1786333735921,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":18,"time":1786333735921,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e2b94008-b067-4ca7-a576-6b4a9060cd83"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
+{"type":"step/end","seq":19,"time":1786333735921,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":20,"time":1786333735921,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.jsonl
new file mode 100644
index 0000000000..293f9c3b83
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/session.jsonl
@@ -0,0 +1,42 @@
+{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"sandbox/mode","seq":0,"time":1786333735842,"data":{"mode":"read-only"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786333735845,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"d554122c-d857-4de0-aea0-6452f260d032"}]}}
+{"type":"turn/start","seq":2,"time":1786333735845,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786333735845,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":4,"time":1786333735878,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":5,"time":1786333735878,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"d554122c-d857-4de0-aea0-6452f260d032"},"surfaceOp":"append"}
+{"type":"user/message","seq":6,"time":1786333735878,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"f931abf5-bb3a-44b4-8fe2-2d06e8766184"},"surfaceOp":"append"}
+{"type":"session/title","seq":7,"time":1786333735878,"data":{"title":"Follow these steps exactly, then","messageSeqs":[5],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":8,"time":1786333735879,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":9,"time":1786333735879,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":10,"time":1789000000000,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":11,"time":1786333735884,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_bg_start","name":"subagent","argumentsDelta":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}
+{"type":"assistant/chunk","seq":12,"time":1786333735884,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}}
+{"type":"assistant/chunk","seq":13,"time":1786333735884,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":14,"time":1786333735884,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":15,"time":1786333735884,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"8ab58a42-e74c-4121-a6ca-63696e592287"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
+{"type":"tool/call","seq":16,"time":1786333735885,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
+{"type":"tool/result","seq":17,"time":1786333735892,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"3478555e-f0d0-4ec1-a7e4-a15ab24b9ecf"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
+{"type":"step/end","seq":18,"time":1786333735892,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":19,"time":1786333735897,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":20,"time":1786333735903,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":21,"time":1786333735903,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
+{"type":"assistant/chunk","seq":22,"time":1786333735903,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
+{"type":"assistant/chunk","seq":23,"time":1786333735904,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":24,"time":1786333735904,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":25,"time":1786333735904,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4057a08e-b50e-45e7-beb0-c74485f2b7d6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"}
+{"type":"step/end","seq":26,"time":1786333735904,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":27,"time":1786333735904,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"agent/inbox/spliced","seq":28,"time":1786374357751,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"e0bd4902-daba-4e23-bfcb-9e102fdd203d"}]}}
+{"type":"turn/start","seq":29,"time":1786374357751,"data":{"turn":2}}
+{"type":"agent/inbox/spliced","seq":30,"time":1786374357751,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":31,"time":1786374357758,"data":{"turn":2,"step":1}}
+{"type":"user/message","seq":32,"time":1786374357758,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"e0bd4902-daba-4e23-bfcb-9e102fdd203d"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":33,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":34,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
+{"type":"assistant/chunk","seq":35,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
+{"type":"assistant/chunk","seq":36,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":37,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":38,"time":1786374357764,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"bbe5ef7b-2a3a-47f4-8475-60945b31a373"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[33,34,35,36,37],"surfaceOp":"append"}
+{"type":"step/end","seq":39,"time":1786374357765,"data":{"turn":2,"step":1}}
+{"type":"turn/end","seq":40,"time":1786374357765,"data":{"turn":2,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/stdout.expected.jsonl
new file mode 100644
index 0000000000..d6a2728b5a
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/stdout.expected.jsonl
@@ -0,0 +1,5 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/system-prompt.1.expected.md b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/system-prompt.1.expected.md
new file mode 100644
index 0000000000..71f4ed05f8
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/system-prompt.1.expected.md
@@ -0,0 +1,24 @@
+You are an AI agent powered by the DeepSeek Harness SDK.
+
+You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+Verify your work by running the code or tests. Keep answers brief and factual.
+
+
+Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.
+
+Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.
+
+Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.
+
+Check the [exit code: N] marker on every bash result; investigate failures before moving on.
+
+Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
+
+Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
+
+Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.
+
+Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
+
+Deliver your result with the report tool before you finish: call it once with a self-contained answer. The agent that started you shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so a closing remark such as "done" leaves it nothing it can use. Report earlier as well whenever a partial finding changes what that agent should do next; reporting never ends your turn.
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/tool-schemas.1.expected.json
new file mode 100644
index 0000000000..253fbc4a32
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable-inheritance/tool-schemas.1.expected.json
@@ -0,0 +1,539 @@
+{
+ "initial": [
+ {
+ "name": "bash",
+ "description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "The bash command to execute."
+ },
+ "description": {
+ "type": "string",
+ "description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
+ },
+ "timeoutMs": {
+ "type": "number",
+ "description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
+ },
+ "workdir": {
+ "type": "string",
+ "description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
+ }
+ },
+ "required": [
+ "command",
+ "description"
+ ]
+ }
+ },
+ {
+ "name": "create_goal",
+ "description": "Create one persisted same-session completion goal when the current direct human request is a long-running objective that should continue across autonomous goal rounds. You may infer that intent without requiring the user to say \"create a goal\". Do not use this for trivial single-turn work. Execution rejects non-human and subagent authority.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The concrete completion objective inferred from the direct human request."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer limit on automatic continuation rounds."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "edit",
+ "description": "Edit an existing UTF-8 text file by replacing literal text.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to edit, resolved by the filesystem backend."
+ },
+ "old_string": {
+ "type": "string",
+ "description": "Literal text to replace. Must match exactly."
+ },
+ "new_string": {
+ "type": "string",
+ "description": "Literal replacement text. Use an empty string to delete the match."
+ },
+ "replace_all": {
+ "type": "boolean",
+ "description": "Replace all matches. Defaults to false; when false, old_string must appear exactly once."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "old_string",
+ "new_string"
+ ]
+ }
+ },
+ {
+ "name": "get_goal",
+ "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "interrupt_agent",
+ "description": "Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "agent_id": {
+ "type": "string",
+ "description": "The agent id of the running agent to interrupt."
+ }
+ },
+ "required": [
+ "agent_id"
+ ]
+ }
+ },
+ {
+ "name": "list_agents",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "scope": {
+ "type": "string",
+ "description": "children (default) lists direct children only; descendants walks the complete tree below you.",
+ "enum": [
+ "children",
+ "descendants"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "name": "ralph",
+ "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "objective": {
+ "type": "string",
+ "description": "The immutable completion objective for every fresh Ralph round."
+ },
+ "maxRounds": {
+ "type": "number",
+ "description": "Optional positive safe-integer round cap, bounded by the deployment ceiling."
+ }
+ },
+ "required": [
+ "objective"
+ ]
+ }
+ },
+ {
+ "name": "read",
+ "description": "Read a UTF-8 text file and return line-numbered content.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to read, resolved by the filesystem backend."
+ },
+ "offset": {
+ "type": "number",
+ "description": "1-based first line to return. Defaults to 1."
+ },
+ "limit": {
+ "type": "number",
+ "description": "Maximum number of lines to return. Defaults to 2000."
+ }
+ },
+ "required": [
+ "file_path"
+ ]
+ }
+ },
+ {
+ "name": "report",
+ "description": "Report selected content to the agent that started you. Call this once before you finish, with a self-contained final result, and earlier for progress or findings that change what that agent does next. That agent shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so finishing your work is not itself a result. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "output": {
+ "type": "string",
+ "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
+ }
+ },
+ "required": [
+ "output"
+ ]
+ }
+ },
+ {
+ "name": "send_message",
+ "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "subagent_id": {
+ "type": "string",
+ "description": "The subagent id returned when the background subagent was started."
+ },
+ "message": {
+ "type": "string",
+ "description": "The message to deliver to the subagent."
+ }
+ },
+ "required": [
+ "subagent_id",
+ "message"
+ ]
+ }
+ },
+ {
+ "name": "skill",
+ "description": "Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The exact skill name from the available skills list."
+ }
+ },
+ "required": [
+ "name"
+ ]
+ }
+ },
+ {
+ "name": "subagent",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
+ },
+ "run_in_background": {
+ "type": "boolean",
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "subagent_fork",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "A short (3-5 word) description of the delegated task, for display."
+ },
+ "prompt": {
+ "type": "string",
+ "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
+ }
+ },
+ "required": [
+ "description",
+ "prompt"
+ ]
+ }
+ },
+ {
+ "name": "task_kill",
+ "description": "Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "reason": {
+ "type": "string",
+ "description": "Optional short reason, recorded in the log and forwarded to the task."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "task_list",
+ "description": "List your background tasks (running and finished) with their ids, kinds, and statuses.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ {
+ "name": "task_output",
+ "description": "Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "Task id returned by the tool that started the background work."
+ },
+ "wait": {
+ "type": "boolean",
+ "description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."
+ },
+ "timeout_ms": {
+ "type": "number",
+ "description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
+ }
+ },
+ "required": [
+ "task_id"
+ ]
+ }
+ },
+ {
+ "name": "todo_write",
+ "description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "todos": {
+ "type": "array",
+ "description": "The COMPLETE task list, replacing any previous list.",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "What the task is — a short imperative line."
+ },
+ "status": {
+ "type": "string",
+ "description": "pending (not started) | in_progress (now) | completed (done).",
+ "enum": [
+ "pending",
+ "in_progress",
+ "completed"
+ ]
+ }
+ },
+ "required": [
+ "content",
+ "status"
+ ]
+ }
+ }
+ },
+ "required": [
+ "todos"
+ ]
+ }
+ },
+ {
+ "name": "update_goal",
+ "description": "Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "goal_id": {
+ "type": "string",
+ "description": "Exact id returned by get_goal."
+ },
+ "revision": {
+ "type": "number",
+ "description": "Exact positive revision returned by get_goal."
+ },
+ "action": {
+ "type": "string",
+ "description": "edit | pause | resume | complete | blocked",
+ "enum": [
+ "edit",
+ "pause",
+ "resume",
+ "complete",
+ "blocked"
+ ]
+ },
+ "objective": {
+ "type": "string",
+ "description": "Replacement objective; valid only with action edit."
+ },
+ "max_goal_rounds": {
+ "type": "number",
+ "description": "Replacement cap; valid only with action edit."
+ },
+ "blocked_reason": {
+ "type": "string",
+ "description": "Concrete blocking condition; required only with action blocked."
+ }
+ },
+ "required": [
+ "goal_id",
+ "revision",
+ "action"
+ ]
+ }
+ },
+ {
+ "name": "workflow",
+ "description": "Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."
+ },
+ "meta": {
+ "type": "object",
+ "description": "The workflow identity block (plain JSON — never code).",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Short kebab-case workflow name."
+ },
+ "description": {
+ "type": "string",
+ "description": "One-line description of what the workflow does."
+ },
+ "whenToUse": {
+ "type": "string",
+ "description": "Optional guidance on when this workflow applies."
+ },
+ "phases": {
+ "type": "array",
+ "description": "Optional phase declarations matched by phase() calls.",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "The phase title phase() calls match by exact string."
+ },
+ "detail": {
+ "type": "string",
+ "description": "Optional one-line description of the phase."
+ },
+ "provider": {
+ "type": "string",
+ "description": "Optional provider override this phase is expected to use."
+ },
+ "model": {
+ "type": "string",
+ "description": "Optional model override this phase is expected to use."
+ }
+ },
+ "required": [
+ "title"
+ ]
+ }
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ]
+ },
+ "args": {
+ "type": "object",
+ "description": "Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).",
+ "additionalProperties": true
+ }
+ },
+ "required": [
+ "script",
+ "meta"
+ ]
+ }
+ },
+ {
+ "name": "write",
+ "description": "Create or fully replace a UTF-8 text file.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {
+ "type": "string",
+ "description": "Path to write, resolved by the filesystem backend."
+ },
+ "content": {
+ "type": "string",
+ "description": "Full UTF-8 text content to write."
+ },
+ "sandbox_permissions": {
+ "type": "string",
+ "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.",
+ "enum": [
+ "workspace-write",
+ "danger-full-access"
+ ]
+ },
+ "justification": {
+ "type": "string",
+ "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access."
+ }
+ },
+ "required": [
+ "file_path",
+ "content"
+ ]
+ }
+ }
+ ],
+ "changes": []
+}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/input.json b/examples/acp-agent/tests/snapshots/subagent-continuable/input.json
index f1354bd76a..380a7825c2 100644
--- a/examples/acp-agent/tests/snapshots/subagent-continuable/input.json
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable/input.json
@@ -10,6 +10,17 @@
"op": "prompt",
"text": "Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."
},
- { "op": "waitForSubagentTurnEnd", "child": 1, "minimumTurn": 3 }
+ {
+ "op": "waitForSubagentTurnEnd",
+ "child": 1,
+ "minimumTurn": 3
+ },
+ {
+ "op": "waitForTurnStart",
+ "minimumTurn": 2
+ },
+ {
+ "op": "waitForTurnEnd"
+ }
]
}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl
index 554de6f448..9352ca6fb9 100644
--- a/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl
@@ -1,37 +1,38 @@
{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
{"type":"subagent/descriptor","seq":0,"time":1785544945198,"data":{"version":2,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
{"type":"session/end-seed","seq":1,"time":1785544945198,"data":{}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785730451347,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c67a308f-d867-424e-b198-c9f464228703"}]}}
-{"type":"turn/start","seq":3,"time":1785821409024,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":4,"time":1785730917162,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","seq":5,"time":1785730917192,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"}]}}
-{"type":"agent/inbox/spliced","seq":6,"time":1785821409076,"data":{"target":"next-turn","start":1,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"755c76db-6ee8-432d-a2d0-f8a3b7914e08"}]}}
-{"type":"step/start","seq":7,"time":1785730917198,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":8,"time":1785730917198,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c67a308f-d867-424e-b198-c9f464228703"},"surfaceOp":"append"}
-{"type":"user/message","seq":9,"time":1785730917198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"49acbc16-4d58-460e-8cc0-62838472dce6"},"surfaceOp":"append"}
-{"type":"session/title","seq":10,"time":1785730917198,"data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":11,"time":1785730917198,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":12,"time":1785730917199,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":13,"time":1785730696668,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":14,"time":1789000000010,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_OK"}}}
-{"type":"assistant/chunk","seq":15,"time":1789000000011,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_OK"}}}}
-{"type":"assistant/chunk","seq":16,"time":1789000000012,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":17,"time":1785730451397,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":18,"time":1785730696668,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"178ea526-9e19-49d2-b3b0-57b682320028"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
-{"type":"step/end","seq":19,"time":1785730696668,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":20,"time":1785730696669,"data":{"turn":1,"reason":{"kind":"completed"}}}
-{"type":"turn/start","seq":21,"time":1785821409092,"data":{"turn":2}}
-{"type":"agent/inbox/spliced","seq":22,"time":1785821409092,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","seq":23,"time":1785730696682,"data":{"turn":2,"step":1}}
-{"type":"user/message","seq":24,"time":1785730696682,"data":{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"},"surfaceOp":"append"}
-{"type":"assistant/chunk","seq":25,"time":1785730696686,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":26,"time":1785730696686,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SECOND_OK"}}}
-{"type":"assistant/chunk","seq":27,"time":1789000000023,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SECOND_OK"}}}}
-{"type":"assistant/chunk","seq":28,"time":1785730451421,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":29,"time":1785730451421,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":30,"time":1785730696686,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SECOND_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ced209bf-5d6d-4880-b187-18cb816a150c"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[25,26,27,28,29],"surfaceOp":"append"}
-{"type":"step/end","seq":31,"time":1785730696686,"data":{"turn":2,"step":1}}
-{"type":"turn/end","seq":32,"time":1785730696686,"data":{"turn":2,"reason":{"kind":"completed"}}}
-{"type":"turn/start","seq":33,"time":1785821409110,"data":{"turn":3}}
-{"type":"agent/inbox/spliced","seq":34,"time":1785821409110,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"turn/end","seq":35,"time":1785821409122,"data":{"turn":3,"reason":{"kind":"error","error":{"message":"snapshot disk full","code":"UNKNOWN"}}}}
+{"type":"approval/policy","seq":2,"time":1786357526242,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357526243,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c67a308f-d867-424e-b198-c9f464228703"}]}}
+{"type":"turn/start","seq":4,"time":1786357526243,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":5,"time":1785730917192,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"agent/inbox/spliced","seq":6,"time":1785821409076,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"}]}}
+{"type":"agent/inbox/spliced","seq":7,"time":1786357526278,"data":{"target":"next-turn","start":1,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"755c76db-6ee8-432d-a2d0-f8a3b7914e08"}]}}
+{"type":"step/start","seq":8,"time":1786357526284,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":9,"time":1785730917198,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c67a308f-d867-424e-b198-c9f464228703"},"surfaceOp":"append"}
+{"type":"user/message","seq":10,"time":1786357526284,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"7f1d7407-d9bc-4ec6-ae42-a8767e0e1153"},"surfaceOp":"append"}
+{"type":"session/title","seq":11,"time":1786357526284,"data":{"title":"Reply with exactly the word","messageSeqs":[9],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":12,"time":1785730917198,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":13,"time":1785730917199,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":14,"time":1785730696668,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":15,"time":1789000000010,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_OK"}}}
+{"type":"assistant/chunk","seq":16,"time":1789000000011,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_OK"}}}}
+{"type":"assistant/chunk","seq":17,"time":1789000000012,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":18,"time":1785730451397,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":19,"time":1785730696668,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"178ea526-9e19-49d2-b3b0-57b682320028"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
+{"type":"step/end","seq":20,"time":1785730696668,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":21,"time":1785730696669,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"turn/start","seq":22,"time":1785821409092,"data":{"turn":2}}
+{"type":"agent/inbox/spliced","seq":23,"time":1785821409092,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":24,"time":1785730696682,"data":{"turn":2,"step":1}}
+{"type":"user/message","seq":25,"time":1785730696682,"data":{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":26,"time":1785730696686,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":27,"time":1785730696686,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SECOND_OK"}}}
+{"type":"assistant/chunk","seq":28,"time":1789000000023,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SECOND_OK"}}}}
+{"type":"assistant/chunk","seq":29,"time":1785730451421,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":30,"time":1785730451421,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":31,"time":1785730696686,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SECOND_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ced209bf-5d6d-4880-b187-18cb816a150c"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[26,27,28,29,30],"surfaceOp":"append"}
+{"type":"step/end","seq":32,"time":1785730696686,"data":{"turn":2,"step":1}}
+{"type":"turn/end","seq":33,"time":1785730696686,"data":{"turn":2,"reason":{"kind":"completed"}}}
+{"type":"turn/start","seq":34,"time":1785821409110,"data":{"turn":3}}
+{"type":"agent/inbox/spliced","seq":35,"time":1785821409110,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"turn/end","seq":36,"time":1785821409122,"data":{"turn":3,"reason":{"kind":"error","error":{"message":"snapshot disk full","code":"UNKNOWN"}}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl
index 898d4b250c..60a55d1373 100644
--- a/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl
@@ -1,10 +1,10 @@
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"agent/inbox/spliced","seq":0,"time":1785730451297,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"125665d3-8c03-4190-b4f9-c27d61d245f4"}]}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785730451297,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"579d3d6d-a57e-4d55-9b48-05832a79d9f8"}]}}
{"type":"turn/start","seq":1,"time":1785821408972,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821408972,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1785730451327,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":4,"time":1785730451327,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"125665d3-8c03-4190-b4f9-c27d61d245f4"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730451328,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e7521889-28d8-4434-84b2-21ff0e044fe7"},"surfaceOp":"append"}
+{"type":"user/message","seq":4,"time":1785730451327,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"579d3d6d-a57e-4d55-9b48-05832a79d9f8"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730451328,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"c4f5f7ed-1c11-4f31-923f-3142c79f0c2c"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730451328,"data":{"title":"Follow these steps exactly, then","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785730451329,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730451329,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -13,9 +13,9 @@
{"type":"assistant/chunk","seq":11,"time":1785544945188,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}}
{"type":"assistant/chunk","seq":12,"time":1785544945188,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":13,"time":1785730451338,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":14,"time":1785730451338,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4a2c2f8b-66be-4860-9bbf-b83feb56009e"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
+{"type":"assistant/message","seq":14,"time":1785730451338,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"680da987-6d29-4141-b83d-af57b050c712"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1785730451338,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
-{"type":"tool/result","seq":16,"time":1785730451348,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"b54a0d61-4233-40f1-ab3a-3eb58e1b0c61"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
+{"type":"tool/result","seq":16,"time":1785730451348,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"7825edb2-080e-49c1-ba74-ad69d16bf566"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1785730451348,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1785730451360,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1789000000019,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -23,9 +23,9 @@
{"type":"assistant/chunk","seq":21,"time":1785544945212,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}}}
{"type":"assistant/chunk","seq":22,"time":1785544945212,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":23,"time":1785730451364,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":24,"time":1785730451364,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"398dea92-a100-4b2f-a9e1-72629def132d"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
+{"type":"assistant/message","seq":24,"time":1785730451364,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ef6eadc7-165e-4705-b865-3889f0af0f36"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"tool/call","seq":25,"time":1785730451365,"data":{"turn":1,"step":2,"callId":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}
-{"type":"tool/result","seq":26,"time":1785730451377,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"45b388fd-6a48-4a02-9f3b-1d642e797c71"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
+{"type":"tool/result","seq":26,"time":1785730451377,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"ac1214a5-1d91-4fab-8f96-833baca114f8"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
{"type":"step/end","seq":27,"time":1785730451377,"data":{"turn":1,"step":2}}
{"type":"step/start","seq":28,"time":1785730451390,"data":{"turn":1,"step":3}}
{"type":"assistant/chunk","seq":29,"time":1789000000029,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -33,9 +33,9 @@
{"type":"assistant/chunk","seq":31,"time":1785544945241,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}}}
{"type":"assistant/chunk","seq":32,"time":1785544945242,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":33,"time":1785730451394,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":34,"time":1785730451394,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f56cec19-e761-4c77-9237-07d12d334275"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
+{"type":"assistant/message","seq":34,"time":1785730451394,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"33939813-0792-4ac5-8864-ec62a4ddff8e"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
{"type":"tool/call","seq":35,"time":1785730451395,"data":{"turn":1,"step":3,"callId":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}
-{"type":"tool/result","seq":36,"time":1785730451406,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"193df31b-de9a-4723-b2b1-c7c96a8eaee4"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
+{"type":"tool/result","seq":36,"time":1785730451406,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"a6f64c64-f50c-47cd-a6b3-a3b57d3dc83d"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
{"type":"step/end","seq":37,"time":1785730451406,"data":{"turn":1,"step":3}}
{"type":"step/start","seq":38,"time":1785730451419,"data":{"turn":1,"step":4}}
{"type":"assistant/chunk","seq":39,"time":1789000000039,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -43,9 +43,9 @@
{"type":"assistant/chunk","seq":41,"time":1785544945273,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}}}
{"type":"assistant/chunk","seq":42,"time":1785544945273,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":43,"time":1785730451424,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":44,"time":1785730451424,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cbf54e23-fb96-45cc-b629-b9cb48fc9876"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}
+{"type":"assistant/message","seq":44,"time":1785730451424,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"95eab91d-b103-4033-8e2e-c9c93b1b0211"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}
{"type":"tool/call","seq":45,"time":1785730451425,"data":{"turn":1,"step":4,"callId":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}
-{"type":"tool/result","seq":46,"time":1785730451437,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"d5d961af-c171-40b6-87a1-33d2c14740a7"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[45],"surfaceOp":"append"}
+{"type":"tool/result","seq":46,"time":1785730451437,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"8a095e4b-3059-420d-856f-1cbd20b6a2e2"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[45],"surfaceOp":"append"}
{"type":"step/end","seq":47,"time":1785730451437,"data":{"turn":1,"step":4}}
{"type":"step/start","seq":48,"time":1785730451450,"data":{"turn":1,"step":5}}
{"type":"assistant/chunk","seq":49,"time":1785394678779,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
@@ -53,6 +53,19 @@
{"type":"assistant/chunk","seq":51,"time":1785544945303,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":52,"time":1785544945303,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":53,"time":1785730451453,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":54,"time":1785730451453,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"fbea6b64-84cf-4411-abec-48d26b3801da"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
+{"type":"assistant/message","seq":54,"time":1785730451453,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"eb51ecb3-3347-4216-ad4e-c2130c43ecfc"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
{"type":"step/end","seq":55,"time":1785730451454,"data":{"turn":1,"step":5}}
{"type":"turn/end","seq":56,"time":1785730451454,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"agent/inbox/spliced","seq":57,"time":1786011346660,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"2cf0afd2-ee6e-4a3f-a35a-2fd4d5b665ca"}]}}
+{"type":"turn/start","seq":58,"time":1786011346660,"data":{"turn":2}}
+{"type":"agent/inbox/spliced","seq":59,"time":1786011346660,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":60,"time":1786012466689,"data":{"turn":2,"step":1}}
+{"type":"user/message","seq":61,"time":1786012466689,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"2cf0afd2-ee6e-4a3f-a35a-2fd4d5b665ca"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":62,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":63,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
+{"type":"assistant/chunk","seq":64,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
+{"type":"assistant/chunk","seq":65,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":66,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":67,"time":1786333283620,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"758adcee-9284-4889-86a2-0181a278a754"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[62,63,64,65,66],"surfaceOp":"append"}
+{"type":"step/end","seq":68,"time":1786333283620,"data":{"turn":2,"step":1}}
+{"type":"turn/end","seq":69,"time":1786333283620,"data":{"turn":2,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable/stdout.expected.jsonl
index 82ae8907ca..d6a2728b5a 100644
--- a/examples/acp-agent/tests/snapshots/subagent-continuable/stdout.expected.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable/stdout.expected.jsonl
@@ -2,3 +2,4 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/system-prompt.1.expected.md b/examples/acp-agent/tests/snapshots/subagent-continuable/system-prompt.1.expected.md
new file mode 100644
index 0000000000..71f4ed05f8
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable/system-prompt.1.expected.md
@@ -0,0 +1,24 @@
+You are an AI agent powered by the DeepSeek Harness SDK.
+
+You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+Verify your work by running the code or tests. Keep answers brief and factual.
+
+
+Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.
+
+Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.
+
+Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.
+
+Check the [exit code: N] marker on every bash result; investigate failures before moving on.
+
+Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
+
+Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
+
+Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.
+
+Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
+
+Deliver your result with the report tool before you finish: call it once with a self-contained answer. The agent that started you shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so a closing remark such as "done" leaves it nothing it can use. Report earlier as well whenever a partial finding changes what that agent should do next; reporting never ends your turn.
diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json
index 7dd791cf27..253fbc4a32 100644
--- a/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json
+++ b/examples/acp-agent/tests/snapshots/subagent-continuable/tool-schemas.1.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -194,13 +194,13 @@
},
{
"name": "report",
- "description": "Report selected content to the agent that started you. Call this zero or more times for progress, findings, or a final answer. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.",
+ "description": "Report selected content to the agent that started you. Call this once before you finish, with a self-contained final result, and earlier for progress or findings that change what that agent does next. That agent shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so finishing your work is not itself a result. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.",
"parameters": {
"type": "object",
"properties": {
"output": {
"type": "string",
- "description": "Self-contained content for your parent; it does not see your private work."
+ "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
}
},
"required": [
@@ -247,7 +247,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -261,7 +261,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -272,7 +272,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -283,10 +283,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl
index 8b8b8cc0c2..d793d62624 100644
--- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl
@@ -1,29 +1,30 @@
{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1001,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498798860,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"}]}}
-{"type":"turn/start","seq":1,"time":1785821414174,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821414174,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821414185,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth one"}}
-{"type":"step/start","seq":4,"time":1785730456013,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730456014,"data":{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730456014,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"3244b13c-f211-445f-acf5-fb8d1534537c"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730456014,"data":{"title":"Call subagent once. Ask that","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730456014,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730456014,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_depth_one_child","name":"subagent","argumentsDelta":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}}
-{"type":"assistant/chunk","seq":12,"time":1785498798883,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}}}
-{"type":"assistant/chunk","seq":13,"time":1785730456018,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":14,"time":1785730456018,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":15,"time":1785730456018,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"21044d12-2e0e-40e3-b47e-4920e21c3e83"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
-{"type":"tool/call","seq":16,"time":1785730456019,"data":{"turn":1,"step":1,"callId":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}
-{"type":"tool/result","seq":17,"time":1785730456072,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_one_child"},"content":[{"type":"tool-result","toolCallId":"call_depth_one_child","content":[{"type":"text","text":"DEPTH_REJECTED"}],"isError":false}],"role":"user","id":"aa5451a8-812b-4a51-a52c-dbc5c84f16d0"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
-{"type":"step/end","seq":18,"time":1785730456072,"data":{"turn":1,"step":1}}
-{"type":"step/start","seq":19,"time":1785730456082,"data":{"turn":1,"step":2}}
-{"type":"assistant/chunk","seq":20,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":21,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DEPTH_ONE_DONE"}}}
-{"type":"assistant/chunk","seq":22,"time":1785498798949,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}}}
-{"type":"assistant/chunk","seq":23,"time":1785730456086,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
-{"type":"assistant/chunk","seq":24,"time":1785730456086,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":25,"time":1785730456086,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5a1911c6-f487-458c-b802-4a66221ec046"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"}
-{"type":"step/end","seq":26,"time":1785730456086,"data":{"turn":1,"step":2}}
-{"type":"turn/end","seq":27,"time":1785730456086,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357533581,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357533582,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"}]}}
+{"type":"turn/start","seq":2,"time":1786357533582,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357533582,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357533600,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth one"}}
+{"type":"step/start","seq":5,"time":1786357533602,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730456014,"data":{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357533602,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"d8d6b5bf-db7b-484f-8095-e35915b94274"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357533602,"data":{"title":"Call subagent once. Ask that","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730456014,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730456014,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_depth_one_child","name":"subagent","argumentsDelta":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}}
+{"type":"assistant/chunk","seq":13,"time":1785498798883,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}}}
+{"type":"assistant/chunk","seq":14,"time":1785730456018,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":15,"time":1785730456018,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":16,"time":1785730456018,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"21044d12-2e0e-40e3-b47e-4920e21c3e83"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
+{"type":"tool/call","seq":17,"time":1785730456019,"data":{"turn":1,"step":1,"callId":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}
+{"type":"tool/result","seq":18,"time":1785730456072,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_one_child"},"content":[{"type":"tool-result","toolCallId":"call_depth_one_child","content":[{"type":"text","text":"DEPTH_REJECTED"}],"isError":false}],"role":"user","id":"aa5451a8-812b-4a51-a52c-dbc5c84f16d0"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
+{"type":"step/end","seq":19,"time":1785730456072,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":20,"time":1785730456082,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":21,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":22,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DEPTH_ONE_DONE"}}}
+{"type":"assistant/chunk","seq":23,"time":1785498798949,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}}}
+{"type":"assistant/chunk","seq":24,"time":1785730456086,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
+{"type":"assistant/chunk","seq":25,"time":1785730456086,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":26,"time":1785730456086,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5a1911c6-f487-458c-b802-4a66221ec046"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[21,22,23,24,25],"surfaceOp":"append"}
+{"type":"step/end","seq":27,"time":1785730456086,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":28,"time":1785730456086,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl
index 7f2ae89966..62c259289d 100644
--- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl
@@ -1,29 +1,30 @@
{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1002,"cwd":"{{cwd}}","parentSession":"22222222-2222-4222-8222-222222222222","origin":"subagent","delegationDepth":2}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498798891,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"}]}}
-{"type":"turn/start","seq":1,"time":1785821414201,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821414201,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821414214,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth two"}}
-{"type":"step/start","seq":4,"time":1785730456041,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730456041,"data":{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730456041,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4a252f7d-8523-433f-a3fc-33812be802ec"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730456041,"data":{"title":"Attempt one subagent call beyond","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730456041,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730456042,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_depth_three_rejected","name":"subagent","argumentsDelta":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}}
-{"type":"assistant/chunk","seq":12,"time":1785498798916,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}}}
-{"type":"assistant/chunk","seq":13,"time":1785730456047,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":14,"time":1785730456047,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":15,"time":1785730456047,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"467433db-5dbf-42ee-94c0-25c011ce711b"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
-{"type":"tool/call","seq":16,"time":1785730456048,"data":{"turn":1,"step":1,"callId":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}
-{"type":"tool/result","seq":17,"time":1785730456056,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_three_rejected"},"content":[{"type":"tool-result","toolCallId":"call_depth_three_rejected","content":[{"type":"text","text":"Error: subagent depth 3 exceeds maxDepth 2"}],"isError":true}],"role":"user","id":"9a3d59f3-542a-4400-a62c-be28dcea3bd1"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
-{"type":"step/end","seq":18,"time":1785730456056,"data":{"turn":1,"step":1}}
-{"type":"step/start","seq":19,"time":1785730456066,"data":{"turn":1,"step":2}}
-{"type":"assistant/chunk","seq":20,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":21,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DEPTH_REJECTED"}}}
-{"type":"assistant/chunk","seq":22,"time":1785498798937,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_REJECTED"}}}}
-{"type":"assistant/chunk","seq":23,"time":1785730456070,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
-{"type":"assistant/chunk","seq":24,"time":1785730456070,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":25,"time":1785730456070,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_REJECTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"57c0ecaf-3f72-4da9-9eb9-a0726e8f097a"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"}
-{"type":"step/end","seq":26,"time":1785730456071,"data":{"turn":1,"step":2}}
-{"type":"turn/end","seq":27,"time":1785730456071,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357533611,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357533611,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"}]}}
+{"type":"turn/start","seq":2,"time":1786357533611,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357533611,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357533628,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth two"}}
+{"type":"step/start","seq":5,"time":1786357533630,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730456041,"data":{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357533630,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"e9907a11-3d7c-4796-a459-16afd4afde32"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357533630,"data":{"title":"Attempt one subagent call beyond","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730456041,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730456042,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_depth_three_rejected","name":"subagent","argumentsDelta":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}}
+{"type":"assistant/chunk","seq":13,"time":1785498798916,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}}}
+{"type":"assistant/chunk","seq":14,"time":1785730456047,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":15,"time":1785730456047,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":16,"time":1785730456047,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"467433db-5dbf-42ee-94c0-25c011ce711b"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
+{"type":"tool/call","seq":17,"time":1785730456048,"data":{"turn":1,"step":1,"callId":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}
+{"type":"tool/result","seq":18,"time":1785730456056,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_depth_three_rejected"},"content":[{"type":"tool-result","toolCallId":"call_depth_three_rejected","content":[{"type":"text","text":"Error: subagent depth 3 exceeds maxDepth 2"}],"isError":true}],"role":"user","id":"9a3d59f3-542a-4400-a62c-be28dcea3bd1"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
+{"type":"step/end","seq":19,"time":1785730456056,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":20,"time":1785730456066,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":21,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":22,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DEPTH_REJECTED"}}}
+{"type":"assistant/chunk","seq":23,"time":1785498798937,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_REJECTED"}}}}
+{"type":"assistant/chunk","seq":24,"time":1785730456070,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
+{"type":"assistant/chunk","seq":25,"time":1785730456070,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":26,"time":1785730456070,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DEPTH_REJECTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"57c0ecaf-3f72-4da9-9eb9-a0726e8f097a"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[21,22,23,24,25],"surfaceOp":"append"}
+{"type":"step/end","seq":27,"time":1785730456071,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":28,"time":1785730456071,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl
index 6288b6d516..ab699d7d18 100644
--- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl
@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1785821414127,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1784540790308,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785498798839,"data":{"content":[{"type":"text","text":"Delegate through two child generations. The depth-two child must attempt one more subagent call and report the rejection."}],"source":{"kind":"user"},"role":"user","id":"b2260a25-4667-49ed-9297-16b233f22332"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730455980,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d3ba1b18-4d27-4c90-a95d-125e9ffc9f29"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730455980,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"55365caf-6fcc-484b-a4b7-646914654bbb"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730455980,"data":{"title":"Delegate through two child generations.","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785498798841,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730455981,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl
index a56f7ccf60..c663606bbd 100644
--- a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl
@@ -20,21 +20,23 @@
{"type":"step/end","seq":40,"time":1785730448979,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":41,"time":1785730448979,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"session/end-seed","seq":42,"time":1785730449008,"data":{}}
-{"type":"agent/inbox/spliced","seq":43,"time":1785498796160,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"d037163e-ed56-4c9c-b5d1-57df017d618c"}]}}
-{"type":"turn/start","seq":44,"time":1785821406523,"data":{"turn":2}}
-{"type":"agent/inbox/spliced","seq":45,"time":1785821406523,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":46,"time":1785821406543,"data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}}
-{"type":"step/start","seq":47,"time":1785730449027,"data":{"turn":2,"step":1}}
-{"type":"user/message","seq":48,"time":1785730449027,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"d037163e-ed56-4c9c-b5d1-57df017d618c"},"surfaceOp":"append"}
-{"type":"request/header","seq":49,"time":1785730449027,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}}
-{"type":"assistant/chunk","seq":50,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":51,"time0":1783352138046,"data":{"turn":2,"step":1,"index":0,"dt":[0,28,1,0,0,0,0,28,0,0,0,0,28,28,1,0,0,28,0,0,29,0,0,28,1,28,1,0,0,0,0,30,2],"texts":["The"," user"," asked"," me"," to"," remember"," the"," project"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," now"," they","'re"," asking"," what"," it"," is","."," I"," should"," just"," reply"," with"," that"," word","."]}}
-{"type":"assistant/chunk","seq":85,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","seq0":86,"time0":1783352138307,"data":{"turn":2,"step":1,"index":1,"dt":[1790166963,239266980,117223942],"texts":["M","ARM","AL","ADE"]}}
-{"type":"assistant/chunk","seq":90,"time":1785498796192,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."}}}}
-{"type":"assistant/chunk","seq":91,"time":1785498796192,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"MARMALADE"}}}}
-{"type":"assistant/chunk","seq":92,"time":1785730449034,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}}}}
-{"type":"assistant/chunk","seq":93,"time":1785730449034,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":94,"time":1785730449034,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."},{"type":"text","text":"MARMALADE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cdc56e00-c648-4669-92b2-7299e41cb743"},"usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}},"sourceEventSeqs":[50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93],"surfaceOp":"append"}
-{"type":"step/end","seq":95,"time":1785730449035,"data":{"turn":2,"step":1}}
-{"type":"turn/end","seq":96,"time":1785730449035,"data":{"turn":2,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":43,"time":1786357523264,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":44,"time":1786357523265,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"d037163e-ed56-4c9c-b5d1-57df017d618c"}]}}
+{"type":"turn/start","seq":45,"time":1786357523265,"data":{"turn":2}}
+{"type":"agent/inbox/spliced","seq":46,"time":1786357523265,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":47,"time":1786357523283,"data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}}
+{"type":"step/start","seq":48,"time":1786357523286,"data":{"turn":2,"step":1}}
+{"type":"user/message","seq":49,"time":1786357523286,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"d037163e-ed56-4c9c-b5d1-57df017d618c"},"surfaceOp":"append"}
+{"type":"user/message","seq":50,"time":1786358035356,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"257e572f-6f95-48f9-b3d7-4ea8b162f374"},"surfaceOp":"append"}
+{"type":"request/header","seq":51,"time":1786358035356,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}}
+{"type":"assistant/chunk","seq":52,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":53,"time0":1783352138074,"data":{"turn":2,"step":1,"index":0,"dt":[1,0,0,0,0,28,0,0,0,0,28,28,1,0,0,28,0,0,29,0,0,28,1,28,1,0,0,0,0,30,2,0,0],"texts":["The"," user"," asked"," me"," to"," remember"," the"," project"," cod","ew","ord"," \"","M","ARM","AL","ADE","\""," and"," now"," they","'re"," asking"," what"," it"," is","."," I"," should"," just"," reply"," with"," that"," word","."]}}
+{"type":"assistant/chunk","seq":87,"time":1785142305270,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":88,"time0":1785381572250,"data":{"turn":2,"step":1,"index":1,"dt":[117223942,0,0],"texts":["M","ARM","AL","ADE"]}}
+{"type":"assistant/chunk","seq":92,"time":1785730449034,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."}}}}
+{"type":"assistant/chunk","seq":93,"time":1785730449034,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"MARMALADE"}}}}
+{"type":"assistant/chunk","seq":94,"time":1786357523292,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}}}}
+{"type":"assistant/chunk","seq":95,"time":1786358035361,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":96,"time":1786358035361,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."},{"type":"text","text":"MARMALADE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cdc56e00-c648-4669-92b2-7299e41cb743"},"usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}},"sourceEventSeqs":[52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95],"surfaceOp":"append"}
+{"type":"step/end","seq":97,"time":1786358035361,"data":{"turn":2,"step":1}}
+{"type":"turn/end","seq":98,"time":1786358035361,"data":{"turn":2,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/input.json b/examples/acp-agent/tests/snapshots/subagent-list-agents/input.json
index b3b1d8e170..ab0c00f2f2 100644
--- a/examples/acp-agent/tests/snapshots/subagent-list-agents/input.json
+++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/input.json
@@ -14,6 +14,13 @@
"op": "waitForFile",
"path": ".dsh-snapshot-subagent-settled"
},
+ {
+ "op": "waitForTurnStart",
+ "minimumTurn": 2
+ },
+ {
+ "op": "waitForTurnEnd"
+ },
{
"op": "prompt",
"text": "Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."
diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl
index d6e54e2096..d81f1964b2 100644
--- a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl
@@ -1,20 +1,21 @@
{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
{"type":"subagent/descriptor","seq":0,"time":1785531795641,"data":{"version":2,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
{"type":"session/end-seed","seq":1,"time":1785531795641,"data":{}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785730454803,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2a46160e-89d3-433b-bf04-66fb0313abfa"}]}}
-{"type":"turn/start","seq":3,"time":1785821412774,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":4,"time":1785821412774,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","seq":5,"time":1785730454835,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":6,"time":1785730454835,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2a46160e-89d3-433b-bf04-66fb0313abfa"},"surfaceOp":"append"}
-{"type":"user/message","seq":7,"time":1785730454835,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"524be394-8639-4c12-a41d-799b9e0120a1"},"surfaceOp":"append"}
-{"type":"session/title","seq":8,"time":1785730454835,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":9,"time":1785730454835,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":10,"time":1785730454835,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":11,"time":1789000000007,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":12,"time":1789000000008,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_OK"}}}
-{"type":"assistant/chunk","seq":13,"time":1785531795683,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_OK"}}}}
-{"type":"assistant/chunk","seq":14,"time":1785531795683,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":3}}}}
-{"type":"assistant/chunk","seq":15,"time":1785730454843,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":16,"time":1785730454843,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f6a952dd-2d09-4b5c-b8ae-5456cfdfeab0"},"usage":{"inputTokens":10,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
-{"type":"step/end","seq":17,"time":1785730454843,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":18,"time":1785730454844,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":2,"time":1786357532080,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357532080,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2a46160e-89d3-433b-bf04-66fb0313abfa"}]}}
+{"type":"turn/start","seq":4,"time":1786357532081,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":5,"time":1786357532081,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":6,"time":1786357532106,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":7,"time":1785730454835,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2a46160e-89d3-433b-bf04-66fb0313abfa"},"surfaceOp":"append"}
+{"type":"user/message","seq":8,"time":1786357532106,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"5fda3f8d-fbac-4878-a9e3-9953a4e1da09"},"surfaceOp":"append"}
+{"type":"session/title","seq":9,"time":1786357532106,"data":{"title":"Reply with exactly the word","messageSeqs":[7],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":10,"time":1785730454835,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":11,"time":1785730454835,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":12,"time":1789000000007,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":13,"time":1789000000008,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_OK"}}}
+{"type":"assistant/chunk","seq":14,"time":1785531795683,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_OK"}}}}
+{"type":"assistant/chunk","seq":15,"time":1785531795683,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":16,"time":1785730454843,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":17,"time":1785730454843,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f6a952dd-2d09-4b5c-b8ae-5456cfdfeab0"},"usage":{"inputTokens":10,"outputTokens":3}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"}
+{"type":"step/end","seq":18,"time":1785730454843,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":19,"time":1785730454844,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl
index f1deec5af9..44549b2e99 100644
--- a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl
@@ -1,10 +1,10 @@
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"agent/inbox/spliced","seq":0,"time":1785730454756,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"c2febfff-792d-4457-a944-933ff0de0570"}]}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785730454756,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"356b3b62-c8b8-4d2a-84d7-7df1b6e4811e"}]}}
{"type":"turn/start","seq":1,"time":1785821412725,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821412725,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1785730454783,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":4,"time":1785730454783,"data":{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"c2febfff-792d-4457-a944-933ff0de0570"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730454783,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"cc8cb20d-5802-46a9-87b8-d3ee784f8e52"},"surfaceOp":"append"}
+{"type":"user/message","seq":4,"time":1785730454783,"data":{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"356b3b62-c8b8-4d2a-84d7-7df1b6e4811e"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730454783,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"9be42fb0-f0d0-4ab9-a232-fb753f7db482"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730454783,"data":{"title":"Call the subagent tool once","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785730454784,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730454784,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -13,9 +13,9 @@
{"type":"assistant/chunk","seq":11,"time":1785531795632,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}}
{"type":"assistant/chunk","seq":12,"time":1785531795632,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":13,"time":1785730454793,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":14,"time":1785730454793,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4dda49d6-5d02-456f-ba95-68699662793d"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
+{"type":"assistant/message","seq":14,"time":1785730454793,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c8802574-4e43-4ee7-8648-5a132935b5dc"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1785730454793,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
-{"type":"tool/result","seq":16,"time":1785730454804,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"7f1fbe79-98b7-410a-af4d-c40d52a366dc"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
+{"type":"tool/result","seq":16,"time":1785730454804,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"c83395ad-93c6-4899-9ae1-8d29f92d4dde"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1785730454804,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1785730454814,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1789000000017,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
@@ -23,39 +23,42 @@
{"type":"assistant/chunk","seq":21,"time":1785531795656,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}}}
{"type":"assistant/chunk","seq":22,"time":1785531795656,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":23,"time":1785730454820,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":24,"time":1785730454820,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"2ee459fa-21f9-48c6-a42e-3c38eca1e4c9"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
+{"type":"assistant/message","seq":24,"time":1785730454820,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"1fefe87b-4c3c-49b0-860c-8097193f9567"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"step/end","seq":25,"time":1785730454821,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":26,"time":1785730454821,"data":{"turn":1,"reason":{"kind":"completed"}}}
-{"type":"agent/inbox/spliced","seq":27,"time":1785730454857,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"3bcac2a3-e0db-465b-94b4-4e2761236475"}]}}
-{"type":"turn/start","seq":28,"time":1785821412840,"data":{"turn":2}}
-{"type":"agent/inbox/spliced","seq":29,"time":1785821412840,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","seq":30,"time":1785730454863,"data":{"turn":2,"step":1}}
-{"type":"user/message","seq":31,"time":1785730454863,"data":{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"3bcac2a3-e0db-465b-94b4-4e2761236475"},"surfaceOp":"append"}
-{"type":"assistant/chunk","seq":32,"time":1785531795686,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":33,"time":1785536135065,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_list","name":"list_agents","argumentsDelta":"{\"scope\":\"descendants\"}"}}}
-{"type":"assistant/chunk","seq":34,"time":1785730454867,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{\"scope\":\"descendants\"}"}}}}
-{"type":"assistant/chunk","seq":35,"time":1785730454867,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":36,"time":1785730454867,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":37,"time":1785730454867,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{\"scope\":\"descendants\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"199c7794-d782-4957-b7ed-69d094b9c0ef"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
-{"type":"tool/call","seq":38,"time":1785730454867,"data":{"turn":2,"step":1,"callId":"call_list","name":"list_agents","arguments":"{\"scope\":\"descendants\"}"}}
-{"type":"tool/result","seq":39,"time":1785730454893,"data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"33333333-3333-4333-8333-333333333333 [complete] parent=11111111-1111-4111-8111-111111111111 depth=1 — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"61ceb650-3e32-413e-9d7e-b6ec8a351858"}},"sourceEventSeqs":[38],"surfaceOp":"append"}
-{"type":"step/end","seq":40,"time":1785730454893,"data":{"turn":2,"step":1}}
-{"type":"step/start","seq":41,"time":1785730454903,"data":{"turn":2,"step":2}}
-{"type":"assistant/chunk","seq":42,"time":1785531795715,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":43,"time":1785536135106,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_interrupt","name":"interrupt_agent","argumentsDelta":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}}}
-{"type":"assistant/chunk","seq":44,"time":1785730454907,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_interrupt","name":"interrupt_agent","arguments":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}}}}
-{"type":"assistant/chunk","seq":45,"time":1785730454907,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":46,"time":1785730454907,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":47,"time":1785730454907,"data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_interrupt","name":"interrupt_agent","arguments":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"17c1a7c5-84f5-493e-adb5-6a65219e6ad6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[42,43,44,45,46],"surfaceOp":"append"}
-{"type":"tool/call","seq":48,"time":1785730454907,"data":{"turn":2,"step":2,"callId":"call_interrupt","name":"interrupt_agent","arguments":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}}
-{"type":"tool/result","seq":49,"time":1785730454917,"data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_interrupt"},"content":[{"type":"tool-result","toolCallId":"call_interrupt","content":[{"type":"text","text":"interrupt requested for agent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"bf93cbe0-1946-4aef-a4ce-431790026c6f"}},"sourceEventSeqs":[48],"surfaceOp":"append"}
-{"type":"step/end","seq":50,"time":1785730454917,"data":{"turn":2,"step":2}}
-{"type":"step/start","seq":51,"time":1785730454927,"data":{"turn":2,"step":3}}
-{"type":"assistant/chunk","seq":52,"time":1785531795715,"data":{"turn":2,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":53,"time":1785536135106,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
-{"type":"assistant/chunk","seq":54,"time":1785730454907,"data":{"turn":2,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
-{"type":"assistant/chunk","seq":55,"time":1785730454907,"data":{"turn":2,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
-{"type":"assistant/chunk","seq":56,"time":1785730454907,"data":{"turn":2,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":57,"time":1785730454907,"data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e4c04cd6-9c23-4c99-a5f7-bac6fc141e95"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[52,53,54,55,56],"surfaceOp":"append"}
-{"type":"step/end","seq":58,"time":1785730454938,"data":{"turn":2,"step":3}}
-{"type":"turn/end","seq":59,"time":1785730454938,"data":{"turn":2,"reason":{"kind":"completed"}}}
+{"type":"agent/inbox/spliced","seq":27,"time":1786011499211,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"9275a12c-bf9a-48e2-b33b-4fc484e936cb"}]}}
+{"type":"turn/start","seq":28,"time":1786011499211,"data":{"turn":2}}
+{"type":"agent/inbox/spliced","seq":29,"time":1786011499211,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":30,"time":1786012470343,"data":{"turn":2,"step":1}}
+{"type":"user/message","seq":31,"time":1786012470343,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"9275a12c-bf9a-48e2-b33b-4fc484e936cb"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":32,"time":1786012470346,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":33,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
+{"type":"assistant/chunk","seq":34,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
+{"type":"assistant/chunk","seq":35,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":36,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":37,"time":1786012470346,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4620e8c0-dd13-4a2f-87dc-f4b66aa51219"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
+{"type":"step/end","seq":38,"time":1786012470346,"data":{"turn":2,"step":1}}
+{"type":"turn/end","seq":39,"time":1786012470346,"data":{"turn":2,"reason":{"kind":"completed"}}}
+{"type":"agent/inbox/spliced","seq":40,"time":1786012470360,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"7a2a86d0-80a3-4db5-822f-2d3fcbc16e11"}]}}
+{"type":"turn/start","seq":41,"time":1786011499224,"data":{"turn":3}}
+{"type":"agent/inbox/spliced","seq":42,"time":1786011499224,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":43,"time":1786011499234,"data":{"turn":3,"step":1}}
+{"type":"user/message","seq":44,"time":1786011499234,"data":{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"7a2a86d0-80a3-4db5-822f-2d3fcbc16e11"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":45,"time":1786011499238,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":46,"time":1786011499238,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_list","name":"list_agents","argumentsDelta":"{}"}}}
+{"type":"assistant/chunk","seq":47,"time":1785531795715,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}}}
+{"type":"assistant/chunk","seq":48,"time":1785536135106,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":49,"time":1785730454907,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":50,"time":1786011499238,"data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"d3402e92-2f7e-4cd5-9537-ae9beedeecab"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"}
+{"type":"tool/call","seq":51,"time":1786011499238,"data":{"turn":3,"step":1,"callId":"call_list","name":"list_agents","arguments":"{}"}}
+{"type":"tool/result","seq":52,"time":1786011499267,"data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"33333333-3333-4333-8333-333333333333 [ready] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"8ae233de-8fde-48d7-a9d0-0d9a480a00d0"}},"sourceEventSeqs":[51],"surfaceOp":"append"}
+{"type":"step/end","seq":53,"time":1785730454908,"data":{"turn":3,"step":1}}
+{"type":"step/start","seq":54,"time":1786011499275,"data":{"turn":3,"step":2}}
+{"type":"assistant/chunk","seq":55,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":56,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
+{"type":"assistant/chunk","seq":57,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
+{"type":"assistant/chunk","seq":58,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
+{"type":"assistant/chunk","seq":59,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":60,"time":1786011499279,"data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3ac0f29f-72ae-44fb-9414-974470095618"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"}
+{"type":"step/end","seq":61,"time":1786011499279,"data":{"turn":3,"step":2}}
+{"type":"turn/end","seq":62,"time":1786011499279,"data":{"turn":3,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-list-agents/stdout.expected.jsonl
index 5a9b409384..4813c19f90 100644
--- a/examples/acp-agent/tests/snapshots/subagent-list-agents/stdout.expected.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/stdout.expected.jsonl
@@ -2,5 +2,6 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"STARTED"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/system-prompt.1.expected.md b/examples/acp-agent/tests/snapshots/subagent-list-agents/system-prompt.1.expected.md
new file mode 100644
index 0000000000..71f4ed05f8
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/system-prompt.1.expected.md
@@ -0,0 +1,24 @@
+You are an AI agent powered by the DeepSeek Harness SDK.
+
+You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+Verify your work by running the code or tests. Keep answers brief and factual.
+
+
+Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.
+
+Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.
+
+Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.
+
+Check the [exit code: N] marker on every bash result; investigate failures before moving on.
+
+Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
+
+Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
+
+Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.
+
+Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
+
+Deliver your result with the report tool before you finish: call it once with a self-contained answer. The agent that started you shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so a closing remark such as "done" leaves it nothing it can use. Report earlier as well whenever a partial finding changes what that agent should do next; reporting never ends your turn.
diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json
index 7dd791cf27..253fbc4a32 100644
--- a/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json
+++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/tool-schemas.1.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -194,13 +194,13 @@
},
{
"name": "report",
- "description": "Report selected content to the agent that started you. Call this zero or more times for progress, findings, or a final answer. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.",
+ "description": "Report selected content to the agent that started you. Call this once before you finish, with a self-contained final result, and earlier for progress or findings that change what that agent does next. That agent shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so finishing your work is not itself a result. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.",
"parameters": {
"type": "object",
"properties": {
"output": {
"type": "string",
- "description": "Self-contained content for your parent; it does not see your private work."
+ "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
}
},
"required": [
@@ -247,7 +247,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -261,7 +261,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -272,7 +272,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -283,10 +283,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/input.json b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/input.json
new file mode 100644
index 0000000000..640bf92f7f
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/input.json
@@ -0,0 +1,14 @@
+{
+ "steps": [
+ {
+ "op": "initialize"
+ },
+ {
+ "op": "newSession"
+ },
+ {
+ "op": "prompt",
+ "text": "Use the subagent tool exactly once to delegate this subtask: \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\" After the subagent returns, reply with the single word PARENT_DONE and stop."
+ }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.1.jsonl
new file mode 100644
index 0000000000..d1f5447275
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.1.jsonl
@@ -0,0 +1,31 @@
+{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":2,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
+{"type":"approval/policy","seq":0,"time":1786373921809,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786373921809,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Write the words 'partial one', call todo_write once, then keep going until you are cut off."}],"source":{"kind":"user"},"role":"user","id":"dbf0670a-79cc-4e2c-a298-c4d804e6fe61"}]}}
+{"type":"turn/start","seq":2,"time":1786373921810,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786373921810,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786373921831,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Truncated child"}}
+{"type":"step/start","seq":5,"time":1786373921836,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1786348800142,"data":{"content":[{"type":"text","text":"Write the words 'partial one', call todo_write once, then keep going until you are cut off."}],"source":{"kind":"user"},"role":"user","id":"dbf0670a-79cc-4e2c-a298-c4d804e6fe61"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786373921837,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"885ea744-63dd-4198-95be-267b9db94a57"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786373921837,"data":{"title":"Write the words 'partial one',","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1786348800142,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1786348800142,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":12,"time":1786348800146,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"partial one"}}}}
+{"type":"assistant/chunk","seq":13,"time":1786348800146,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":14,"time":1786348800146,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_child_1","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"keep going\", \"status\": \"in_progress\"}]}"}}}}
+{"type":"assistant/chunk","seq":15,"time":1786348800146,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":9}}}}
+{"type":"assistant/chunk","seq":16,"time":1786348800146,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":17,"time":1786348800146,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"partial one"},{"type":"tool-call","id":"call_child_1","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"keep going\", \"status\": \"in_progress\"}]}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5e4d07b2-6ce2-4ab6-8be0-fbdf2d3af138"},"usage":{"inputTokens":20,"outputTokens":9}},"sourceEventSeqs":[11,12,13,14,15,16],"surfaceOp":"append"}
+{"type":"tool/call","seq":18,"time":1786348800146,"data":{"turn":1,"step":1,"callId":"call_child_1","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"keep going\", \"status\": \"in_progress\"}]}"}}
+{"type":"todo/write","seq":19,"time":1786348800150,"data":{"todos":[{"content":"keep going","status":"in_progress"}]}}
+{"type":"tool/result","seq":20,"time":1786348800151,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_child_1"},"content":[{"type":"tool-result","toolCallId":"call_child_1","content":[{"type":"text","text":"Updated todo list: 0 pending, 1 in progress, 0 completed."}],"isError":false}],"role":"user","id":"67efbbf3-ca1e-4d23-8f19-940cb391ff1e"}},"sourceEventSeqs":[18],"surfaceOp":"append"}
+{"type":"step/end","seq":21,"time":1786348800151,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":22,"time":1786348800156,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":23,"time":1786348800160,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":24,"time":1786348800160,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_child_2","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"keep going\", \"status\": \"completed\"}]}"}}}}
+{"type":"assistant/chunk","seq":25,"time":1786348800160,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}}}
+{"type":"assistant/chunk","seq":26,"time":1786348800160,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"max-tokens"}}}}
+{"type":"assistant/message","seq":27,"time":1786348800160,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"bb92e4ec-f260-4415-9782-b71147ea378d"},"usage":{"inputTokens":30,"outputTokens":4}},"sourceEventSeqs":[23,24,25,26],"surfaceOp":"append"}
+{"type":"step/end","seq":28,"time":1786348800160,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":29,"time":1786348800160,"data":{"turn":1,"reason":{"kind":"max-tokens"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.jsonl
new file mode 100644
index 0000000000..f07b01589d
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/session.jsonl
@@ -0,0 +1,26 @@
+{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"agent/inbox/spliced","seq":0,"time":1786348800078,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the subagent tool exactly once to delegate this subtask: \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\" After the subagent returns, reply with the single word PARENT_DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"8787ce07-4f1f-4368-bf58-18e30484ed44"}]}}
+{"type":"turn/start","seq":1,"time":1786348800079,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":2,"time":1786348800079,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":3,"time":1786348800114,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1786348800114,"data":{"content":[{"type":"text","text":"Use the subagent tool exactly once to delegate this subtask: \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\" After the subagent returns, reply with the single word PARENT_DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"8787ce07-4f1f-4368-bf58-18e30484ed44"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1786348800114,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"32a8b2ce-f1f9-411b-940d-c80f772561ac"},"surfaceOp":"append"}
+{"type":"session/title","seq":6,"time":1786348800114,"data":{"title":"Use the subagent tool exactly","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":7,"time":1786348800115,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":8,"time":1786348800115,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":9,"time":1786348800120,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":10,"time":1786348800120,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\"}"}}}}
+{"type":"assistant/chunk","seq":11,"time":1786348800120,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":12,"time":1786348800120,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":13,"time":1786348800120,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f4269cd2-9132-4b68-8f9b-ff3a40321bc9"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12],"surfaceOp":"append"}
+{"type":"tool/call","seq":14,"time":1786348800121,"data":{"turn":1,"step":1,"callId":"call_parent_1","name":"subagent","arguments":"{\"description\": \"Truncated child\", \"prompt\": \"Write the words 'partial one', call todo_write once, then keep going until you are cut off.\"}"}}
+{"type":"tool/result","seq":15,"time":1786348800163,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parent_1"},"content":[{"type":"tool-result","toolCallId":"call_parent_1","content":[{"type":"text","text":"Error: subagent run hit its token limit before finishing\nPartial output before the run ended:\npartial one"}],"isError":true}],"role":"user","id":"5dd34050-a533-4f1b-99ee-5fc62c6a4502"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
+{"type":"step/end","seq":16,"time":1786348800163,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":17,"time":1786348800169,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":18,"time":1786348800173,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":19,"time":1786348800173,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}}}
+{"type":"assistant/chunk","seq":20,"time":1786348800173,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2}}}}
+{"type":"assistant/chunk","seq":21,"time":1786348800173,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":22,"time":1786348800173,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"fb14560d-1d98-4b18-8736-b079de400315"},"usage":{"inputTokens":12,"outputTokens":2}},"sourceEventSeqs":[18,19,20,21],"surfaceOp":"append"}
+{"type":"step/end","seq":23,"time":1786348800173,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":24,"time":1786348800173,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/stdout.expected.jsonl
new file mode 100644
index 0000000000..a460e019d4
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-max-tokens-partial/stdout.expected.jsonl
@@ -0,0 +1,4 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"PARENT_DONE"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl
index e275607cbf..ca5b3abd3a 100644
--- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl
@@ -1,22 +1,23 @@
{"type":"session","version":0,"id":"e4aafa18-b9e3-48d0-8aae-6c9b25dcae80","createdAt":1783352145223,"cwd":"{{cwd}}","parentSession":"959ffdf5-03e2-465e-9482-009b704632dc","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498797416,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"}]}}
-{"type":"turn/start","seq":1,"time":1785821407754,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821407754,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821407767,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply ALPHA only"}}
-{"type":"step/start","seq":4,"time":1785730450187,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730450187,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730450187,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4a5a7c59-b6f8-47b0-8c09-d9a05607deac"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730450187,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730450187,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730450188,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":1783352146014,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":11,"time0":1783352146042,"data":{"turn":1,"step":1,"index":0,"dt":[1,0,0,0,28,0,0,0,0,0,29,0,0,0,0,29,0,0],"texts":["The"," user"," asked"," me"," to"," reply"," with"," exactly"," the"," word"," \"","AL","P","HA","\""," and"," nothing"," else","."]}}
-{"type":"assistant/chunk","seq":30,"time":1783352146129,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","seq0":31,"time0":1783352146129,"data":{"turn":1,"step":1,"index":1,"dt":[0,0],"texts":["AL","P","HA"]}}
-{"type":"assistant/chunk","seq":34,"time":1783352146129,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to reply with exactly the word \"ALPHA\" and nothing else."}}}}
-{"type":"assistant/chunk","seq":35,"time":1785498797444,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"ALPHA"}}}}
-{"type":"assistant/chunk","seq":36,"time":1785730450194,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":48,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}}}}
-{"type":"assistant/chunk","seq":37,"time":1785730450194,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":38,"time":1785730450194,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user asked me to reply with exactly the word \"ALPHA\" and nothing else."},{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cfff210d-8dd3-4acc-bbc3-fa860baf88cf"},"usage":{"inputTokens":48,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}},"sourceEventSeqs":[10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"surfaceOp":"append"}
-{"type":"step/end","seq":39,"time":1785730450194,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":40,"time":1785730450195,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357524735,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357524735,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"}]}}
+{"type":"turn/start","seq":2,"time":1786357524735,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357524735,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357524752,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply ALPHA only"}}
+{"type":"step/start","seq":5,"time":1786357524755,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730450187,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357524755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"cb073ca7-b412-4509-99ef-4e1351e7e74d"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357524755,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730450187,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730450188,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1783352146014,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":12,"time0":1783352146042,"data":{"turn":1,"step":1,"index":0,"dt":[1,0,0,0,28,0,0,0,0,0,29,0,0,0,0,29,0,0],"texts":["The"," user"," asked"," me"," to"," reply"," with"," exactly"," the"," word"," \"","AL","P","HA","\""," and"," nothing"," else","."]}}
+{"type":"assistant/chunk","seq":31,"time":1783352146129,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":32,"time0":1783352146129,"data":{"turn":1,"step":1,"index":1,"dt":[0,0],"texts":["AL","P","HA"]}}
+{"type":"assistant/chunk","seq":35,"time":1783352146129,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to reply with exactly the word \"ALPHA\" and nothing else."}}}}
+{"type":"assistant/chunk","seq":36,"time":1785498797444,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"ALPHA"}}}}
+{"type":"assistant/chunk","seq":37,"time":1785730450194,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":48,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}}}}
+{"type":"assistant/chunk","seq":38,"time":1785730450194,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":39,"time":1785730450194,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user asked me to reply with exactly the word \"ALPHA\" and nothing else."},{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cfff210d-8dd3-4acc-bbc3-fa860baf88cf"},"usage":{"inputTokens":48,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}},"sourceEventSeqs":[11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"surfaceOp":"append"}
+{"type":"step/end","seq":40,"time":1785730450194,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":41,"time":1785730450195,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl
index fb6e5e0971..d846a2bcd8 100644
--- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl
@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1785821407687,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1783352142835,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785498797379,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is SAFFRON. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3d1ea7cb-c273-4c38-a765-5ff256eaaf51"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"473ecf9e-52c4-4db2-be56-1c8f7fa7d932"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e0a9678e-ff95-49f4-b4f7-4ace69a670a3"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730450135,"data":{"title":"Remember this fact for later:","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785498797380,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730450136,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -20,21 +20,23 @@
{"type":"step/end","seq":34,"time":1785730450146,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":35,"time":1785730450146,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"session/end-seed","seq":36,"time":1785730450227,"data":{}}
-{"type":"agent/inbox/spliced","seq":37,"time":1785498797482,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"}]}}
-{"type":"turn/start","seq":38,"time":1785821407808,"data":{"turn":2}}
-{"type":"agent/inbox/spliced","seq":39,"time":1785821407808,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":40,"time":1785821407826,"data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}}
-{"type":"step/start","seq":41,"time":1785730450246,"data":{"turn":2,"step":1}}
-{"type":"user/message","seq":42,"time":1785730450246,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"},"surfaceOp":"append"}
-{"type":"request/header","seq":43,"time":1785730450247,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}}
-{"type":"assistant/chunk","seq":44,"time":1783352148076,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":45,"time0":1783352148076,"data":{"turn":2,"step":1,"index":0,"dt":[1,0,0,0,29,0,0,0,35,0,0,0,0,26,29,31,0,30,0,0,27,1,27,0,1,0,0,31,1,0],"texts":["The"," user"," is"," asking"," me"," to"," recall"," the"," project"," cod","ew","ord"," that"," was"," mentioned"," earlier"," in"," the"," conversation","."," I"," was"," told"," to"," remember"," it",":"," SA","FF","RON","."]}}
-{"type":"assistant/chunk","seq":76,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","seq0":77,"time0":1785142306309,"data":{"turn":2,"step":1,"index":1,"dt":[239267243,117223959],"texts":["SA","FF","RON"]}}
-{"type":"assistant/chunk","seq":80,"time":1785498797511,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."}}}}
-{"type":"assistant/chunk","seq":81,"time":1785498797511,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SAFFRON"}}}}
-{"type":"assistant/chunk","seq":82,"time":1785730450254,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}}}}
-{"type":"assistant/chunk","seq":83,"time":1785730450254,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":84,"time":1785730450254,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."},{"type":"text","text":"SAFFRON"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e1f347c1-ce65-4ca9-8a9e-05e4366ef365"},"usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}},"sourceEventSeqs":[44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83],"surfaceOp":"append"}
-{"type":"step/end","seq":85,"time":1785730450254,"data":{"turn":2,"step":1}}
-{"type":"turn/end","seq":86,"time":1785730450254,"data":{"turn":2,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":37,"time":1786357524782,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":38,"time":1786357524783,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"}]}}
+{"type":"turn/start","seq":39,"time":1786357524783,"data":{"turn":2}}
+{"type":"agent/inbox/spliced","seq":40,"time":1786357524783,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":41,"time":1786357524800,"data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}}
+{"type":"step/start","seq":42,"time":1786357524803,"data":{"turn":2,"step":1}}
+{"type":"user/message","seq":43,"time":1786357524803,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"},"surfaceOp":"append"}
+{"type":"user/message","seq":44,"time":1786358036899,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"d5ebfdfe-de5f-4f3d-9772-7d6179fe8c3d"},"surfaceOp":"append"}
+{"type":"request/header","seq":45,"time":1786358036900,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}}
+{"type":"assistant/chunk","seq":46,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":47,"time0":1783352148077,"data":{"turn":2,"step":1,"index":0,"dt":[0,0,29,0,0,0,35,0,0,0,0,26,29,31,0,30,0,0,27,1,27,0,1,0,0,31,1,0,0,1790157964],"texts":["The"," user"," is"," asking"," me"," to"," recall"," the"," project"," cod","ew","ord"," that"," was"," mentioned"," earlier"," in"," the"," conversation","."," I"," was"," told"," to"," remember"," it",":"," SA","FF","RON","."]}}
+{"type":"assistant/chunk","seq":78,"time":1785381573552,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":79,"time0":1785498797511,"data":{"turn":2,"step":1,"index":1,"dt":[0,0],"texts":["SA","FF","RON"]}}
+{"type":"assistant/chunk","seq":82,"time":1785730450254,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."}}}}
+{"type":"assistant/chunk","seq":83,"time":1785730450254,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SAFFRON"}}}}
+{"type":"assistant/chunk","seq":84,"time":1786357524808,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}}}}
+{"type":"assistant/chunk","seq":85,"time":1786358036906,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":86,"time":1786358036906,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."},{"type":"text","text":"SAFFRON"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e1f347c1-ce65-4ca9-8a9e-05e4366ef365"},"usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}},"sourceEventSeqs":[46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85],"surfaceOp":"append"}
+{"type":"step/end","seq":87,"time":1786358036906,"data":{"turn":2,"step":1}}
+{"type":"turn/end","seq":88,"time":1786358036906,"data":{"turn":2,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl
index fa390cf176..4963b5275b 100644
--- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl
@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1785821407687,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1783352142835,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785498797379,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is SAFFRON. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3d1ea7cb-c273-4c38-a765-5ff256eaaf51"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"473ecf9e-52c4-4db2-be56-1c8f7fa7d932"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e0a9678e-ff95-49f4-b4f7-4ace69a670a3"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730450135,"data":{"title":"Remember this fact for later:","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785498797380,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730450136,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl
index 7948013736..45fbf5e48b 100644
--- a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl
@@ -1,22 +1,23 @@
{"type":"session","version":0,"id":"553f8e92-aac1-4df3-8657-eacbb58f9581","createdAt":1783352127669,"cwd":"{{cwd}}","parentSession":"14dda109-5728-45ba-a002-7db9543fe50e","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498794788,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"}]}}
-{"type":"turn/start","seq":1,"time":1785821405232,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821405232,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821405245,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return ALPHA only"}}
-{"type":"step/start","seq":4,"time":1785730447828,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730447828,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730447828,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"bab5cdff-7925-478d-b55a-daa2ef524d7c"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730447828,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730447828,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730447828,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":1783352128280,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":11,"time0":1783352128280,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,1,19,0,0,0,0,1,31,0,0,0,0,32,1,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","AL","P","HA","\""," and"," nothing"," else","."]}}
-{"type":"assistant/chunk","seq":30,"time":1783352128365,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","seq0":31,"time0":1783352128365,"data":{"turn":1,"step":1,"index":1,"dt":[0,0],"texts":["AL","P","HA"]}}
-{"type":"assistant/chunk","seq":34,"time":1783352128365,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ALPHA\" and nothing else."}}}}
-{"type":"assistant/chunk","seq":35,"time":1785498794825,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"ALPHA"}}}}
-{"type":"assistant/chunk","seq":36,"time":1785730447834,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":49,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}}}}
-{"type":"assistant/chunk","seq":37,"time":1785730447834,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":38,"time":1785730447834,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ALPHA\" and nothing else."},{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5f1e6087-da72-4a56-9bc0-ae1ac6618a8a"},"usage":{"inputTokens":49,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}},"sourceEventSeqs":[10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"surfaceOp":"append"}
-{"type":"step/end","seq":39,"time":1785730447834,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":40,"time":1785730447834,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357521737,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357521737,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"}]}}
+{"type":"turn/start","seq":2,"time":1786357521737,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357521737,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357521754,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return ALPHA only"}}
+{"type":"step/start","seq":5,"time":1786357521756,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730447828,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357521756,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"6f423049-484c-48d6-9cdd-db504d79f892"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357521756,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730447828,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730447828,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1783352128280,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":12,"time0":1783352128280,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,1,19,0,0,0,0,1,31,0,0,0,0,32,1,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","AL","P","HA","\""," and"," nothing"," else","."]}}
+{"type":"assistant/chunk","seq":31,"time":1783352128365,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":32,"time0":1783352128365,"data":{"turn":1,"step":1,"index":1,"dt":[0,0],"texts":["AL","P","HA"]}}
+{"type":"assistant/chunk","seq":35,"time":1783352128365,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ALPHA\" and nothing else."}}}}
+{"type":"assistant/chunk","seq":36,"time":1785498794825,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"ALPHA"}}}}
+{"type":"assistant/chunk","seq":37,"time":1785730447834,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":49,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}}}}
+{"type":"assistant/chunk","seq":38,"time":1785730447834,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":39,"time":1785730447834,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ALPHA\" and nothing else."},{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5f1e6087-da72-4a56-9bc0-ae1ac6618a8a"},"usage":{"inputTokens":49,"outputTokens":23,"cacheReadTokens":2816,"reasoningTokens":19}},"sourceEventSeqs":[11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"surfaceOp":"append"}
+{"type":"step/end","seq":40,"time":1785730447834,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":41,"time":1785730447834,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl
index 64e58b3741..7b390e04d4 100644
--- a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl
@@ -1,23 +1,24 @@
{"type":"session","version":0,"id":"5f49e80c-16fc-42c7-a617-0b6bd0680aa3","createdAt":1783352129662,"cwd":"{{cwd}}","parentSession":"14dda109-5728-45ba-a002-7db9543fe50e","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498794853,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"}]}}
-{"type":"turn/start","seq":1,"time":1785821405286,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821405286,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821405299,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return BETA only"}}
-{"type":"step/start","seq":4,"time":1785730447881,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730447881,"data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730447881,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"036067ef-a106-4955-841c-a0d2effe51ef"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730447881,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730447881,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730447881,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":1783352130413,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":11,"time0":1783352130413,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,35,0,0,0,0,0,36,0,0,0,0,0,43],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","B","ETA","\""," and"," nothing"," else","."]}}
-{"type":"assistant/chunk","seq":29,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":30,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"B"}}}
-{"type":"assistant/chunk","seq":31,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ETA"}}}
-{"type":"assistant/chunk","seq":32,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"BETA\" and nothing else."}}}}
-{"type":"assistant/chunk","seq":33,"time":1785498794882,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"BETA"}}}}
-{"type":"assistant/chunk","seq":34,"time":1785730447887,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}}}}
-{"type":"assistant/chunk","seq":35,"time":1785730447887,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":36,"time":1785730447887,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"BETA\" and nothing else."},{"type":"text","text":"BETA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"adc4527d-efd1-4c89-b42b-826c33f2bb12"},"usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}},"sourceEventSeqs":[10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35],"surfaceOp":"append"}
-{"type":"step/end","seq":37,"time":1785730447887,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":38,"time":1785730447887,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357521782,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357521783,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"}]}}
+{"type":"turn/start","seq":2,"time":1786357521783,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357521783,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357521799,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return BETA only"}}
+{"type":"step/start","seq":5,"time":1786357521801,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730447881,"data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357521802,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"5832009f-12a3-4f6e-85e5-257c06f4c716"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357521802,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730447881,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730447881,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1783352130413,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":12,"time0":1783352130413,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,35,0,0,0,0,0,36,0,0,0,0,0,43],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","B","ETA","\""," and"," nothing"," else","."]}}
+{"type":"assistant/chunk","seq":30,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":31,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"B"}}}
+{"type":"assistant/chunk","seq":32,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ETA"}}}
+{"type":"assistant/chunk","seq":33,"time":1783352130527,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"BETA\" and nothing else."}}}}
+{"type":"assistant/chunk","seq":34,"time":1785498794882,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"BETA"}}}}
+{"type":"assistant/chunk","seq":35,"time":1785730447887,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}}}}
+{"type":"assistant/chunk","seq":36,"time":1785730447887,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":37,"time":1785730447887,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"BETA\" and nothing else."},{"type":"text","text":"BETA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"adc4527d-efd1-4c89-b42b-826c33f2bb12"},"usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}},"sourceEventSeqs":[11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"surfaceOp":"append"}
+{"type":"step/end","seq":38,"time":1785730447887,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":39,"time":1785730447887,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl
index 3a48c2d760..a1337e166b 100644
--- a/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl
@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1785821405184,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1783352126252,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785498794765,"data":{"content":[{"type":"text","text":"Use the subagent tool TWICE, once at a time, to delegate two subtasks to child agents. First subtask: 'Reply with exactly the word ALPHA and nothing else.' Second subtask (after the first returns): 'Reply with exactly the word BETA and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"07bf16df-0499-420d-9510-3204061f0122"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730447790,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"9b4b262d-cbd7-4cd8-b24b-70b2b401b0fe"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730447790,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"50a1100d-448e-41f2-8f99-39be199db492"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730447790,"data":{"title":"Use the subagent tool TWICE,","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785498794766,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730447791,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-parallel/input.json b/examples/acp-agent/tests/snapshots/subagent-parallel/input.json
new file mode 100644
index 0000000000..86e72676ee
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-parallel/input.json
@@ -0,0 +1,7 @@
+{
+ "steps": [
+ { "op": "initialize" },
+ { "op": "newSession" },
+ { "op": "prompt", "text": "Use the subagent tool TWICE in the SAME assistant message (two parallel tool calls in one response), each delegating the identical subtask: 'Reply with exactly the word ALPHA and nothing else.' Give both calls the description 'Say the word ALPHA'. After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool." }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/subagent-parallel/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-parallel/session.1.jsonl
new file mode 100644
index 0000000000..d5f1733ce0
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-parallel/session.1.jsonl
@@ -0,0 +1,18 @@
+{"type":"session","version":0,"id":"bbbbbbbb-0000-4000-8000-000000000002","createdAt":1783352127000,"cwd":"{{cwd}}","parentSession":"aaaaaaaa-0000-4000-8000-000000000001","origin":"subagent","delegationDepth":1}
+{"type":"approval/policy","seq":0,"time":1786373947132,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786373947134,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"84b2bc8e-878b-4337-ae82-26c3074069f2"}]}}
+{"type":"turn/start","seq":2,"time":1786373947134,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786373947134,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786373947165,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Say the word ALPHA"}}
+{"type":"step/start","seq":5,"time":1786373947168,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1786338530759,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"84b2bc8e-878b-4337-ae82-26c3074069f2"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786373947168,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"05fbd821-3adf-4877-b805-2cc5e6de3f9c"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786373947168,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1786338530759,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1786338530759,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1786338530769,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":12,"time":1786338530769,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ALPHA"}}}}
+{"type":"assistant/chunk","seq":13,"time":1786338530769,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":14,"time":1786338530769,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"224c83f1-4449-451d-8f8a-f2889c19068e"}},"sourceEventSeqs":[11,12,13],"surfaceOp":"append"}
+{"type":"step/end","seq":15,"time":1786338530769,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":16,"time":1786338530769,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-parallel/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-parallel/session.2.jsonl
new file mode 100644
index 0000000000..aea0d83108
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-parallel/session.2.jsonl
@@ -0,0 +1,18 @@
+{"type":"session","version":0,"id":"cccccccc-0000-4000-8000-000000000003","createdAt":1783352127001,"cwd":"{{cwd}}","parentSession":"aaaaaaaa-0000-4000-8000-000000000001","origin":"subagent","delegationDepth":1}
+{"type":"approval/policy","seq":0,"time":1786373947132,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786373947133,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a3da85dc-6428-4a31-8653-b7327e518436"}]}}
+{"type":"turn/start","seq":2,"time":1786373947133,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786373947134,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786373947170,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Say the word ALPHA"}}
+{"type":"step/start","seq":5,"time":1786373947173,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1786338530749,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a3da85dc-6428-4a31-8653-b7327e518436"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786373947174,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"670f5fc4-3eb6-4430-959a-08e8adf25036"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786373947174,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1786338530749,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1786338530749,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1786338530759,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":12,"time":1786338530759,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ALPHA"}}}}
+{"type":"assistant/chunk","seq":13,"time":1786338530759,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":14,"time":1786338530759,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ALPHA"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ac2dc91f-7dfa-45f6-88e0-21a2e762c3dd"}},"sourceEventSeqs":[11,12,13],"surfaceOp":"append"}
+{"type":"step/end","seq":15,"time":1786338530760,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":16,"time":1786338530760,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-parallel/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-parallel/session.jsonl
new file mode 100644
index 0000000000..7e2d0e05bb
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-parallel/session.jsonl
@@ -0,0 +1,28 @@
+{"type":"session","version":0,"id":"aaaaaaaa-0000-4000-8000-000000000001","createdAt":1783352126000,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"agent/inbox/spliced","seq":0,"time":1786338530687,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the subagent tool TWICE in the SAME assistant message (two parallel tool calls in one response), each delegating the identical subtask: 'Reply with exactly the word ALPHA and nothing else.' Give both calls the description 'Say the word ALPHA'. After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"02062dd0-83d4-4b40-ab23-2fbcb0a8be96"}]}}
+{"type":"turn/start","seq":1,"time":1786338530688,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":2,"time":1786338530688,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":3,"time":1786338530716,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1786338530716,"data":{"content":[{"type":"text","text":"Use the subagent tool TWICE in the SAME assistant message (two parallel tool calls in one response), each delegating the identical subtask: 'Reply with exactly the word ALPHA and nothing else.' Give both calls the description 'Say the word ALPHA'. After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"02062dd0-83d4-4b40-ab23-2fbcb0a8be96"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1786338530716,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"2dd192ab-72ed-4c20-a487-40aa14bd5c07"},"surfaceOp":"append"}
+{"type":"session/title","seq":6,"time":1786338530716,"data":{"title":"Use the subagent tool TWICE","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":7,"time":1786338530717,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":8,"time":1786338530717,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":9,"time":1786338530722,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":10,"time":1786338530722,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}}}}
+{"type":"assistant/chunk","seq":11,"time":1786338530722,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":12,"time":1786338530722,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}}}}
+{"type":"assistant/chunk","seq":13,"time":1786338530722,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":14,"time":1786338530723,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"},{"type":"tool-call","id":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"6ff33634-55af-4c37-a491-dd5b8673923f"}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
+{"type":"tool/call","seq":15,"time":1786338530723,"data":{"turn":1,"step":1,"callId":"call_parallel_alpha_1","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}}
+{"type":"tool/call","seq":16,"time":1786338530723,"data":{"turn":1,"step":1,"callId":"call_parallel_alpha_2","name":"subagent","arguments":"{\"description\": \"Say the word ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}}
+{"type":"tool/result","seq":17,"time":1786338530770,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_1"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_1","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"9fa4fa59-7326-4bc8-8ea2-120b37d91ea3"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
+{"type":"tool/result","seq":18,"time":1786338530770,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_parallel_alpha_2"},"content":[{"type":"tool-result","toolCallId":"call_parallel_alpha_2","content":[{"type":"text","text":"ALPHA"}],"isError":false}],"role":"user","id":"895bb620-534e-4857-9743-68273126eccf"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
+{"type":"step/end","seq":19,"time":1786338530770,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":20,"time":1786338530778,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":21,"time":1786338530782,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":22,"time":1786338530782,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_DONE"}}}}
+{"type":"assistant/chunk","seq":23,"time":1786338530782,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":24,"time":1786338530782,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"84586dd8-4985-4286-ab4b-fa9965803fb8"}},"sourceEventSeqs":[21,22,23],"surfaceOp":"append"}
+{"type":"step/end","seq":25,"time":1786338530782,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":26,"time":1786338530782,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-parallel/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-parallel/stdout.expected.jsonl
new file mode 100644
index 0000000000..a460e019d4
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-parallel/stdout.expected.jsonl
@@ -0,0 +1,4 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"PARENT_DONE"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.1.jsonl
new file mode 100644
index 0000000000..a0433f1290
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.1.jsonl
@@ -0,0 +1,2 @@
+{"type":"session","version":0,"id":"eb69342c-62b6-4320-a78b-961745f89333","createdAt":1786358409171,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
+{"type":"approval/policy","seq":0,"time":1786358409171,"data":{"policy":"never","source":"delegation"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-report/input.json b/examples/acp-agent/tests/snapshots/subagent-report/input.json
index b15e0e4d71..15ca4b53eb 100644
--- a/examples/acp-agent/tests/snapshots/subagent-report/input.json
+++ b/examples/acp-agent/tests/snapshots/subagent-report/input.json
@@ -14,6 +14,13 @@
{
"op": "waitForSubagentTurnEnd"
},
+ {
+ "op": "waitForTurnStart",
+ "minimumTurn": 2
+ },
+ {
+ "op": "waitForTurnEnd"
+ },
{
"op": "promptAndWaitForAgentMessage",
"text": "Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools.",
diff --git a/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl
index b9992f1519..b83358f93d 100644
--- a/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl
@@ -1,30 +1,31 @@
{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1789000001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
{"type":"subagent/descriptor","seq":0,"time":1785594881508,"data":{"version":2,"mode":"continuable","provider":"spawn","label":"Report a finding","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
{"type":"session/end-seed","seq":1,"time":1785594881508,"data":{}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785730453612,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call the report tool once with output exactly CHILD_REPORT_OK, then stop."}],"source":{"kind":"user"},"role":"user","id":"9045ac78-393a-4f24-b20d-8999286dd6ce"}]}}
-{"type":"turn/start","seq":3,"time":1785821411475,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":4,"time":1785821411475,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","seq":5,"time":1785730453639,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":6,"time":1785730453639,"data":{"content":[{"type":"text","text":"Call the report tool once with output exactly CHILD_REPORT_OK, then stop."}],"source":{"kind":"user"},"role":"user","id":"9045ac78-393a-4f24-b20d-8999286dd6ce"},"surfaceOp":"append"}
-{"type":"user/message","seq":7,"time":1785730453639,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"67c76a21-6142-45a6-9a49-0485f51edc8d"},"surfaceOp":"append"}
-{"type":"session/title","seq":8,"time":1785730453639,"data":{"title":"Call the report tool once","messageSeqs":[6],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":9,"time":1785730453639,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":10,"time":1785730453639,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":11,"time":1785594881546,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":12,"time":1785594881546,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_report_1","name":"report","argumentsDelta":"{\"output\": \"CHILD_REPORT_OK\"}"}}}
-{"type":"assistant/chunk","seq":13,"time":1789000001010,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}}}}
-{"type":"assistant/chunk","seq":14,"time":1789000001011,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":15,"time":1785730453647,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":16,"time":1785730453647,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c9e50afb-b732-41ab-b0fc-8e98948ad9ec"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
-{"type":"tool/call","seq":17,"time":1785730453647,"data":{"turn":1,"step":1,"callId":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}}
-{"type":"tool/result","seq":18,"time":1785730453654,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_report_1"},"content":[{"type":"tool-result","toolCallId":"call_report_1","content":[{"type":"text","text":"report accepted by the agent that started you as message 824dc60a-f9d7-48ea-a0d4-6d56df83bd4f"}],"isError":false}],"role":"user","id":"e6764773-c667-40b5-a13f-8bdc5a9c7762"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
-{"type":"step/end","seq":19,"time":1785730453654,"data":{"turn":1,"step":1}}
-{"type":"step/start","seq":20,"time":1785730453664,"data":{"turn":1,"step":2}}
-{"type":"assistant/chunk","seq":21,"time":1785594881567,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":22,"time":1785594881567,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"Reported."}}}
-{"type":"assistant/chunk","seq":23,"time":1789000001020,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Reported."}}}}
-{"type":"assistant/chunk","seq":24,"time":1789000001021,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":25,"time":1785730453668,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":26,"time":1785730453668,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"Reported."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"96784835-2d0f-4d00-aef5-ee3a14820dd1"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[21,22,23,24,25],"surfaceOp":"append"}
-{"type":"step/end","seq":27,"time":1785730453668,"data":{"turn":1,"step":2}}
-{"type":"turn/end","seq":28,"time":1785730453668,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":2,"time":1786374158805,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786374158806,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call the report tool once with output exactly CHILD_REPORT_OK, then stop."}],"source":{"kind":"user"},"role":"user","id":"9045ac78-393a-4f24-b20d-8999286dd6ce"}]}}
+{"type":"turn/start","seq":4,"time":1786374158806,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":5,"time":1786374158806,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":6,"time":1786374158840,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":7,"time":1785730453639,"data":{"content":[{"type":"text","text":"Call the report tool once with output exactly CHILD_REPORT_OK, then stop."}],"source":{"kind":"user"},"role":"user","id":"9045ac78-393a-4f24-b20d-8999286dd6ce"},"surfaceOp":"append"}
+{"type":"user/message","seq":8,"time":1786374158840,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"0f107d71-9b56-4ad8-b6f1-d93cb4c82105"},"surfaceOp":"append"}
+{"type":"session/title","seq":9,"time":1786374158840,"data":{"title":"Call the report tool once","messageSeqs":[7],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":10,"time":1785730453639,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":11,"time":1785730453639,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":12,"time":1785594881546,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":13,"time":1785594881546,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_report_1","name":"report","argumentsDelta":"{\"output\": \"CHILD_REPORT_OK\"}"}}}
+{"type":"assistant/chunk","seq":14,"time":1789000001010,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}}}}
+{"type":"assistant/chunk","seq":15,"time":1789000001011,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":16,"time":1785730453647,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":17,"time":1785730453647,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c9e50afb-b732-41ab-b0fc-8e98948ad9ec"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"}
+{"type":"tool/call","seq":18,"time":1785730453647,"data":{"turn":1,"step":1,"callId":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}}
+{"type":"tool/result","seq":19,"time":1785730453654,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_report_1"},"content":[{"type":"tool-result","toolCallId":"call_report_1","content":[{"type":"text","text":"report accepted by the agent that started you as message 87627538-d804-4d36-bb10-4768b6fcfb65"}],"isError":false}],"role":"user","id":"22f25be2-d3f9-4558-b3ea-db22fa900aa3"}},"sourceEventSeqs":[18],"surfaceOp":"append"}
+{"type":"step/end","seq":20,"time":1785730453654,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":21,"time":1785730453664,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":22,"time":1785594881567,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":23,"time":1785594881567,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"Reported."}}}
+{"type":"assistant/chunk","seq":24,"time":1789000001020,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Reported."}}}}
+{"type":"assistant/chunk","seq":25,"time":1789000001021,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":26,"time":1785730453668,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":27,"time":1785730453668,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"Reported."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"96784835-2d0f-4d00-aef5-ee3a14820dd1"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[22,23,24,25,26],"surfaceOp":"append"}
+{"type":"step/end","seq":28,"time":1785730453668,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":29,"time":1785730453668,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl
index db5cd4a77f..f8b5deef29 100644
--- a/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl
@@ -1,10 +1,10 @@
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"agent/inbox/spliced","seq":0,"time":1785730453561,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"5cf78378-e004-4fd5-af4f-cef3b7e190ad"}]}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785730453561,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"b765ae32-73e2-4625-81ba-01095f8c83d0"}]}}
{"type":"turn/start","seq":1,"time":1785821411429,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821411429,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1785730453591,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":4,"time":1785730453591,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"5cf78378-e004-4fd5-af4f-cef3b7e190ad"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730453592,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"77141070-eb99-4ec0-908d-646c387982f6"},"surfaceOp":"append"}
+{"type":"user/message","seq":4,"time":1785730453591,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"b765ae32-73e2-4625-81ba-01095f8c83d0"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730453592,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"1f4f5888-2068-4df0-904f-12ffb4aa3321"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730453592,"data":{"title":"Follow these steps exactly, then","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785730453592,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730453593,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -13,9 +13,9 @@
{"type":"assistant/chunk","seq":11,"time":1785501592851,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}}}}
{"type":"assistant/chunk","seq":12,"time":1785501592851,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":13,"time":1785730453601,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":14,"time":1785730453602,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"664c39ff-9dac-4bb3-a151-e18a7863d15a"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
+{"type":"assistant/message","seq":14,"time":1785730453602,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"97b897d5-0d01-4a6c-ad0c-4776c61c9c68"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1785730453602,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}}
-{"type":"tool/result","seq":16,"time":1785730453613,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"d7927ad2-29db-4de8-ab4d-59a4ebcddd72"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
+{"type":"tool/result","seq":16,"time":1785730453613,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"91fb94ce-cf3e-47ed-ab20-8f46cf4aec55"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1785730453613,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1785730453623,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1789000000018,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
@@ -23,22 +23,35 @@
{"type":"assistant/chunk","seq":21,"time":1785501592877,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}}}
{"type":"assistant/chunk","seq":22,"time":1785501592877,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":23,"time":1785730453628,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":24,"time":1785730453628,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"571faad7-adbd-480c-922a-1499e1329ead"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
+{"type":"assistant/message","seq":24,"time":1785730453628,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"b1b1cf78-11a8-4440-b9f9-2096d15e7884"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"step/end","seq":25,"time":1785730453628,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":26,"time":1785730453629,"data":{"turn":1,"reason":{"kind":"completed"}}}
-{"type":"agent/inbox/spliced","seq":27,"time":1785730453654,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"824dc60a-f9d7-48ea-a0d4-6d56df83bd4f"}]}}
-{"type":"agent/inbox/spliced","seq":28,"time":1785730453673,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"43f17984-22c3-48b9-911e-923a2f68dce0"}]}}
-{"type":"turn/start","seq":29,"time":1785821411548,"data":{"turn":2}}
-{"type":"agent/inbox/spliced","seq":30,"time":1785730453673,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","seq":31,"time":1785821411548,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","seq":32,"time":1785730453683,"data":{"turn":2,"step":1}}
-{"type":"user/message","seq":33,"time":1785730453683,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"824dc60a-f9d7-48ea-a0d4-6d56df83bd4f"},"surfaceOp":"append"}
-{"type":"user/message","seq":34,"time":1785730453683,"data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"43f17984-22c3-48b9-911e-923a2f68dce0"},"surfaceOp":"append"}
-{"type":"assistant/chunk","seq":35,"time":1785730453687,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":36,"time":1785730453687,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_REPORT_OK"}}}
-{"type":"assistant/chunk","seq":37,"time":1785730453687,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_REPORT_OK"}}}}
-{"type":"assistant/chunk","seq":38,"time":1785730453687,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":39,"time":1785730453687,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":40,"time":1785730453687,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"77bba235-d2b6-4a32-9ba3-ebb69d9b0654"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[35,36,37,38,39],"surfaceOp":"append"}
-{"type":"step/end","seq":41,"time":1785730453687,"data":{"turn":2,"step":1}}
-{"type":"turn/end","seq":42,"time":1785730453687,"data":{"turn":2,"reason":{"kind":"completed"}}}
+{"type":"agent/inbox/spliced","seq":27,"time":1786008565468,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"ec024a7a-5506-4ebf-a9d8-82ce01dc88b4"}]}}
+{"type":"agent/inbox/spliced","seq":28,"time":1786012334062,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Reported."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"08101cc1-abde-49ca-9745-1d075a3911b5"}]}}
+{"type":"turn/start","seq":29,"time":1786012334062,"data":{"turn":2}}
+{"type":"agent/inbox/spliced","seq":30,"time":1786012334062,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
+{"type":"agent/inbox/spliced","seq":31,"time":1786012334062,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":32,"time":1786376144953,"data":{"turn":2,"step":1}}
+{"type":"user/message","seq":33,"time":1786376144953,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"ec024a7a-5506-4ebf-a9d8-82ce01dc88b4"},"surfaceOp":"append"}
+{"type":"user/message","seq":34,"time":1786012334068,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Reported."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"08101cc1-abde-49ca-9745-1d075a3911b5"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":35,"time":1786376144959,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":36,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
+{"type":"assistant/chunk","seq":37,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
+{"type":"assistant/chunk","seq":38,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":39,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":40,"time":1786376144959,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c6fecd6e-033f-4be8-98ee-cc0733b18c83"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[35,36,37,38,39],"surfaceOp":"append"}
+{"type":"step/end","seq":41,"time":1786376144960,"data":{"turn":2,"step":1}}
+{"type":"turn/end","seq":42,"time":1786376144960,"data":{"turn":2,"reason":{"kind":"completed"}}}
+{"type":"agent/inbox/spliced","seq":43,"time":1786376145165,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"6c0b0e51-4ad9-4c4b-bbbc-508973862b77"}]}}
+{"type":"turn/start","seq":44,"time":1786012334073,"data":{"turn":3}}
+{"type":"agent/inbox/spliced","seq":45,"time":1786012334073,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":46,"time":1786012334082,"data":{"turn":3,"step":1}}
+{"type":"user/message","seq":47,"time":1786012334083,"data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"6c0b0e51-4ad9-4c4b-bbbc-508973862b77"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":48,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":49,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_REPORT_OK"}}}
+{"type":"assistant/chunk","seq":50,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_REPORT_OK"}}}}
+{"type":"assistant/chunk","seq":51,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":52,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":53,"time":1786012334087,"data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"98d44266-6695-482b-910c-0e1e570fe7a6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[48,49,50,51,52],"surfaceOp":"append"}
+{"type":"step/end","seq":54,"time":1786012334087,"data":{"turn":3,"step":1}}
+{"type":"turn/end","seq":55,"time":1786012334087,"data":{"turn":3,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl
index 09cd3e4a68..785ee28128 100644
--- a/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl
@@ -2,5 +2,6 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"STARTED"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"CHILD_REPORT_OK"}}}}
{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-report/system-prompt.1.expected.md b/examples/acp-agent/tests/snapshots/subagent-report/system-prompt.1.expected.md
new file mode 100644
index 0000000000..71f4ed05f8
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/subagent-report/system-prompt.1.expected.md
@@ -0,0 +1,24 @@
+You are an AI agent powered by the DeepSeek Harness SDK.
+
+You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
+
+Verify your work by running the code or tests. Keep answers brief and factual.
+
+
+Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.
+
+Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.
+
+Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.
+
+Check the [exit code: N] marker on every bash result; investigate failures before moving on.
+
+Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
+
+Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
+
+Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.
+
+Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
+
+Deliver your result with the report tool before you finish: call it once with a self-contained answer. The agent that started you shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so a closing remark such as "done" leaves it nothing it can use. Report earlier as well whenever a partial finding changes what that agent should do next; reporting never ends your turn.
diff --git a/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json b/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json
index 7dd791cf27..253fbc4a32 100644
--- a/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json
+++ b/examples/acp-agent/tests/snapshots/subagent-report/tool-schemas.1.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -194,13 +194,13 @@
},
{
"name": "report",
- "description": "Report selected content to the agent that started you. Call this zero or more times for progress, findings, or a final answer. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.",
+ "description": "Report selected content to the agent that started you. Call this once before you finish, with a self-contained final result, and earlier for progress or findings that change what that agent does next. That agent shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so finishing your work is not itself a result. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.",
"parameters": {
"type": "object",
"properties": {
"output": {
"type": "string",
- "description": "Self-contained content for your parent; it does not see your private work."
+ "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
}
},
"required": [
@@ -247,7 +247,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -261,7 +261,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -272,7 +272,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -283,10 +283,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl
index 7fa229c2e3..5eb0455932 100644
--- a/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl
@@ -1,22 +1,23 @@
{"type":"session","version":0,"id":"ea339828-7885-42e1-9083-4355e6f1708d","createdAt":1783352120855,"cwd":"{{cwd}}","parentSession":"5138ed0d-e86e-4a7d-b75b-803307e92b17","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498793648,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"54ed23d6-e960-4f36-b192-cf06e1618ea6"}]}}
-{"type":"turn/start","seq":1,"time":1785821404007,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821404007,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821404020,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply with CHILD_OK"}}
-{"type":"step/start","seq":4,"time":1785730446720,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730446720,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"54ed23d6-e960-4f36-b192-cf06e1618ea6"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730446720,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"1b537017-6493-4f52-8504-01a7384e8cc6"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730446720,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730446720,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730446721,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":1783352121663,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":11,"time0":1783352121664,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,27,0,0,29,0,0,27,0,0,0,0,1],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else","."]}}
-{"type":"assistant/chunk","seq":28,"time":1783352121777,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","seq0":29,"time0":1783352121777,"data":{"turn":1,"step":1,"index":1,"dt":[0,0],"texts":["CH","ILD","_OK"]}}
-{"type":"assistant/chunk","seq":32,"time":1783352121777,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word CHILD_OK and nothing else."}}}}
-{"type":"assistant/chunk","seq":33,"time":1785498793670,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CHILD_OK"}}}}
-{"type":"assistant/chunk","seq":34,"time":1785730446727,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":17}}}}
-{"type":"assistant/chunk","seq":35,"time":1785730446727,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":36,"time":1785730446727,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"16118fc6-2262-476e-9a4a-4b533cff09bc"},"usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":17}},"sourceEventSeqs":[10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35],"surfaceOp":"append"}
-{"type":"step/end","seq":37,"time":1785730446727,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":38,"time":1785730446727,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357520283,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357520283,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"54ed23d6-e960-4f36-b192-cf06e1618ea6"}]}}
+{"type":"turn/start","seq":2,"time":1786357520283,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357520283,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357520300,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply with CHILD_OK"}}
+{"type":"step/start","seq":5,"time":1786357520303,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730446720,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"54ed23d6-e960-4f36-b192-cf06e1618ea6"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357520303,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"24630f5a-f790-469f-96a6-cf234ded3759"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357520303,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730446720,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730446721,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1783352121663,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":12,"time0":1783352121664,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,27,0,0,29,0,0,27,0,0,0,0,1],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," CH","ILD","_OK"," and"," nothing"," else","."]}}
+{"type":"assistant/chunk","seq":29,"time":1783352121777,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":30,"time0":1783352121777,"data":{"turn":1,"step":1,"index":1,"dt":[0,0],"texts":["CH","ILD","_OK"]}}
+{"type":"assistant/chunk","seq":33,"time":1783352121777,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word CHILD_OK and nothing else."}}}}
+{"type":"assistant/chunk","seq":34,"time":1785498793670,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CHILD_OK"}}}}
+{"type":"assistant/chunk","seq":35,"time":1785730446727,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":17}}}}
+{"type":"assistant/chunk","seq":36,"time":1785730446727,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":37,"time":1785730446727,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"16118fc6-2262-476e-9a4a-4b533cff09bc"},"usage":{"inputTokens":48,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":17}},"sourceEventSeqs":[11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"surfaceOp":"append"}
+{"type":"step/end","seq":38,"time":1785730446727,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":39,"time":1785730446727,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl
index edf8950dac..0ac1be7454 100644
--- a/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl
@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1785821403947,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1783352119275,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785498793625,"data":{"content":[{"type":"text","text":"Use the subagent tool exactly once to delegate this subtask to a child agent: 'Reply with exactly the word CHILD_OK and nothing else.' After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"a9485ebd-2b4a-434a-bc35-afd757ce141b"},"surfaceOp":"append"}
-{"type":"user/message","seq":5,"time":1785730446685,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e40b1354-1856-48c3-a638-1be67af32920"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730446685,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"bfd99a70-ad54-4073-9c0d-8a63711fe34a"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730446685,"data":{"title":"Use the subagent tool exactly","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785498793626,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730446686,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
diff --git a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json
index e0b7a41bcf..a2629a3153 100644
--- a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -256,7 +256,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -267,10 +267,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json
index 97c51af14b..f11d079c63 100644
--- a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json
+++ b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json
@@ -133,7 +133,7 @@
},
{
"name": "list_agents",
- "description": "List your continuable background subagents by durable id and label. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and complete means it exists only in storage — a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
+ "description": "List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.",
"parameters": {
"type": "object",
"properties": {
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
- "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
+ "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -256,7 +256,7 @@
},
{
"name": "subagent_fork",
- "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
+ "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"parameters": {
"type": "object",
"properties": {
@@ -267,10 +267,6 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
- },
- "run_in_background": {
- "type": "boolean",
- "description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
}
},
"required": [
diff --git a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl
index 7b77a09f5e..080198e7bc 100644
--- a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl
+++ b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl
@@ -1,22 +1,23 @@
{"type":"session","version":0,"id":"583a4db2-3350-436c-b4a5-5615fd159052","createdAt":1783600636316,"cwd":"{{cwd}}","parentSession":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498800317,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"f0f46771-663a-494a-8d40-6866a5bbe7c9"}]}}
-{"type":"turn/start","seq":1,"time":1785821416523,"data":{"turn":1}}
-{"type":"agent/inbox/spliced","seq":2,"time":1785821416523,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"subagent/descriptor","seq":3,"time":1785821416542,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
-{"type":"step/start","seq":4,"time":1785730457309,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730457309,"data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"f0f46771-663a-494a-8d40-6866a5bbe7c9"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730457309,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e076edc0-a2bf-4fc6-aa58-d44bf1e8fd00"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730457309,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":8,"time":1785730457310,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","seq":9,"time":1785730457310,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","seq":10,"time":1783600638189,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","seq0":11,"time0":1783600638189,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,24,0,0,0,0,29,0,0,0,0,0,34,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","WF","_CH","ILD","_OK","\""," and"," nothing"," else","."]}}
-{"type":"assistant/chunk","seq":29,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","seq0":30,"time0":1783600638276,"data":{"turn":1,"step":1,"index":1,"dt":[4,0,0],"texts":["WF","_CH","ILD","_OK"]}}
-{"type":"assistant/chunk","seq":34,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."}}}}
-{"type":"assistant/chunk","seq":35,"time":1785498800343,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WF_CHILD_OK"}}}}
-{"type":"assistant/chunk","seq":36,"time":1785730457316,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}}}}
-{"type":"assistant/chunk","seq":37,"time":1785730457316,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":38,"time":1785730457316,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."},{"type":"text","text":"WF_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0ddaf3d1-53dc-45df-bc19-54ad72d6d7fb"},"usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}},"sourceEventSeqs":[10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"surfaceOp":"append"}
-{"type":"step/end","seq":39,"time":1785730457316,"data":{"turn":1,"step":1}}
-{"type":"turn/end","seq":40,"time":1785730457316,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"approval/policy","seq":0,"time":1786357536718,"data":{"policy":"never","source":"delegation"}}
+{"type":"agent/inbox/spliced","seq":1,"time":1786357536719,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"f0f46771-663a-494a-8d40-6866a5bbe7c9"}]}}
+{"type":"turn/start","seq":2,"time":1786357536719,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":3,"time":1786357536719,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"subagent/descriptor","seq":4,"time":1786357536736,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
+{"type":"step/start","seq":5,"time":1786357536738,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":6,"time":1785730457309,"data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"f0f46771-663a-494a-8d40-6866a5bbe7c9"},"surfaceOp":"append"}
+{"type":"user/message","seq":7,"time":1786357536738,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"12bbd4dd-4040-4cc7-8acf-e526144f1ee5"},"surfaceOp":"append"}
+{"type":"session/title","seq":8,"time":1786357536738,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":9,"time":1785730457310,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":10,"time":1785730457310,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"assistant/chunk","seq":11,"time":1783600638189,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":12,"time0":1783600638189,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,24,0,0,0,0,29,0,0,0,0,0,34,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","WF","_CH","ILD","_OK","\""," and"," nothing"," else","."]}}
+{"type":"assistant/chunk","seq":30,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":31,"time0":1783600638276,"data":{"turn":1,"step":1,"index":1,"dt":[4,0,0],"texts":["WF","_CH","ILD","_OK"]}}
+{"type":"assistant/chunk","seq":35,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."}}}}
+{"type":"assistant/chunk","seq":36,"time":1785498800343,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WF_CHILD_OK"}}}}
+{"type":"assistant/chunk","seq":37,"time":1785730457316,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}}}}
+{"type":"assistant/chunk","seq":38,"time":1785730457316,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":39,"time":1785730457316,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."},{"type":"text","text":"WF_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0ddaf3d1-53dc-45df-bc19-54ad72d6d7fb"},"usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}},"sourceEventSeqs":[11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"surfaceOp":"append"}
+{"type":"step/end","seq":40,"time":1785730457316,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":41,"time":1785730457316,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/web.cordis.snapshot.yml b/examples/acp-agent/web.cordis.snapshot.yml
index f0da7617b0..facd66f76c 100644
--- a/examples/acp-agent/web.cordis.snapshot.yml
+++ b/examples/acp-agent/web.cordis.snapshot.yml
@@ -2,7 +2,7 @@
# fixture server stay real (the tool call re-executes the actual HTTP fetch and
# markdown rendering); only the model adapter is replaced by replay.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/web.cordis.yml b/examples/acp-agent/web.cordis.yml
index 1ed0b3efba..63ce043340 100644
--- a/examples/acp-agent/web.cordis.yml
+++ b/examples/acp-agent/web.cordis.yml
@@ -4,7 +4,7 @@
# fixture server the scenario prompt fetches — deterministic content, no
# external network, in recording and replay alike.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/workspace-context.cordis.snapshot.yml b/examples/acp-agent/workspace-context.cordis.snapshot.yml
index 6abaaa3f13..0215128de1 100644
--- a/examples/acp-agent/workspace-context.cordis.snapshot.yml
+++ b/examples/acp-agent/workspace-context.cordis.snapshot.yml
@@ -2,7 +2,7 @@
# compose across includes, so this applies the scenario config and model swap
# directly to the live tree.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/acp-agent/workspace-context.cordis.yml b/examples/acp-agent/workspace-context.cordis.yml
index 34562e7324..db9fb85353 100644
--- a/examples/acp-agent/workspace-context.cordis.yml
+++ b/examples/acp-agent/workspace-context.cordis.yml
@@ -2,7 +2,7 @@
# discovery inside the scenario's temporary cwd. The app config patch replaces
# the whole base config, so the base fields are restated verbatim.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/README.i18n.yaml b/examples/headless-agent/README.i18n.yaml
index 90dd1c4f2a..e965ffee3a 100644
--- a/examples/headless-agent/README.i18n.yaml
+++ b/examples/headless-agent/README.i18n.yaml
@@ -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 examples/headless-agent/README.md
-README.md: f12a56920c79f3a7e257c4e56163f323e4312d11
-README.zh.md: 9e409735f03afc62cd788fa2a5d1afdef0fa6c2a
+README.md: 2f854924d4bfd2bf66b3d6f47433136098d7b762
+README.zh.md: 4128645da8a0f8a84b17d819fc614051b68f2a11
diff --git a/examples/headless-agent/README.md b/examples/headless-agent/README.md
index f12a56920c..2f854924d4 100644
--- a/examples/headless-agent/README.md
+++ b/examples/headless-agent/README.md
@@ -10,10 +10,10 @@ This directory owns the replay and real-model test composition for a headless co
# repo root .env (gitignored) or exported env:
# DEEPSEEK_API_KEY=sk-…
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
-pnpm run dsh run "fix the failing test in this workspace"
+pnpm dsh --profile headless "fix the failing test in this workspace"
```
-The product command is [`dsh run`](../../apps/cli/README.md): it accepts one nonblank task, creates and persists a fresh session, prints the final assistant text, and exits. The root `demo:headless` script is only an alias of that command.
+The product command is [`dsh --profile headless`](../../apps/cli/README.md): it accepts one nonblank task, creates and persists a fresh session, prints the final assistant text, and exits.
Snapshot suites run this directory's configuration through [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts), an unexported test-only process that emits canonical session events as JSONL before its result record. That stream is test infrastructure, not a supported CLI output format. Child sessions surface only through parent tool events and results.
diff --git a/examples/headless-agent/README.zh.md b/examples/headless-agent/README.zh.md
index 9e409735f0..4128645da8 100644
--- a/examples/headless-agent/README.zh.md
+++ b/examples/headless-agent/README.zh.md
@@ -10,10 +10,10 @@
# repo root .env (gitignored) or exported env:
# DEEPSEEK_API_KEY=sk-…
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
-pnpm run dsh run "fix the failing test in this workspace"
+pnpm dsh --profile headless "fix the failing test in this workspace"
```
-产品命令是 [`dsh run`](../../apps/cli/README.md):它接受一项非空任务,创建并持久化新会话,打印最终 assistant 文本,然后退出。根目录的 `demo:headless` 脚本只是该命令的别名。
+产品命令是 [`dsh --profile headless`](../../apps/cli/README.md):它接受一项非空任务,创建并持久化新会话,打印最终 assistant 文本,然后退出。
快照套件通过 [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts) 运行本目录的配置。这个未导出且仅供测试使用的进程会在结果记录之前,以 JSONL 发出规范会话事件。该事件流属于测试基础设施,不是受支持的 CLI(命令行界面)输出格式。子会话只通过父会话的工具事件和结果对外显示。
diff --git a/examples/headless-agent/advanced.cordis.snapshot.yml b/examples/headless-agent/advanced.cordis.snapshot.yml
index 881a976e33..6ce18953cc 100644
--- a/examples/headless-agent/advanced.cordis.snapshot.yml
+++ b/examples/headless-agent/advanced.cordis.snapshot.yml
@@ -8,7 +8,7 @@
# disables the key-requiring DeepSeek adapter and inserts `llm-replay` to serve
# recorded JSONL without a key or network.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/advanced.cordis.yml b/examples/headless-agent/advanced.cordis.yml
index 09ecec6ea2..ab0bd18d6d 100644
--- a/examples/headless-agent/advanced.cordis.yml
+++ b/examples/headless-agent/advanced.cordis.yml
@@ -1,6 +1,6 @@
# Add Code Mode and Cordis tools to the headless spawn/workflow stack.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/compaction.cordis.snapshot.yml b/examples/headless-agent/compaction.cordis.snapshot.yml
index 13515cf5cc..56431c6e37 100644
--- a/examples/headless-agent/compaction.cordis.snapshot.yml
+++ b/examples/headless-agent/compaction.cordis.snapshot.yml
@@ -1,6 +1,6 @@
# Keyless context-overflow composition for the assembled compaction snapshot.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/cordis.yml b/examples/headless-agent/cordis.yml
index b6f1b774f2..61a0190142 100644
--- a/examples/headless-agent/cordis.yml
+++ b/examples/headless-agent/cordis.yml
@@ -118,12 +118,16 @@
backgroundMode: continuable
maxDepth: 1
+# Fork stays one-shot because a continuable child's `report` tool and prompt
+# section precede the inherited history a fork reuses; `run_in_background` is off
+# because this example mounts no task service. See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md.
- id: tool-subagent-fork
name: '@deepseek-ai/dsh-tool-subagent'
config:
provider: fork
toolName: subagent_fork
- backgroundMode: continuable
+ backgroundMode: one-shot
+ enableRunInBackground: false
maxDepth: 1
# The worker-thread workflow engine fans a model-written JavaScript script's
diff --git a/examples/headless-agent/credentials.cordis.snapshot.yml b/examples/headless-agent/credentials.cordis.snapshot.yml
index 3a8638089a..7f1cb2b1f2 100644
--- a/examples/headless-agent/credentials.cordis.snapshot.yml
+++ b/examples/headless-agent/credentials.cordis.snapshot.yml
@@ -3,7 +3,7 @@
# the deepseek-official route still registers — so the prompt fails with the actionable
# MISSING_CREDENTIAL guidance this snapshot pins as first-run UX.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/e2b.cordis.yml b/examples/headless-agent/e2b.cordis.yml
index 61bdbc8aa7..3c48a397ad 100644
--- a/examples/headless-agent/e2b.cordis.yml
+++ b/examples/headless-agent/e2b.cordis.yml
@@ -8,7 +8,7 @@
# falls back to /home/user/workspace while Bash and PTY keep targeting the
# host path, so every tool call fails with a remote spawn error.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./advanced.cordis.yml
patches:
diff --git a/examples/headless-agent/goal.cordis.snapshot.yml b/examples/headless-agent/goal.cordis.snapshot.yml
index f6eeeb05ec..00c34e272e 100644
--- a/examples/headless-agent/goal.cordis.snapshot.yml
+++ b/examples/headless-agent/goal.cordis.snapshot.yml
@@ -2,7 +2,7 @@
# a config patch cannot target an entry behind a nested include, then restates
# the goal overlay while replacing the live model with keyless replay.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/goal.cordis.yml b/examples/headless-agent/goal.cordis.yml
index 8f8cdf9e0b..fe84de7bf5 100644
--- a/examples/headless-agent/goal.cordis.yml
+++ b/examples/headless-agent/goal.cordis.yml
@@ -1,6 +1,6 @@
# Add the persisted goal domain and its model-facing tools to the real one-shot app.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/pty.cordis.snapshot.yml b/examples/headless-agent/pty.cordis.snapshot.yml
index 49f3a95afb..3fb20ff468 100644
--- a/examples/headless-agent/pty.cordis.snapshot.yml
+++ b/examples/headless-agent/pty.cordis.snapshot.yml
@@ -1,6 +1,6 @@
# Keyless opt-in PTY composition for the headless stream-json snapshot.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/ralph.cordis.snapshot.yml b/examples/headless-agent/ralph.cordis.snapshot.yml
index e84bdfed31..87e5619cde 100644
--- a/examples/headless-agent/ralph.cordis.snapshot.yml
+++ b/examples/headless-agent/ralph.cordis.snapshot.yml
@@ -1,6 +1,6 @@
# Replay counterpart to cordis.yml for the shipped Ralph-loop snapshot.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/retry.cordis.snapshot.yml b/examples/headless-agent/retry.cordis.snapshot.yml
index b8fc79d17f..30a67f45f6 100644
--- a/examples/headless-agent/retry.cordis.snapshot.yml
+++ b/examples/headless-agent/retry.cordis.snapshot.yml
@@ -1,6 +1,6 @@
# Keyless provider-retry composition for the headless stream-json snapshot.
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
diff --git a/examples/headless-agent/subagent-settlement.cordis.snapshot.yml b/examples/headless-agent/subagent-settlement.cordis.snapshot.yml
new file mode 100644
index 0000000000..4812d676a2
--- /dev/null
+++ b/examples/headless-agent/subagent-settlement.cordis.snapshot.yml
@@ -0,0 +1,21 @@
+# Keyless assembled-app coverage for continuable child settlement delivery. The
+# replay child deliberately never calls report; the parent can reach its final
+# answer only if the continuation manager places the child's closing message in
+# the parent turn without list_agents, send_message, or a Task collector.
+
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+ - insert:
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+
+# Prevent platform scheduling from choosing a streamed-chunk interleave. The
+# fence releases only after the real manager notice enters the parent inbox.
+- id: settlement-fence
+ name: './tests/fixtures/subagent-settlement-fence.ts'
diff --git a/examples/headless-agent/tests/code-mode.e2e.ts b/examples/headless-agent/tests/code-mode.e2e.ts
index ad049ab308..d360f7083d 100644
--- a/examples/headless-agent/tests/code-mode.e2e.ts
+++ b/examples/headless-agent/tests/code-mode.e2e.ts
@@ -2,7 +2,7 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { Context } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, HarnessError } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
diff --git a/examples/headless-agent/tests/coding-task.e2e.ts b/examples/headless-agent/tests/coding-task.e2e.ts
index 75b10ee2bc..42b87eff8a 100644
--- a/examples/headless-agent/tests/coding-task.e2e.ts
+++ b/examples/headless-agent/tests/coding-task.e2e.ts
@@ -4,7 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { codingHarness, finalText, SYSTEM_PROMPT, waitForIdle } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
diff --git a/examples/headless-agent/tests/compaction.e2e.ts b/examples/headless-agent/tests/compaction.e2e.ts
index 35a9a1627e..94dd510b84 100644
--- a/examples/headless-agent/tests/compaction.e2e.ts
+++ b/examples/headless-agent/tests/compaction.e2e.ts
@@ -3,7 +3,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { codingHarness, finalText, SYSTEM_PROMPT, waitForIdle } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
diff --git a/examples/headless-agent/tests/fixtures/cli-mock-llm.ts b/examples/headless-agent/tests/fixtures/cli-mock-llm.ts
index b0b120ecb2..57cb384138 100644
--- a/examples/headless-agent/tests/fixtures/cli-mock-llm.ts
+++ b/examples/headless-agent/tests/fixtures/cli-mock-llm.ts
@@ -1,4 +1,4 @@
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import {
CallId,
LlmAdapter,
diff --git a/examples/headless-agent/tests/fixtures/cli.cordis.yml b/examples/headless-agent/tests/fixtures/cli.cordis.yml
index 21a26bcd73..3106467857 100644
--- a/examples/headless-agent/tests/fixtures/cli.cordis.yml
+++ b/examples/headless-agent/tests/fixtures/cli.cordis.yml
@@ -1,11 +1,8 @@
- id: cli-mock-llm
name: './cli-mock-llm.ts'
-- id: repository-plugin-fixture
- name: './repository-plugin/load.mjs'
-
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ../../cordis.yml
patches:
diff --git a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml
index 930363cf37..fab8ae1b7b 100644
--- a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml
+++ b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml
@@ -1,5 +1,5 @@
- id: base
- name: '@cordisjs/plugin-include'
+ name: '@deepseek-ai/cordis-plugin-include'
config:
path: ../../cordis.yml
patches:
diff --git a/examples/headless-agent/tests/fixtures/goal-domain/seed-goal.ts b/examples/headless-agent/tests/fixtures/goal-domain/seed-goal.ts
index d8dc2c2465..9776438079 100644
--- a/examples/headless-agent/tests/fixtures/goal-domain/seed-goal.ts
+++ b/examples/headless-agent/tests/fixtures/goal-domain/seed-goal.ts
@@ -1,6 +1,6 @@
/** Test-only Loader plugin that creates a goal at the first real step edge. */
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-goal'
export const name = 'seed-goal'
diff --git a/examples/headless-agent/tests/fixtures/headless-driver.ts b/examples/headless-agent/tests/fixtures/headless-driver.ts
index 88d7d73b67..8020129bfb 100644
--- a/examples/headless-agent/tests/fixtures/headless-driver.ts
+++ b/examples/headless-agent/tests/fixtures/headless-driver.ts
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/** Snapshot-only Loader driver: stream one fixture turn as canonical JSONL. */
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
diff --git a/examples/headless-agent/tests/fixtures/dsh-run.cordis.yml b/examples/headless-agent/tests/fixtures/headless-profile.cordis.yml
similarity index 100%
rename from examples/headless-agent/tests/fixtures/dsh-run.cordis.yml
rename to examples/headless-agent/tests/fixtures/headless-profile.cordis.yml
diff --git a/examples/headless-agent/tests/fixtures/repository-plugin/dsh-plugin-assets/skills/0/repository-fixture/SKILL.md b/examples/headless-agent/tests/fixtures/repository-plugin/dsh-plugin-assets/skills/0/repository-fixture/SKILL.md
deleted file mode 100644
index e24104e79f..0000000000
--- a/examples/headless-agent/tests/fixtures/repository-plugin/dsh-plugin-assets/skills/0/repository-fixture/SKILL.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-name: repository-fixture
-description: Repository fixture skill.
----
-
-Static instructions from a prepared repository plugin.
diff --git a/examples/headless-agent/tests/fixtures/repository-plugin/dsh-plugin.mjs b/examples/headless-agent/tests/fixtures/repository-plugin/dsh-plugin.mjs
deleted file mode 100644
index 29aa97c5ec..0000000000
--- a/examples/headless-agent/tests/fixtures/repository-plugin/dsh-plugin.mjs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by dsh-plugin-prepare. Do not edit.
-const manifest = {"name":"headless-repository-fixture","skills":["dsh-plugin-assets/skills/0"]}
-// Value mirror: Cordis const enum FiberState.ACTIVE; keep aligned with dsh-repository-plugin source.ts.
-const FIBER_ACTIVE = 2
-export const name = "headless-repository-fixture"
-export const inject = ["loader","skills"]
-async function mount(ctx, plugin, label, config) {
- const fiber = ctx.plugin(plugin, config)
- await fiber
- if (fiber.state !== FIBER_ACTIVE) {
- const missing = Object.keys(fiber.inject).filter(service => fiber.ctx.get(service) === undefined)
- throw new Error(`${label} did not activate (waiting for services: ${missing.join(', ') || 'unknown'})`)
- }
-}
-export async function apply(ctx) {
- const runtime = ctx.loader.builtins["dsh-repository-plugin"]
- if (runtime === undefined) throw new Error("missing Cordis builtin dsh-repository-plugin")
- await mount(ctx, runtime, 'repository Plugin runtime', { baseUrl: import.meta.url, manifest })
-}
diff --git a/examples/headless-agent/tests/fixtures/repository-plugin/load.mjs b/examples/headless-agent/tests/fixtures/repository-plugin/load.mjs
deleted file mode 100644
index 90f759876a..0000000000
--- a/examples/headless-agent/tests/fixtures/repository-plugin/load.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Keyless fixture owner that mounts the runtime before its prepared wrapper.
- * Cordis starts sibling Loader entries concurrently, so row order is not a dependency edge.
- */
-import * as RepositoryPlugin from '@deepseek-ai/dsh-repository-plugin'
-import * as PreparedPlugin from './dsh-plugin.mjs'
-
-export const name = 'headless-repository-fixture-loader'
-
-export async function apply(ctx) {
- await ctx.plugin(RepositoryPlugin)
- await ctx.plugin(PreparedPlugin)
-}
diff --git a/examples/headless-agent/tests/fixtures/retry-snapshot-backend.mjs b/examples/headless-agent/tests/fixtures/retry-snapshot-backend.mjs
index 5a2fc5d128..76981fe79c 100644
--- a/examples/headless-agent/tests/fixtures/retry-snapshot-backend.mjs
+++ b/examples/headless-agent/tests/fixtures/retry-snapshot-backend.mjs
@@ -46,7 +46,7 @@ export const inject = ['llm']
/**
* Register the deterministic provider adapter.
- * @param {import('cordis').Context} ctx - plugin context carrying the LLM service.
+ * @param {import('@deepseek-ai/cordis').Context} ctx - plugin context carrying the LLM service.
*/
export function apply(ctx) {
ctx.llm.registerAdapter(['deepseek-official'], new RetrySnapshotAdapter())
diff --git a/examples/headless-agent/tests/fixtures/semantic-checkpoint-agent.ts b/examples/headless-agent/tests/fixtures/semantic-checkpoint-agent.ts
index ef7cf4bafe..f8832da8b3 100644
--- a/examples/headless-agent/tests/fixtures/semantic-checkpoint-agent.ts
+++ b/examples/headless-agent/tests/fixtures/semantic-checkpoint-agent.ts
@@ -3,7 +3,7 @@
* @module semantic-checkpoint-agent
*/
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
/** Fixture plugin name. */
diff --git a/examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts b/examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts
index f77afe7e0a..a869ab6c9f 100644
--- a/examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts
+++ b/examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts
@@ -4,7 +4,7 @@
* @module subagent-diagnostic-agent
*/
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
/** Fixture plugin name. */
diff --git a/examples/headless-agent/tests/fixtures/subagent-inheritance-agent.ts b/examples/headless-agent/tests/fixtures/subagent-inheritance-agent.ts
index 9cd3e6235f..681793a85b 100644
--- a/examples/headless-agent/tests/fixtures/subagent-inheritance-agent.ts
+++ b/examples/headless-agent/tests/fixtures/subagent-inheritance-agent.ts
@@ -3,7 +3,7 @@
* @module subagent-inheritance-agent
*/
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
/** Fixture plugin name. */
diff --git a/examples/headless-agent/tests/fixtures/subagent-settlement-fence.ts b/examples/headless-agent/tests/fixtures/subagent-settlement-fence.ts
new file mode 100644
index 0000000000..c2c8a87885
--- /dev/null
+++ b/examples/headless-agent/tests/fixtures/subagent-settlement-fence.ts
@@ -0,0 +1,43 @@
+/**
+ * Loader fixture that holds the parent's second step until settlement delivery.
+ * @module subagent-settlement-fence
+ */
+
+import type { Context } from '@deepseek-ai/cordis'
+import type {} from '@deepseek-ai/dsh-agent-loop'
+import type {} from '@deepseek-ai/dsh-subagent'
+
+/** Fixture plugin name. */
+export const name = 'subagent-settlement-fence'
+
+/**
+ * Fence the parent's post-spawn request behind admission of the manager notice.
+ *
+ * This pins content order, not step placement: the held pre-step runs after its
+ * own `Inbox.claim()`, so the notice lands after step 2's claim and is claimed at
+ * step 3 because the child's settlement pipeline is strictly longer than the
+ * parent's claim path, not because a barrier forces it.
+ * @param ctx - assembled headless-agent context.
+ */
+export function apply(ctx: Context): void {
+ const delivered = Promise.withResolvers()
+ let hasDelivered = false
+
+ ctx.effect(() => {
+ const disposeInbox = ctx.root.on('agent/inbox/inserted', ({ agent, message }) => {
+ if (agent.session.header.parentSession !== undefined || message.source.kind !== 'subagent-settled') return
+ hasDelivered = true
+ delivered.resolve(undefined)
+ })
+ const disposeStep = ctx.root.on('agent/pre-step', async ({ agent, turn, step }, next) => {
+ if (agent.session.header.parentSession === undefined && turn === 1 && step === 2 && !hasDelivered) {
+ await delivered.promise
+ }
+ return next()
+ })
+ return () => {
+ disposeStep()
+ disposeInbox()
+ }
+ }, 'subagent-settlement-fence.listeners')
+}
diff --git a/examples/headless-agent/tests/fixtures/telemetry-otel.cordis.yml b/examples/headless-agent/tests/fixtures/telemetry-otel.cordis.yml
index 27172b40d8..d7be47fbbe 100644
--- a/examples/headless-agent/tests/fixtures/telemetry-otel.cordis.yml
+++ b/examples/headless-agent/tests/fixtures/telemetry-otel.cordis.yml
@@ -3,7 +3,7 @@
# The redact-rule entry models a deployment mounting its own scrub rule on the
# telemetry/record waterfall — the seam itself ships no rules.
- id: logger-console
- name: '@cordisjs/plugin-logger-console'
+ name: '@deepseek-ai/cordis-plugin-logger-console'
config:
colors: false
levels:
diff --git a/examples/headless-agent/tests/fixtures/telemetry-redact-rule.ts b/examples/headless-agent/tests/fixtures/telemetry-redact-rule.ts
index 7a2aa7958a..5f205f606e 100644
--- a/examples/headless-agent/tests/fixtures/telemetry-redact-rule.ts
+++ b/examples/headless-agent/tests/fixtures/telemetry-redact-rule.ts
@@ -1,4 +1,4 @@
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
/**
* Deployment-style redaction rule for the telemetry e2e: scrubs the fixture
diff --git a/examples/headless-agent/tests/fixtures/time-context-mock-llm.ts b/examples/headless-agent/tests/fixtures/time-context-mock-llm.ts
index 8cd3155ca7..9689dafbcf 100644
--- a/examples/headless-agent/tests/fixtures/time-context-mock-llm.ts
+++ b/examples/headless-agent/tests/fixtures/time-context-mock-llm.ts
@@ -1,4 +1,4 @@
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm'
/** Deterministic one-step adapter for the time-context Loader fixture. */
diff --git a/examples/headless-agent/tests/fixtures/workspace-context-resume-agent.ts b/examples/headless-agent/tests/fixtures/workspace-context-resume-agent.ts
index 4cdf7d157a..ca1499d897 100644
--- a/examples/headless-agent/tests/fixtures/workspace-context-resume-agent.ts
+++ b/examples/headless-agent/tests/fixtures/workspace-context-resume-agent.ts
@@ -3,7 +3,7 @@
* @module workspace-context-resume-agent
*/
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
/** Fixture plugin name. */
diff --git a/examples/headless-agent/tests/full-loop.e2e.ts b/examples/headless-agent/tests/full-loop.e2e.ts
index f4bb8695c9..a8825cc84d 100644
--- a/examples/headless-agent/tests/full-loop.e2e.ts
+++ b/examples/headless-agent/tests/full-loop.e2e.ts
@@ -3,7 +3,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { codingHarness, finalText, SYSTEM_PROMPT, waitForIdle } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
diff --git a/examples/headless-agent/tests/harness.ts b/examples/headless-agent/tests/harness.ts
index 9b953256b7..7cf28a0dc0 100644
--- a/examples/headless-agent/tests/harness.ts
+++ b/examples/headless-agent/tests/harness.ts
@@ -1,4 +1,4 @@
-import { Context } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
diff --git a/examples/headless-agent/tests/headless.snapshot.ts b/examples/headless-agent/tests/headless.snapshot.ts
index b09458ec3d..61c1711566 100644
--- a/examples/headless-agent/tests/headless.snapshot.ts
+++ b/examples/headless-agent/tests/headless.snapshot.ts
@@ -45,6 +45,8 @@ const credentialsConfigPath = fileURLToPath(new URL('../credentials.cordis.snaps
const invalidCredentialScenarioDir = join(snapshotsDir, 'invalid-credential')
const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
+const settlementScenarioDir = join(snapshotsDir, 'subagent-settlement')
+const settlementConfigPath = fileURLToPath(new URL('../subagent-settlement.cordis.snapshot.yml', import.meta.url))
const startupFailureConfigPath = fileURLToPath(new URL('./fixtures/startup-activation-error/cordis.yml', import.meta.url))
const startupFailureExpected = join(snapshotsDir, 'startup-activation-error', 'stderr.expected.txt')
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
@@ -52,9 +54,9 @@ const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', impor
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
const deepseekDefaultsConfigPath = fileURLToPath(new URL('./fixtures/deepseek-defaults.cordis.yml', import.meta.url))
-const dshRunOverlayPath = fileURLToPath(new URL('./fixtures/dsh-run.cordis.yml', import.meta.url))
-const dshRunSessionExpected = join(snapshotsDir, 'dsh-run', 'session.expected.jsonl')
-const dshRunFailureExpected = join(snapshotsDir, 'dsh-run', 'stderr.expected.txt')
+const headlessOverlayPath = fileURLToPath(new URL('./fixtures/headless-profile.cordis.yml', import.meta.url))
+const headlessSessionExpected = join(snapshotsDir, 'headless-profile', 'session.expected.jsonl')
+const headlessFailureExpected = join(snapshotsDir, 'headless-profile', 'stderr.expected.txt')
const cliMockLlmPluginPath = fileURLToPath(new URL('./fixtures/cli-mock-llm.ts', import.meta.url))
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
@@ -217,14 +219,14 @@ async function prepareCliMockFixture(cwd: string): Promise {
}
describe('headless stream-json snapshots', () => {
- it('runs one task through the product dsh run command', async () => {
- const task = 'Prove the product dsh run path with one real tool round trip.'
+ it('runs one task through the product headless profile command', async () => {
+ const task = 'Prove the product headless profile path with one real tool round trip.'
const result = await runLoaderSmoke({
- label: 'product dsh run snapshot',
- tempDirPrefix: 'headless-snapshot-dsh-run-',
+ label: 'product headless profile snapshot',
+ tempDirPrefix: 'headless-snapshot-profile-',
binScript: dshBinScript,
- configPath: dshRunOverlayPath,
- binArgs: ['run', '--patch', dshRunOverlayPath, task],
+ configPath: headlessOverlayPath,
+ binArgs: ['--profile', 'headless', '--patch', headlessOverlayPath, task],
tsconfigPath,
env: {
DSH_PERMISSION_MODE: 'danger-full-access',
@@ -236,11 +238,11 @@ describe('headless stream-json snapshots', () => {
const logs = await persistedLogs(cwd, join(cwd, '.dsh', 'sessions'))
expect(logs).toHaveLength(1)
const actual = logs[0]
- if (actual === undefined) throw new Error('dsh run did not persist its session')
+ if (actual === undefined) throw new Error('the headless profile did not persist its session')
const context = contextFromLogs([actual.content])
const session = scrubRequestHeaders(normalizeSessionLog(actual.content, context))
- if (refreshing) await writeFile(dshRunSessionExpected, session)
- expect(session).toBe(await readFile(dshRunSessionExpected, 'utf8'))
+ if (refreshing) await writeFile(headlessSessionExpected, session)
+ expect(session).toBe(await readFile(headlessSessionExpected, 'utf8'))
expect(session).toContain(task)
expect(session).toContain('CLI tool round trip complete: CLI_TOOL_ROUND_TRIP')
},
@@ -250,13 +252,13 @@ describe('headless stream-json snapshots', () => {
expect(result.stderr).toBe('')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
- it('prints a terminal model failure through the product dsh run command', async () => {
+ it('prints a terminal model failure through the product headless profile command', async () => {
const result = await runLoaderSmoke({
- label: 'product dsh run model failure snapshot',
- tempDirPrefix: 'headless-snapshot-dsh-run-failure-',
+ label: 'product headless profile model failure snapshot',
+ tempDirPrefix: 'headless-snapshot-profile-failure-',
binScript: dshBinScript,
- configPath: dshRunOverlayPath,
- binArgs: ['run', '--patch', dshRunOverlayPath, 'Trigger the keyless model failure.'],
+ configPath: headlessOverlayPath,
+ binArgs: ['--profile', 'headless', '--patch', headlessOverlayPath, 'Trigger the keyless model failure.'],
tsconfigPath,
expectedExitCode: 1,
env: {
@@ -268,7 +270,7 @@ describe('headless stream-json snapshots', () => {
})
expect(result.stdout).toBe('\n')
- await expect(result.stderr).toMatchFileSnapshot(dshRunFailureExpected)
+ await expect(result.stderr).toMatchFileSnapshot(headlessFailureExpected)
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('prints the original Loader activation error through the assembled one-shot app', async () => {
@@ -777,6 +779,77 @@ describe('headless stream-json snapshots', () => {
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
+ it('delivers a continuable child result without parent polling', async () => {
+ const parentReplay = join(settlementScenarioDir, 'parent.replay.jsonl')
+ const parentOverride = join(settlementScenarioDir, 'parent.override.json')
+ const childReplay = join(settlementScenarioDir, 'child.replay.jsonl')
+ const childExpected = join(settlementScenarioDir, 'child.expected.jsonl')
+ const streamExpected = join(settlementScenarioDir, 'stream-json.expected.jsonl')
+ const task = 'Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, task_output, or task_list.'
+ let runCwd = ''
+ const result = await runLoaderSmoke({
+ label: 'continuable settlement headless stream-json snapshot',
+ tempDirPrefix: 'headless-snapshot-subagent-settlement-',
+ binScript,
+ libBinScript: binScript,
+ configPath: settlementConfigPath,
+ binArgs: [settlementConfigPath, task],
+ tsconfigPath,
+ env: {
+ // The override fully supplies the parent script; the child fixture
+ // remains separate so replay binds it to the fresh child Session.
+ DSH_SNAPSHOT_FILE: parentReplay,
+ DSH_SNAPSHOT_OVERRIDE: parentOverride,
+ DSH_SNAPSHOT_CHILD_FILES: childReplay,
+ NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
+ },
+ prepare: (cwd) => { runCwd = cwd },
+ inspect: async (cwd) => {
+ const logs = await persistedLogs(cwd)
+ expect(logs).toHaveLength(2)
+ const parent = logs.find(log => typeof log.header.parentSession !== 'string')
+ const child = logs.find(log => typeof log.header.parentSession === 'string')
+ if (parent === undefined || child === undefined) throw new Error('missing persisted parent or child log')
+
+ const parentRecords = parseJsonl(parent.content)
+ const calls = parentRecords.filter(record => record.type === 'tool/call')
+ expect(calls.map(record => (record.data as JsonObject | undefined)?.name)).toEqual(['subagent'])
+ const callArguments = (calls[0]?.data as JsonObject | undefined)?.arguments
+ if (typeof callArguments !== 'string') throw new Error('subagent call did not persist its arguments')
+ expect(JSON.parse(callArguments)).toMatchObject({ run_in_background: true })
+
+ const notices = parentRecords.flatMap((record) => {
+ if (record.type !== 'agent/inbox/spliced') return []
+ const inserted = (record.data as JsonObject | undefined)?.inserted
+ if (!Array.isArray(inserted)) return []
+ return (inserted as JsonObject[]).filter((message) => {
+ const source = message.source as JsonObject | undefined
+ return source?.kind === 'subagent-settled'
+ })
+ })
+ expect(notices).toHaveLength(1)
+ expect(JSON.stringify(notices[0])).toContain('CHILD_RESULT')
+
+ const context = contextFromLogs([parent.content, child.content])
+ const normalizedChild = scrubRequestHeaders(normalizeSessionLog(child.content, context))
+ if (refreshing) await writeFile(childExpected, normalizedChild)
+ expect(normalizedChild).toBe(await readFile(childExpected, 'utf8'))
+ expect(normalizedChild).toContain('CHILD_RESULT')
+ expect(normalizedChild).not.toContain('"name":"report"')
+ },
+ })
+
+ expect(result.stderr).toBe('')
+ const records = parseJsonl(result.stdout)
+ expect(records.at(-1)).toMatchObject({
+ type: 'result',
+ output: 'PARENT_RECEIVED_CHILD_RESULT',
+ })
+ const normalized = normalizeHeadlessStream(result.stdout, runCwd)
+ if (refreshing) await writeFile(streamExpected, normalized)
+ expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
+ }, LOADER_SMOKE_TEST_TIMEOUT_MS)
+
it('replays persistent PTY tools through the one-shot app', async () => {
const input = JSON.parse(await readFile(join(ptyScenarioDir, 'input.json'), 'utf8')) as {
steps?: { op?: unknown; text?: unknown }[]
diff --git a/examples/headless-agent/tests/keyless-smoke.e2e.ts b/examples/headless-agent/tests/keyless-smoke.e2e.ts
index f855c958bf..9eee7cd0ab 100644
--- a/examples/headless-agent/tests/keyless-smoke.e2e.ts
+++ b/examples/headless-agent/tests/keyless-smoke.e2e.ts
@@ -1,17 +1,10 @@
-import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
-import { tmpdir } from 'node:os'
+import { readFile, readdir } from 'node:fs/promises'
import { zstdDecompress } from 'node:zlib'
import { promisify } from 'node:util'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
-import {
- PREPARED_ENTRY_FILENAME,
- REPOSITORY_PLUGIN_PREPARE_COMMAND,
- REPOSITORY_PLUGIN_PACKAGE_NAME,
- prepareDshPlugin,
-} from '@deepseek-ai/dsh-repository-plugin'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
@@ -44,16 +37,6 @@ describe('headless-agent keyless smoke', () => {
const result = lines.at(-1)
expect(stderr).toBe('')
expect(events.some(event => event.type === 'tool/call' && event.data.name === 'bash')).toBe(true)
- const catalogMessage = events.find(event => event.type === 'user/message'
- && event.data.source.kind === 'skill-catalog')
- const catalog = catalogMessage?.type === 'user/message'
- ? catalogMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('\n')
- : ''
- expect(catalog.split('\n').find(line => line.includes('repository-fixture'))).toMatchInlineSnapshot(
- `
- "- \`repository-fixture\`: Repository fixture skill."
- `,
- )
const toolResult = events.find(event => event.type === 'tool/result')
expect(JSON.stringify(toolResult)).toContain('CLI_TOOL_ROUND_TRIP')
expect(result).toMatchObject({
@@ -63,30 +46,4 @@ describe('headless-agent keyless smoke', () => {
expect(String(result?.['output'])).toContain('CLI_TOOL_ROUND_TRIP')
expect(persistedHeader).toMatchObject({ type: 'session' })
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
-
- it('keeps the checked-in prepared wrapper identical to the generator output for its manifest', async () => {
- // The fixture claims "Generated by dsh-plugin-prepare"; this pin makes the
- // claim true — a wrapper-template change fails here until the fixture is
- // regenerated, so the assembled smoke can never exercise stale generated fields.
- const fixture = fileURLToPath(new URL('./fixtures/repository-plugin/', import.meta.url))
- const root = await mkdtemp(join(tmpdir(), 'dsh-fixture-drift-'))
- try {
- const plugin = join(root, '.dsh-plugin')
- await mkdir(plugin, { recursive: true })
- await cp(join(fixture, 'dsh-plugin-assets/skills/0'), join(root, 'skills'), { recursive: true })
- await writeFile(join(plugin, 'package.json'), `${JSON.stringify({
- name: 'headless-repository-fixture',
- version: '0.0.0',
- scripts: { prepack: REPOSITORY_PLUGIN_PREPARE_COMMAND },
- devDependencies: { [REPOSITORY_PLUGIN_PACKAGE_NAME]: '0.0.1' },
- dsh: { skills: ['../skills'] },
- }, undefined, 2)}\n`)
- await prepareDshPlugin(plugin)
- const generated = await readFile(join(plugin, PREPARED_ENTRY_FILENAME), 'utf8')
- const checkedIn = await readFile(join(fixture, PREPARED_ENTRY_FILENAME), 'utf8')
- expect(checkedIn).toBe(generated)
- } finally {
- await rm(root, { recursive: true, force: true })
- }
- })
})
diff --git a/examples/headless-agent/tests/resume.e2e.ts b/examples/headless-agent/tests/resume.e2e.ts
index 3875a157d8..6767357b4e 100644
--- a/examples/headless-agent/tests/resume.e2e.ts
+++ b/examples/headless-agent/tests/resume.e2e.ts
@@ -3,7 +3,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { SessionId } from '@deepseek-ai/dsh-session'
import { codingHarness, finalText, SYSTEM_PROMPT, waitForIdle } from './harness.ts'
diff --git a/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts b/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts
index a8b8f02b9a..474422b4d2 100644
--- a/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts
+++ b/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts
@@ -1,7 +1,7 @@
import { readFile, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
-import { Context } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
import { normalizeSessionLog, scrubRequestHeaders, type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createUserMessage, CallId , createMessage } from '@deepseek-ai/dsh-llm'
diff --git a/examples/headless-agent/tests/session-format-guard.snapshot.ts b/examples/headless-agent/tests/session-format-guard.snapshot.ts
new file mode 100644
index 0000000000..ac1b5ae43c
--- /dev/null
+++ b/examples/headless-agent/tests/session-format-guard.snapshot.ts
@@ -0,0 +1,107 @@
+/**
+ * Assembled-app regression for the session-format refusal surface: resuming a
+ * log written by a "newer" harness (format version ahead, or an unknown
+ * required event type) fails loud through the real Loader composition, and the
+ * error the product user sees names the direction and the raw log path.
+ * @module session-format-guard-snapshot
+ */
+
+import { join, dirname } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { Context } from '@deepseek-ai/cordis'
+import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
+import SessionStore, {
+ SESSION_FORMAT_VERSION,
+ SessionId,
+ type SessionEvent,
+ type SessionHeader,
+} from '@deepseek-ai/dsh-session'
+import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
+import { describe, expect, it } from 'vitest'
+
+const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), 'workspace-context-resume-snapshots/offline-edit')
+const replayFixture = join(fixtureDir, 'replay.jsonl')
+const configPath = fileURLToPath(new URL('../workspace-context-resume.cordis.snapshot.yml', import.meta.url))
+const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
+const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
+// The resumed-agent fixture in the shared config resumes exactly this id.
+const sessionId = SessionId('workspace-context-resume')
+
+/** Persist one session with the given header version and events, returning its log path. */
+async function seedSession(root: string, cwd: string, version: number, events: SessionEvent[]): Promise {
+ const ctx = new Context()
+ await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
+ const meta: SessionHeader = { version, id: sessionId, createdAt: 1, cwd }
+ try {
+ await ctx.sessionPersistence.create(meta)
+ await ctx.sessionPersistence.append(sessionId, events)
+ const location = ctx.sessionPersistence.locate(meta)
+ if (location === undefined) throw new Error('JSONL backend did not locate the seeded session')
+ return location.path
+ } finally {
+ await ctx.fiber.dispose()
+ }
+}
+
+function closedTurn(): SessionEvent[] {
+ return [
+ { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
+ { type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
+ ]
+}
+
+describe('session format guard through the assembled app', () => {
+ it('refuses to resume a newer-format log, naming the upgrade direction and the raw log path', async () => {
+ let sessionPath = ''
+ const result = await runLoaderSmoke({
+ label: 'newer-format resume refusal',
+ tempDirPrefix: 'dsh-format-guard-version-',
+ binScript,
+ libBinScript: binScript,
+ configPath,
+ binArgs: [configPath, 'Try to resume.'],
+ tsconfigPath,
+ env: { DSH_SNAPSHOT_FILE: replayFixture },
+ expectedExitCode: 1,
+ prepare: async (runCwd) => {
+ sessionPath = await seedSession(join(runCwd, '.sessions'), runCwd, SESSION_FORMAT_VERSION + 99, closedTurn())
+ },
+ })
+ expect(result.stderr).toContain(
+ `session "${sessionId}" uses log format v${SESSION_FORMAT_VERSION + 99}, but this harness reads only v${SESSION_FORMAT_VERSION}: the log was written by a newer harness — upgrade the harness to open it`,
+ )
+ // macOS reports the temp dir via the /private symlink parent; assert the
+ // stable path suffix instead of the realpath-dependent prefix.
+ expect(result.stderr).toContain('(raw log: ')
+ expect(result.stderr).toContain(sessionPath.slice(sessionPath.indexOf('/.sessions/')))
+ }, LOADER_SMOKE_TEST_TIMEOUT_MS)
+
+ it('refuses to resume a log with an unknown required event type', async () => {
+ let sessionPath = ''
+ const result = await runLoaderSmoke({
+ label: 'unknown-event resume refusal',
+ tempDirPrefix: 'dsh-format-guard-event-',
+ binScript,
+ libBinScript: binScript,
+ configPath,
+ binArgs: [configPath, 'Try to resume.'],
+ tsconfigPath,
+ env: { DSH_SNAPSHOT_FILE: replayFixture },
+ expectedExitCode: 1,
+ prepare: async (runCwd) => {
+ sessionPath = await seedSession(join(runCwd, '.sessions'), runCwd, SESSION_FORMAT_VERSION, [
+ ...closedTurn(),
+ { type: 'future/event', seq: 2, time: 3, data: { payload: 1 } } as unknown as SessionEvent,
+ ])
+ },
+ })
+ expect(result.stderr).toContain(
+ `session "${sessionId}" contains event type "future/event" (seq 2) unknown to this harness and not marked ignorable; refusing to interpret the log — it was likely written by a newer harness`,
+ )
+ // macOS reports the temp dir via the /private symlink parent; assert the
+ // stable path suffix instead of the realpath-dependent prefix.
+ expect(result.stderr).toContain('(raw log: ')
+ expect(result.stderr).toContain(sessionPath.slice(sessionPath.indexOf('/.sessions/')))
+ }, LOADER_SMOKE_TEST_TIMEOUT_MS)
+})
diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
index 44cbf0e360..584aa2265c 100644
--- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
+++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
@@ -1,18 +1,19 @@
{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498583877,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"fc62f9e7-b8f6-441f-9ee8-17f1f9e4feca"}]}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785498583877,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ecf26e7-d4bc-4a5c-a611-b87c3ab33d22"}]}}
{"type":"turn/start","seq":1,"time":1785821454445,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821454445,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","seq":3,"time":1785821454466,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"step/start","seq":4,"time":1785730501506,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785730501506,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"fc62f9e7-b8f6-441f-9ee8-17f1f9e4feca"},"surfaceOp":"append"}
-{"type":"session/title","seq":6,"time":1785730501506,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
-{"type":"request/header","seq":7,"time":1785498583897,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\n## Writing code for run_code\n\nPass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:\n\n- 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.\n- 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.\n- 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`.\n- 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.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc. */\n cordis_inspect: {\n /** Limit the report to one section. Omit for all sections. */\n what?: \"services\" | \"plugins\" | \"tools\" | \"temporary\" | \"api\" | \"events\";\n /** Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\". */\n name?: string;\n } & Record;\n /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */\n cordis_mount: {\n /** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */\n code: string;\n } & Record;\n /** Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins. */\n cordis_unmount: {\n /** The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart. */\n id: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n /** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */\n run_in_background?: boolean;\n } & Record;\n /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */\n task_kill: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Optional short reason, recorded in the log and forwarded to the task. */\n reason?: string;\n } & Record;\n /** List your background tasks (running and finished) with their ids, kinds, and statuses. */\n task_list: Record;\n /** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n task_output: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record