test(sdk): snapshot suite over the SDK path; docs for the SDK stack
- examples/jsonrpc-agent gains its first snapshot suite (sdk.snapshot.ts): the real dsh-jsonrpc-agent runtime driven through the real dsh-sdk-client, keyless llm-replay behind a new cordis.snapshot.yml overlay; three recorded scenarios (text turn, bash tool, spawn subagent) pin the notification stream, the SDK turn result, and the persisted parent+child session logs. - Bilingual READMEs for dsh-sdk-protocol / dsh-sdk-client / dsh-subagent-sdk; sdk/ and subagent/ group tables extended; dsh-jsonrpc README points at the extracted protocol package; Agent Note (en+zh) owns the decision. - The proposed make-jsonrpc-directional note is updated for the transport's new home and its second (client) consumer. - Model Experience sentence allowlist entries for the two client-side packages; module graph + config catalog regenerated; i18n pairings recorded. doc-sync passes 24/24.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 5e3bddc67d213d74766a75da65cc44a21c8bb149
|
||||
README.zh.md: 4391809ee83c822fcada25f0bdc021af44be9354
|
||||
README.md: 15b05f22d5ab2ed6bdfc4f3725737d62afc8f7c1
|
||||
README.zh.md: d331530f60cf584ed906553a5caa00d6a18efbc2
|
||||
|
||||
@@ -10,10 +10,11 @@ The subagent seam: an agent delegating work to a child agent. Like the [bash](..
|
||||
| `subagent-inprocess/` | Shared in-process run driver (no provider; one cleanup effect per run) | — |
|
||||
| `subagent-spawn/` | In-process backend: a fresh child agent | (registers on `ctx.subagents`) |
|
||||
| `subagent-fork/` | In-process backend: a child seeded with the parent's completed-turn prefix | (registers on `ctx.subagents`) |
|
||||
| `subagent-subprocess/` | Shared out-of-process machinery: env scrub, dispose ladder, isolated config dirs (pure lib; registers nothing) | — |
|
||||
| `subagent-subprocess/` | Shared out-of-process machinery: env scrub, dispose ladder, cwd resolution, isolated config dirs (pure lib; registers nothing) | — |
|
||||
| `subagent-acp/` | Out-of-process backend: a child agent in a spawned subprocess, driven over ACP | (registers on `ctx.subagents`) |
|
||||
| `subagent-sdk/` | Out-of-process backend: a child harness runtime in a spawned subprocess, driven over stdio JSON-RPC through the TypeScript SDK client | (registers on `ctx.subagents`) |
|
||||
| `tool-subagent/` | Model-facing `subagent` delegation tool over `ctx.subagents` | (registers on `ctx.tools`) |
|
||||
|
||||
The interface lives at `subagent/subagent/`. The in-process `subagent-spawn` / `subagent-fork` backends share the `subagent-inprocess` driver (a library with no provider of its own — both depend on it, neither on the other), and the out-of-process `subagent-acp` backend builds on the `subagent-subprocess` library (the credential env scrub, the dispose ladder, isolated config dirs). Tests replace only the child boundary with package-local fixtures.
|
||||
The interface lives at `subagent/subagent/`. The in-process `subagent-spawn` / `subagent-fork` backends share the `subagent-inprocess` driver (a library with no provider of its own — both depend on it, neither on the other), and the out-of-process `subagent-acp` / `subagent-sdk` backends build on the `subagent-subprocess` library (the credential env scrub, the dispose ladder, child cwd resolution, isolated config dirs). Tests replace only the child boundary with package-local fixtures.
|
||||
|
||||
The proposal and design rationale: [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md).
|
||||
|
||||
@@ -10,10 +10,11 @@ subagent seam 允许 agent(智能体)把工作委派给子 agent。与 [bash
|
||||
| `subagent-inprocess/` | 共享进程内运行驱动器(不提供提供方;每次运行使用一个清理 effect) | 无 |
|
||||
| `subagent-spawn/` | 进程内后端:全新的子 agent | (注册到 `ctx.subagents`) |
|
||||
| `subagent-fork/` | 进程内后端:以父 agent 已完成轮次的前缀作为初始内容的子 agent | (注册到 `ctx.subagents`) |
|
||||
| `subagent-subprocess/` | 共享进程外机制:环境变量清理、dispose(资源释放)阶梯、隔离配置目录(纯库;不注册任何内容) | 无 |
|
||||
| `subagent-subprocess/` | 共享进程外机制:环境变量清理、dispose(资源释放)阶梯、cwd 解析、隔离配置目录(纯库;不注册任何内容) | 无 |
|
||||
| `subagent-acp/` | 进程外后端:在派生子进程中运行并通过 ACP(Agent Client Protocol)驱动的子 agent | (注册到 `ctx.subagents`) |
|
||||
| `subagent-sdk/` | 进程外后端:在派生子进程中运行的子 harness 运行时,经 TypeScript SDK 客户端走 stdio JSON-RPC 驱动 | (注册到 `ctx.subagents`) |
|
||||
| `tool-subagent/` | 面向模型的 `subagent` 委派工具,基于 `ctx.subagents` | (注册到 `ctx.tools`) |
|
||||
|
||||
接口位于 `subagent/subagent/`。进程内 `subagent-spawn` / `subagent-fork` 后端共享 `subagent-inprocess` 驱动器(一个自身不提供提供方的库:两者都依赖它,彼此不依赖),进程外 `subagent-acp` 后端则构建于 `subagent-subprocess` 库之上(凭据环境变量清理、dispose 阶梯、隔离配置目录)。测试只用包内 fixture(测试前置数据)替换子 agent 边界。
|
||||
接口位于 `subagent/subagent/`。进程内 `subagent-spawn` / `subagent-fork` 后端共享 `subagent-inprocess` 驱动器(一个自身不提供提供方的库:两者都依赖它,彼此不依赖),进程外 `subagent-acp` / `subagent-sdk` 后端则构建于 `subagent-subprocess` 库之上(凭据环境变量清理、dispose 阶梯、子进程 cwd 解析、隔离配置目录)。测试只用包内 fixture(测试前置数据)替换子 agent 边界。
|
||||
|
||||
提案与设计理由见 [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)。
|
||||
|
||||
6
packages/subagent/subagent-sdk/README.i18n.yaml
Normal file
6
packages/subagent/subagent-sdk/README.i18n.yaml
Normal file
@@ -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
|
||||
README.md: bc9b1ed7706d16d0a7463d0b26beb3ccf171ce5b
|
||||
README.zh.md: 76eb4ffaf9854892e05ad891c7b951a838b89e8e
|
||||
97
packages/subagent/subagent-sdk/README.md
Normal file
97
packages/subagent/subagent-sdk/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# @deepseek-ai/dsh-subagent-sdk
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The SDK provider runs each subagent as a complete DeepSeek Harness runtime in a fresh subprocess, driven over stdio JSON-RPC through the [TypeScript SDK client](../../sdk/sdk-client/README.md). It is the second out-of-process backend beside [`subagent-acp`](../subagent-acp/README.md), differing in the wire and the child contract: the ACP backend drives any Agent Client Protocol agent; this backend drives specifically a harness SDK runtime (`dsh-jsonrpc-agent` bin or packaged executable), so the child is a full peer harness — own `cordis.yml`-decided composition, session persistence, model route, and tools.
|
||||
|
||||
## Start and ownership
|
||||
|
||||
`start(request)` resolves the child's working directory, spawns the runtime through `DeepSeekHarness`, and completes the `initialize` handshake (with the configured `provider`/`model` route) before it fulfills. Fulfillment therefore means the child runtime is ready and ownership has transferred to the caller. A spawn, handshake, or pre-publication cancellation failure rejects only after the subprocess has been reaped; a working-directory resolution failure rejects before anything is spawned.
|
||||
|
||||
The working directory resolves exactly like the ACP backend, through the shared [`subagent-subprocess` helpers](../subagent-subprocess/README.md): the configured `cwd` override when set (validated once at load), else the delegating parent session's cwd — never the server process's own cwd. The resolved path becomes the child process cwd and the workspace cwd of its SDK session.
|
||||
|
||||
The returned run id is minted in the parent namespace; the child runtime's session id exists only inside the child process. After publication the provider runs one SDK turn and reads the child's answer from its session events: the last complete `assistant/message`, or the `text-delta` stream accumulated so far when the turn was cut short — a partial answer survives cancel and error paths.
|
||||
|
||||
`dispose()` is idempotent: it settles the result locally as `aborted` (there is no wire-level prompt cancel), then closes the runtime — a bounded protocol `shutdown` request followed by the shared stdin-EOF → SIGTERM → SIGKILL ladder to actual exit.
|
||||
|
||||
## Stop-reason mapping
|
||||
|
||||
The child reports its turn outcome as a structured `TurnEndReason` on `session.finished`; the provider maps it into the seam vocabulary. `completed` → `completed`, `max-tokens` → `max-tokens`, `aborted` → `aborted`; everything else — `error`, `rejected`, `interrupted`, `disposed`, a future variant, or a turn that never ran — maps to `error`, so an unclean stop is never reported as success. Transport-level failures after publication flatten to `stopReason: 'error'` through the `onError` diagnostic sink (wired to `ctx.logger.warn`); the seam contract forbids `result` rejecting.
|
||||
|
||||
## Capabilities and context
|
||||
|
||||
The provider advertises no start-time capabilities (`outputSchema`/`depthLimit`/`toolFilter`/`persona` all false) and `inheritsParentContext: false`: the child is a fresh runtime in another process, and the only parent-derived input is the workspace cwd. `dsh-tool-subagent` deployments over this provider set `maxDepth: 'provider-managed'` — the child harness owns its own recursion budget.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `providerName` | `sdk` | Registry name on `ctx.subagents`. |
|
||||
| `command` | required | Executable spawned per run (the child runtime bin or packaged exe). |
|
||||
| `args` | `[]` | Command arguments (typically the child's `cordis.yml` path). |
|
||||
| `cwd` | parent session cwd | Working-directory override; same validation as [`subagent-acp`](../subagent-acp/README.md). |
|
||||
| `provider` | `deepseek` | Provider route sent in the child's `initialize`. |
|
||||
| `model` | `deepseek-v4-flash` | Model sent in the child's `initialize`. |
|
||||
| `env` | `{}` | Explicit child environment layered over a credential-scrubbed parent environment (e.g. the child's own `DEEPSEEK_API_KEY`, or `DSH_CORDIS_CONFIG`). |
|
||||
| `shutdownTimeoutMs` | `1000` | Bound on the protocol `shutdown` exchange during dispose. |
|
||||
| `disposeEofGraceMs` | `6000` | Grace after stdin EOF before platform termination. |
|
||||
| `disposeGraceMs` | `3000` | Exit-confirmation grace after termination; POSIX also waits this long after SIGTERM before SIGKILL. |
|
||||
|
||||
```yaml
|
||||
- id: subagent-sdk
|
||||
name: '@deepseek-ai/dsh-subagent-sdk'
|
||||
config:
|
||||
providerName: sdk
|
||||
command: node
|
||||
args: ['./packages/examples/jsonrpc-demo/lib/bin.js', './examples/jsonrpc-agent/cordis.yml']
|
||||
env:
|
||||
DEEPSEEK_API_KEY: !!js process.env.DEEPSEEK_API_KEY
|
||||
- id: tool-subagent
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config: { provider: sdk, toolName: subagent, maxDepth: 'provider-managed' }
|
||||
```
|
||||
|
||||
## Process boundary
|
||||
|
||||
The child environment is built by [`buildChildEnv`](../subagent-subprocess/README.md): credential-shaped ambient variables are removed, then explicit `config.env` values are applied. The JSON-RPC wire is the real serialization boundary.
|
||||
|
||||
The package has no default export. Cordis loader unwrapping would otherwise hide the named `inject` metadata; see [postmortem 0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md).
|
||||
|
||||
Keyless tests drive the SDK client package's scripted fake runtime over real stdio, including a Loader-composed e2e where the child is a real second harness runtime proving parent-session cwd inheritance end to end (`tests/loader-composition.e2e.ts`).
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Child-agent request
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The child runtime's model receives the standalone task as its user message plus that runtime's own configured system prompt, tools, and fresh session. It receives no parent conversation. This provider advertises no optional start-time capabilities, so the local service rejects requests for persona, tool filtering, depth enforcement, or structured output instead of silently omitting them.
|
||||
|
||||
#### Token effect
|
||||
|
||||
The child pays for an independent full context and its own multi-step history. These tokens never enter the parent's context.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Independent of the parent request cache. Each SDK child can reuse only prefixes identical under its own provider, model, composition, and history; child steps otherwise grow append-only.
|
||||
|
||||
### Parent tool result, indirectly
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Through `dsh-tool-subagent`, the parent receives only the child's final assistant text (or accumulated partial text) or that consumer's exact stop-reason error, not intermediate messages or tool traffic.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Parent input grows only by the final result or error, which is data-dependent and retained until compaction. This provider adds no parent schema itself.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **A fresh runtime process per run** — no pooling; a harness runtime boots a full plugin tree, so per-run spawn cost is higher than the ACP backend's typical child.
|
||||
- **No optional start-time capabilities** — the parent cannot enforce `outputSchema`, depth, tool filters, or persona inside the child process; configure the child's own `cordis.yml` instead.
|
||||
- **The child's transcript stays in the child's own session root** — the parent log records only the delegation tool call/result (the seam's child-isolation rule); the streamed `session.event` channel is consumed for output extraction, not bridged into the parent log.
|
||||
- **Local child processes only** — the resolved cwd is a local path; a remote runtime would need its own backend.
|
||||
97
packages/subagent/subagent-sdk/README.zh.md
Normal file
97
packages/subagent/subagent-sdk/README.zh.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# @deepseek-ai/dsh-subagent-sdk
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
SDK provider 把每个子代理作为一个完整的 DeepSeek Harness 运行时跑在全新子进程里,经由 [TypeScript SDK 客户端](../../sdk/sdk-client/README.md)走 stdio JSON-RPC 驱动。它是 [`subagent-acp`](../subagent-acp/README.md) 之外的第二个进程外后端,差异在线协议与子进程契约:ACP 后端能驱动任何 Agent Client Protocol 代理;本后端专门驱动 harness SDK 运行时(`dsh-jsonrpc-agent` bin 或打包可执行文件),因此子进程是一个完整的对等 harness——自有 `cordis.yml` 决定的组成、会话持久化、模型路由与工具。
|
||||
|
||||
## 启动与所有权
|
||||
|
||||
`start(request)` 先解析子进程工作目录,经 `DeepSeekHarness` 生成运行时,并在履行前完成 `initialize` 握手(携带配置的 `provider`/`model` 路由)。因此履行意味着子运行时已就绪、所有权已移交调用方。生成、握手或发布前取消的失败只在子进程被收割之后拒绝;工作目录解析失败在生成任何东西之前拒绝。
|
||||
|
||||
工作目录的解析与 ACP 后端完全一致,经由共享的 [`subagent-subprocess` 助手](../subagent-subprocess/README.md):设置了 `cwd` 覆盖则用之(加载时校验一次),否则用发起委托的父会话 cwd——绝不用服务器进程自己的 cwd。解析出的路径同时成为子进程 cwd 与其 SDK 会话的工作区 cwd。
|
||||
|
||||
返回的 run id 铸造于父命名空间;子运行时的会话 id 只存在于子进程内部。发布之后,provider 跑一个 SDK 回合,并从子会话事件中读取答案:最后一条完整 `assistant/message`,或回合被截断时已累积的 `text-delta` 流——部分答案在取消与错误路径上都得以保留。
|
||||
|
||||
`dispose()` 幂等:先把结果就地定格为 `aborted`(线上没有 prompt 取消方法),再关闭运行时——一次有界的协议 `shutdown` 请求,随后是共享的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出。
|
||||
|
||||
## 停止原因映射
|
||||
|
||||
子进程在 `session.finished` 上以结构化 `TurnEndReason` 报告回合结局;provider 把它映射进接缝词汇表。`completed` → `completed`,`max-tokens` → `max-tokens`,`aborted` → `aborted`;其余一切——`error`、`rejected`、`interrupted`、`disposed`、未来变体、或根本没跑回合——映射为 `error`,不洁终止绝不报告为成功。发布后的传输层失败经 `onError` 诊断汇(接到 `ctx.logger.warn`)压平为 `stopReason: 'error'`;接缝契约禁止 `result` 拒绝。
|
||||
|
||||
## 能力与上下文
|
||||
|
||||
Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilter`/`persona` 全为 false),且 `inheritsParentContext: false`:子进程是另一进程里的全新运行时,唯一来自父方的输入是工作区 cwd。基于本 provider 的 `dsh-tool-subagent` 部署应设置 `maxDepth: 'provider-managed'`——子 harness 拥有自己的递归预算。
|
||||
|
||||
## 配置
|
||||
|
||||
| 键 | 默认 | 含义 |
|
||||
|---|---|---|
|
||||
| `providerName` | `sdk` | `ctx.subagents` 上的注册名。 |
|
||||
| `command` | 必填 | 每次 run 生成的可执行文件(子运行时 bin 或打包 exe)。 |
|
||||
| `args` | `[]` | 命令参数(通常是子进程的 `cordis.yml` 路径)。 |
|
||||
| `cwd` | 父会话 cwd | 工作目录覆盖;校验规则与 [`subagent-acp`](../subagent-acp/README.md) 相同。 |
|
||||
| `provider` | `deepseek` | 写入子进程 `initialize` 的 provider 路由。 |
|
||||
| `model` | `deepseek-v4-flash` | 写入子进程 `initialize` 的模型。 |
|
||||
| `env` | `{}` | 在凭据擦除后的父环境之上叠加的显式子环境(例如子进程自己的 `DEEPSEEK_API_KEY`,或 `DSH_CORDIS_CONFIG`)。 |
|
||||
| `shutdownTimeoutMs` | `1000` | 处置期间协议 `shutdown` 交换的时限。 |
|
||||
| `disposeEofGraceMs` | `6000` | stdin EOF 之后、平台终止之前的宽限。 |
|
||||
| `disposeGraceMs` | `3000` | 终止后的退出确认窗口;POSIX 在 SIGTERM 之后、SIGKILL 之前也等待同样时长。 |
|
||||
|
||||
```yaml
|
||||
- id: subagent-sdk
|
||||
name: '@deepseek-ai/dsh-subagent-sdk'
|
||||
config:
|
||||
providerName: sdk
|
||||
command: node
|
||||
args: ['./packages/examples/jsonrpc-demo/lib/bin.js', './examples/jsonrpc-agent/cordis.yml']
|
||||
env:
|
||||
DEEPSEEK_API_KEY: !!js process.env.DEEPSEEK_API_KEY
|
||||
- id: tool-subagent
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config: { provider: sdk, toolName: subagent, maxDepth: 'provider-managed' }
|
||||
```
|
||||
|
||||
## 进程边界
|
||||
|
||||
子环境由 [`buildChildEnv`](../subagent-subprocess/README.md) 构建:先移除形似凭据的环境变量,再应用显式 `config.env` 值。JSON-RPC 线就是真实的序列化边界。
|
||||
|
||||
本包没有默认导出。否则 Cordis loader 解包会隐藏具名 `inject` 元数据;见[事后分析 0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)。
|
||||
|
||||
免密钥测试通过真实 stdio 驱动 SDK 客户端包的脚本化伪运行时,还包括一个 Loader 组合 e2e:子进程是真实的第二个 harness 运行时,端到端证明父会话 cwd 继承(`tests/loader-composition.e2e.ts`)。
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Child-agent request
|
||||
|
||||
#### What the model sees
|
||||
|
||||
子运行时的模型收到独立任务作为其用户消息,加上该运行时自己配置的系统提示、工具与全新会话。它收不到任何父对话。本 provider 不宣告可选启动期能力,因此本地服务会拒绝需要 persona、工具过滤、深度强制或结构化输出的请求,而不是静默省略。
|
||||
|
||||
#### Token effect
|
||||
|
||||
子进程支付一份独立的完整上下文与自己的多步历史。这些 token 绝不进入父上下文。
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
独立于父请求缓存。每个 SDK 子进程只能复用在其自身 provider、模型、组成与历史下完全相同的前缀;子步骤在此之外只增不改。
|
||||
|
||||
### Parent tool result, indirectly
|
||||
|
||||
#### What the model sees
|
||||
|
||||
经由 `dsh-tool-subagent`,父方只收到子进程的最终助手文本(或累积的部分文本),或该消费者精确的停止原因错误——收不到中间消息与工具流量。
|
||||
|
||||
#### Token effect
|
||||
|
||||
父输入只增长最终结果或错误,其大小依数据而定,保留至压缩。本 provider 自身不给父方增加任何 schema。
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
只追加;新可见内容跟在可复用请求前缀之后,不使既有 KV 缓存条目失效。
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **每次 run 一个全新运行时进程** —— 无池化;harness 运行时要启动完整插件树,单次生成成本高于 ACP 后端的典型子进程。
|
||||
- **无可选启动期能力** —— 父方无法在子进程内强制 `outputSchema`、深度、工具过滤或 persona;请改为配置子进程自己的 `cordis.yml`。
|
||||
- **子进程的转录留在其自己的会话根** —— 父日志只记录委托工具调用/结果(接缝的子隔离规则);流式 `session.event` 通道只用于提取输出,不桥接进父日志。
|
||||
- **仅限本地子进程** —— 解析出的 cwd 是本地路径;远程运行时需要自己的后端。
|
||||
Reference in New Issue
Block a user