refactor: rename the backend to dsh-subagent-dsh-sdk
The group's convention is package suffix == provider default (subagent-acp/'acp', subagent-spawn/'spawn', subagent-fork/'fork'), and the provider default became dsh-sdk in the last review round — so the package follows: @deepseek-ai/dsh-subagent-dsh-sdk at packages/subagent/subagent-dsh-sdk, plugin name subagent-dsh-sdk, diagnostics prefixed subagent-dsh-sdk:. The dsh echo has precedent (dsh-llm-deepseek). Directory, fixture path, knip/tsconfig/examples registrations, catalogs, READMEs (en+zh), and the Agent Note follow; the sdk-client dispose ladder moves to its own module (src/dispose.ts) with the deterministic FakeChild tier tests restored alongside it.
This commit is contained in:
+3
-3
@@ -1,6 +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
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: b0a5461b00c76e06a75d9ac4bd8cde9778a26ce8
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 856db82f6d47ee686216c8ef5f91048493a90801
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: a1481bb9e2c3abfc3111dce8a1c38835436e3e13
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 64a55f6aa0cb4b9a4a5efc625ece6a0800217f52
|
||||
+8
-8
@@ -13,9 +13,9 @@ The stdio JSON-RPC serving surface (`@deepseek-ai/dsh-jsonrpc`, the [single-exe
|
||||
Three packages, layered exactly like the existing Python stack, plus one seam registration:
|
||||
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/sdk-protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-jsonrpc` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message).
|
||||
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/sdk-client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Session-tree scoping from `subagent.started` lineage edges is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `buildChildEnv` is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown reuses `disposeChildProcess` instead of hand-rolled terminate/kill.
|
||||
- **`@deepseek-ai/dsh-subagent-sdk`** (`packages/subagent/subagent-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `provider`/`model` config feeds the child's `initialize`; `env` is where deployments pass the child's own key and `DSH_CORDIS_CONFIG`.
|
||||
- **`dsh-subagent-subprocess` grows a third shared concern**: child cwd resolution (`assertUsableCwd`/`validateConfiguredCwd`/`resolveChildCwd`), extracted from `subagent-acp` when the SDK backend needed the identical config-override-else-parent-session-cwd policy, prefix-parameterized for diagnostics.
|
||||
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/sdk-client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Session-tree scoping from `subagent.started` lineage edges is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`).
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `provider`/`model` config feeds the child's `initialize`; `env` is where deployments pass the child's own key and `DSH_CORDIS_CONFIG`.
|
||||
- **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself.
|
||||
|
||||
`dsh-jsonrpc` keeps serving unchanged (the wire is byte-identical); `dsh-jsonrpc-agent-pkg` (the Python runtime closure) gains the `dsh-sdk-protocol` dependency line.
|
||||
|
||||
@@ -23,18 +23,18 @@ Three packages, layered exactly like the existing Python stack, plus one seam re
|
||||
|
||||
Four tiers, per [testing policy](../../../../docs/testing.md):
|
||||
|
||||
- **Keyless unit** — `sdk-client` drives a scripted fake runtime (`tests/fake-runtime.ts`, env-scripted, protocol-only — the Python `test_client.py` pattern) over real stdio; `subagent-sdk` drives the same fake through the real provider. 100% per-file coverage on all three packages.
|
||||
- **Keyless Loader composition** — `subagent-sdk/tests/loader-composition.e2e.ts` boots a test-only cordis.yml (`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/`) where the child is a REAL second harness runtime with its own cordis.yml; asserts the parent tool result and the child's own persisted transcript both carry the parent session's cwd. The child launch resolves through `resolveExampleLaunch`, so src/lib modes both hold.
|
||||
- **Keyless unit** — `sdk-client` drives a scripted fake runtime (`tests/fake-runtime.ts`, env-scripted, protocol-only — the Python `test_client.py` pattern) over real stdio; `subagent-dsh-sdk` drives the same fake through the real provider. 100% per-file coverage on all three packages.
|
||||
- **Keyless Loader composition** — `subagent-dsh-sdk/tests/loader-composition.e2e.ts` boots a test-only cordis.yml (`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/`) where the child is a REAL second harness runtime with its own cordis.yml; asserts the parent tool result and the child's own persisted transcript both carry the parent session's cwd. The child launch resolves through `resolveExampleLaunch`, so src/lib modes both hold.
|
||||
- **Keyless snapshot** — `examples/jsonrpc-agent/tests/sdk.snapshot.ts` is the jsonrpc example's first snapshot suite: the real `dsh-jsonrpc-agent` runtime driven through the real `dsh-sdk-client`, replaying recorded fixtures via `llm-replay` behind the new `cordis.snapshot.yml` overlay (passed explicitly through `DSH_CORDIS_CONFIG`; the jsonrpc bin performs no snapshot config swap of its own). Three scenarios — text turn, bash tool, spawn subagent — each pinning the normalized notification stream, the SDK turn result, and the persisted parent+child logs. This also closes the protocol-tier gap the single-exe note's Python-side snapshot left on the vitest side.
|
||||
- **With-key e2e** — the snapshot suite's `DSH_SNAPSHOT=record` mode is the live-API path (it produced the committed fixtures); the composition e2e needs no key by design.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Import wire types from `dsh-jsonrpc` instead of extracting a protocol package.** Makes every SDK consumer (including `subagent-sdk`, which must not serve JSON-RPC) depend on the server plugin and its `dsh-agent`/`dsh-llm-deepseek` peer set, and leaves the notification payloads anonymous. The capability-seam rule (interface/implementation/consumer as separate packages) already names this shape; the transport is genuinely two-sided.
|
||||
**Import wire types from `dsh-jsonrpc` instead of extracting a protocol package.** Makes every SDK consumer (including `subagent-dsh-sdk`, which must not serve JSON-RPC) depend on the server plugin and its `dsh-agent`/`dsh-llm-deepseek` peer set, and leaves the notification payloads anonymous. The capability-seam rule (interface/implementation/consumer as separate packages) already names this shape; the transport is genuinely two-sided.
|
||||
|
||||
**Have `subagent-sdk` speak raw JSON-RPC without the client SDK.** Duplicates the request/notification pairing, subscription fan-out, timeout, and teardown logic the SDK exists to own; the user's ask was explicitly a backend that *uses* the SDK, and the layering earns its keep by making the backend ~200 lines of policy over a reusable client.
|
||||
**Have `subagent-dsh-sdk` speak raw JSON-RPC without the client SDK.** Duplicates the request/notification pairing, subscription fan-out, timeout, and teardown logic the SDK exists to own; the user's ask was explicitly a backend that *uses* the SDK, and the layering earns its keep by making the backend ~200 lines of policy over a reusable client.
|
||||
|
||||
**Fold the SDK backend into `subagent-acp` with a transport switch.** The two backends share the subprocess lifecycle but nothing about the wire (ACP SDK connection vs harness JSON-RPC), the child contract (any ACP agent vs a harness runtime), or the result extraction (`agent_message_chunk` accumulation vs session-event reading). A config discriminant would bury two protocols in one package; the shared parts are exactly what `subagent-subprocess` already holds, so that library grew instead.
|
||||
**Fold the SDK backend into `subagent-acp` with a transport switch.** The two backends share the subprocess lifecycle but nothing about the wire (ACP SDK connection vs harness JSON-RPC), the child contract (any ACP agent vs a harness runtime), or the result extraction (`agent_message_chunk` accumulation vs session-event reading). A config discriminant would bury two protocols in one package; the genuinely shared provider-side parts moved into the subagent seam's `out-of-process.ts`, and the process mechanics live in the `dsh-subprocess` seam.
|
||||
|
||||
**Give the TS SDK bundled-runtime resolution parity with Python.** Python's carrier resolution exists to ship wheels to users without Node. A TypeScript consumer definitionally has Node and (in-repo) the workspace; inventing a distribution story with no consumer violates the require-current-need rule. Deferred until a real npm-distribution consumer appears.
|
||||
|
||||
|
||||
+8
-8
@@ -13,9 +13,9 @@ stdio JSON-RPC 服务表面(`@deepseek-ai/dsh-jsonrpc`,见[单文件可执
|
||||
三个包,分层与既有 Python 栈完全一致,外加一个接缝注册:
|
||||
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`**(`packages/sdk/sdk-protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport` 从 `dsh-jsonrpc` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result`、`SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data` 的 `JsonRpcResponseError` 拒绝(Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
|
||||
- **`@deepseek-ai/dsh-sdk-client`**(`packages/sdk/sdk-client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(生成、分帧、通知扇出、有类型的错误表面、经共享处置阶梯关闭至静止)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`、`run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费者的发行问题);`env` 整体替换而非合并(凭据策略归调用方;`buildChildEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`(Python 只暴露 `status`);拆除复用 `disposeChildProcess` 而不是手写 terminate/kill。
|
||||
- **`@deepseek-ai/dsh-subagent-sdk`**(`packages/subagent/subagent-sdk/`)—— 第二个进程外 `SubagentProvider`,以 `subagent-acp` 的同胞结构组织:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样的经 `onError` 汇把结果压平为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未跑回合就尘埃落定的子进程,都是 `error`)。其 `provider`/`model` 配置喂给子进程的 `initialize`;`env` 是部署传入子进程自有密钥与 `DSH_CORDIS_CONFIG` 的地方。
|
||||
- **`dsh-subagent-subprocess` 增长出第三个共享关注点**:子进程 cwd 解析(`assertUsableCwd`/`validateConfiguredCwd`/`resolveChildCwd`),在 SDK 后端需要与 `subagent-acp` 完全相同的"配置覆盖、否则父会话 cwd"策略时从后者提取,以前缀参数化诊断信息。
|
||||
- **`@deepseek-ai/dsh-sdk-client`**(`packages/sdk/sdk-client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(生成、分帧、通知扇出、有类型的错误表面、经共享处置阶梯关闭至静止)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`、`run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费者的发行问题);`env` 整体替换而非合并(凭据策略归调用方;subprocess 接缝的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`(Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,以 `subagent-acp` 的同胞结构组织:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样的经 `onError` 汇把结果压平为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未跑回合就尘埃落定的子进程,都是 `error`)。其 `provider`/`model` 配置喂给子进程的 `initialize`;`env` 是部署传入子进程自有密钥与 `DSH_CORDIS_CONFIG` 的地方。
|
||||
- **subagent 接缝增长出 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境擦除、进程树拆除)属于 `dsh-subprocess` 接缝;`subagent-acp` 经 `ctx.subprocess` 生成子进程,本后端则经 SDK 客户端生成(subprocess README 记载的 SDK 托管传输例外)并自行应用接缝的 `scrubbedParentEnv()`。
|
||||
|
||||
`dsh-jsonrpc` 的服务不变(线上字节完全一致);`dsh-jsonrpc-agent-pkg`(Python 运行时闭包)增加 `dsh-sdk-protocol` 一行依赖。
|
||||
|
||||
@@ -23,18 +23,18 @@ stdio JSON-RPC 服务表面(`@deepseek-ai/dsh-jsonrpc`,见[单文件可执
|
||||
|
||||
四层,依[测试政策](../../../../docs/testing.md):
|
||||
|
||||
- **免密钥单元** —— `sdk-client` 通过真实 stdio 驱动脚本化伪运行时(`tests/fake-runtime.ts`,环境变量脚本化、纯协议——即 Python `test_client.py` 的模式);`subagent-sdk` 经真实 provider 驱动同一伪运行时。三个包全部 100% 逐文件覆盖。
|
||||
- **免密钥 Loader 组合** —— `subagent-sdk/tests/loader-composition.e2e.ts` 启动仅测试用 cordis.yml(`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/`),其中子进程是真实的第二个 harness 运行时、带自己的 cordis.yml;断言父工具结果与子进程自己持久化的转录都携带父会话 cwd。子启动经 `resolveExampleLaunch` 解析,src/lib 两种模式都成立。
|
||||
- **免密钥单元** —— `sdk-client` 通过真实 stdio 驱动脚本化伪运行时(`tests/fake-runtime.ts`,环境变量脚本化、纯协议——即 Python `test_client.py` 的模式);`subagent-dsh-sdk` 经真实 provider 驱动同一伪运行时。三个包全部 100% 逐文件覆盖。
|
||||
- **免密钥 Loader 组合** —— `subagent-dsh-sdk/tests/loader-composition.e2e.ts` 启动仅测试用 cordis.yml(`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/`),其中子进程是真实的第二个 harness 运行时、带自己的 cordis.yml;断言父工具结果与子进程自己持久化的转录都携带父会话 cwd。子启动经 `resolveExampleLaunch` 解析,src/lib 两种模式都成立。
|
||||
- **免密钥快照** —— `examples/jsonrpc-agent/tests/sdk.snapshot.ts` 是 jsonrpc 示例的第一个快照套件:真实 `dsh-jsonrpc-agent` 运行时经真实 `dsh-sdk-client` 驱动,在新的 `cordis.snapshot.yml` 覆盖层后经 `llm-replay` 回放已录制夹具(经 `DSH_CORDIS_CONFIG` 显式传入;jsonrpc bin 自身不做快照配置切换)。三个场景——文本回合、bash 工具、spawn 子代理——各自钉住规范化通知流、SDK 回合结果与持久化的父+子日志。这也补上了单文件可执行 Note 的 Python 侧快照在 vitest 侧留下的协议层缺口。
|
||||
- **带密钥 e2e** —— 快照套件的 `DSH_SNAPSHOT=record` 模式即真实 API 路径(已提交夹具由它产出);组合 e2e 设计上无需密钥。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**从 `dsh-jsonrpc` 导入线类型而不是提取协议包。** 会让每个 SDK 消费者(包括绝不能提供 JSON-RPC 服务的 `subagent-sdk`)依赖服务器插件及其 `dsh-agent`/`dsh-llm-deepseek` peer 集合,且通知载荷仍然匿名。能力接缝规则(接口/实现/消费者三包分立)已经点名了这种形态;这个传输是货真价实的双边物。
|
||||
**从 `dsh-jsonrpc` 导入线类型而不是提取协议包。** 会让每个 SDK 消费者(包括绝不能提供 JSON-RPC 服务的 `subagent-dsh-sdk`)依赖服务器插件及其 `dsh-agent`/`dsh-llm-deepseek` peer 集合,且通知载荷仍然匿名。能力接缝规则(接口/实现/消费者三包分立)已经点名了这种形态;这个传输是货真价实的双边物。
|
||||
|
||||
**让 `subagent-sdk` 直说裸 JSON-RPC、绕开客户端 SDK。** 会复制 SDK 存在意义所在的请求/通知配对、订阅扇出、超时与拆除逻辑;用户的要求明确是一个*使用* SDK 的后端,分层的回报是后端成为可复用客户端之上约 200 行的纯策略。
|
||||
**让 `subagent-dsh-sdk` 直说裸 JSON-RPC、绕开客户端 SDK。** 会复制 SDK 存在意义所在的请求/通知配对、订阅扇出、超时与拆除逻辑;用户的要求明确是一个*使用* SDK 的后端,分层的回报是后端成为可复用客户端之上约 200 行的纯策略。
|
||||
|
||||
**把 SDK 后端折进 `subagent-acp`、用传输开关区分。** 两个后端共享子进程生命周期,但线协议(ACP SDK 连接 vs harness JSON-RPC)、子进程契约(任意 ACP 代理 vs harness 运行时)、结果提取(`agent_message_chunk` 累积 vs 会话事件读取)毫无共享。配置判别子会把两个协议埋进一个包;共享部分恰好就是 `subagent-subprocess` 已持有的,于是让那个库生长。
|
||||
**把 SDK 后端折进 `subagent-acp`、用传输开关区分。** 两个后端共享子进程生命周期,但线协议(ACP SDK 连接 vs harness JSON-RPC)、子进程契约(任意 ACP 代理 vs harness 运行时)、结果提取(`agent_message_chunk` 累积 vs 会话事件读取)毫无共享。配置判别子会把两个协议埋进一个包;真正共享的 provider 侧部分移入 subagent 接缝的 `out-of-process.ts`,进程机制则住在 `dsh-subprocess` 接缝。
|
||||
|
||||
**给 TS SDK 与 Python 对等的捆绑运行时解析。** Python 的载体解析是为了给没有 Node 的用户发 wheel。TypeScript 消费者定义上就有 Node 且(仓库内)有工作区;为不存在的消费者发明发行故事违反"要求当前需求"规则。推迟到真实 npm 发行消费者出现。
|
||||
|
||||
|
||||
+17
-17
@@ -1332,23 +1332,9 @@ export interface Config {
|
||||
export type PermissionPolicy = 'allow' | 'reject'
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-acp/src/index.ts:20`](../packages/subagent/subagent-acp/src/index.ts)
|
||||
Source: [`packages/subagent/subagent-acp/src/index.ts:21`](../packages/subagent/subagent-acp/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-fork`
|
||||
|
||||
Requires: `subagents`
|
||||
|
||||
```ts config-catalog
|
||||
/** Config: the registry name to register the provider under. */
|
||||
export interface Config {
|
||||
/** Provider name on `ctx.subagents` (default `fork`). */
|
||||
providerName: string
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-fork/src/index.ts:25`](../packages/subagent/subagent-fork/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-sdk`
|
||||
## `@deepseek-ai/dsh-subagent-dsh-sdk`
|
||||
|
||||
Requires: `subagents`
|
||||
|
||||
@@ -1395,7 +1381,21 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-sdk/src/index.ts:29`](../packages/subagent/subagent-sdk/src/index.ts)
|
||||
Source: [`packages/subagent/subagent-dsh-sdk/src/index.ts:29`](../packages/subagent/subagent-dsh-sdk/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-fork`
|
||||
|
||||
Requires: `subagents`
|
||||
|
||||
```ts config-catalog
|
||||
/** Config: the registry name to register the provider under. */
|
||||
export interface Config {
|
||||
/** Provider name on `ctx.subagents` (default `fork`). */
|
||||
providerName: string
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-fork/src/index.ts:25`](../packages/subagent/subagent-fork/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-spawn`
|
||||
|
||||
|
||||
@@ -797,7 +797,7 @@ A ready child settled. Scope-filtered dispatch uses the same delegating parent c
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:139`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:140`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/provider-added` — emit
|
||||
|
||||
@@ -814,7 +814,7 @@ A provider became resolvable in the registry.
|
||||
|
||||
Types: [SubagentProvider](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:113`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:114`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/provider-removed` — emit
|
||||
|
||||
@@ -829,7 +829,7 @@ A provider left the registry. Accepted runs remain holder-owned.
|
||||
'subagent/provider-removed'(name: string): void
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:119`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:120`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/start` — emit
|
||||
|
||||
@@ -851,7 +851,7 @@ A provider established a ready child. For in-process providers, `ctx.agents.get(
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:130`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:131`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
## `system-prompt/*`
|
||||
|
||||
|
||||
@@ -1581,7 +1581,7 @@ async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
|
||||
|
||||
Types: [SubagentProvider](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:180`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:181`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
## `ctx.subprocess` — `SubprocessService` (abstract seam)
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `slash/input-consume-token` | `bail` | [`packages/client/ui-slash/src/types.ts:234`](../packages/client/ui-slash/src/types.ts) | - | `ui-conversation` |
|
||||
| `slash/input-insert-reference` | `bail` | [`packages/client/ui-slash/src/types.ts:227`](../packages/client/ui-slash/src/types.ts) | - | `ui-conversation` |
|
||||
| `slash/input-insert-text` | `bail` | [`packages/client/ui-slash/src/types.ts:242`](../packages/client/ui-slash/src/types.ts) | - | `ui-conversation` |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:113`](../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:119`](../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:130`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:114`](../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:120`](../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:131`](../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`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:156`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
|
||||
|
||||
+30
-8
@@ -61,6 +61,7 @@ flowchart TD
|
||||
subgraph group_subagent["packages/subagent"]
|
||||
pkg_subagent["subagent"]
|
||||
pkg_subagent_acp["subagent-acp"]
|
||||
pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
|
||||
pkg_subagent_fork["subagent-fork"]
|
||||
pkg_subagent_inprocess["subagent-inprocess"]
|
||||
pkg_subagent_spawn["subagent-spawn"]
|
||||
@@ -203,6 +204,8 @@ flowchart TD
|
||||
subgraph group_sdk["packages/sdk"]
|
||||
pkg_helper["helper"]
|
||||
pkg_scripts["scripts"]
|
||||
pkg_sdk_client["sdk-client"]
|
||||
pkg_sdk_protocol["sdk-protocol"]
|
||||
pkg_telemetry["telemetry"]
|
||||
end
|
||||
subgraph group_storage["packages/storage"]
|
||||
@@ -752,13 +755,6 @@ flowchart TD
|
||||
pkg_hooks_claude --> pkg_session_persistence
|
||||
pkg_hooks_claude --> pkg_subagent
|
||||
pkg_hooks_claude --> pkg_tools
|
||||
pkg_jsonrpc --> pkg_agent
|
||||
pkg_jsonrpc --> pkg_invariants
|
||||
pkg_jsonrpc --> pkg_llm
|
||||
pkg_jsonrpc --> pkg_llm_deepseek
|
||||
pkg_jsonrpc --> pkg_scope
|
||||
pkg_jsonrpc --> pkg_session
|
||||
pkg_jsonrpc --> pkg_subagent
|
||||
pkg_tui --> pkg_agent
|
||||
pkg_tui --> pkg_agent_loop
|
||||
pkg_tui --> pkg_commands
|
||||
@@ -797,6 +793,10 @@ flowchart TD
|
||||
pkg_agent_spine_demo --> pkg_tool_tasks
|
||||
pkg_agent_spine_demo --> pkg_tools
|
||||
pkg_agent_spine_demo --> pkg_workspace_context
|
||||
pkg_sdk_protocol --> pkg_invariants
|
||||
pkg_sdk_protocol --> pkg_llm
|
||||
pkg_sdk_protocol --> pkg_session
|
||||
pkg_sdk_protocol --> pkg_subagent
|
||||
pkg_tool_ralph --> pkg_agent
|
||||
pkg_tool_ralph --> pkg_invariants
|
||||
pkg_tool_ralph --> pkg_llm
|
||||
@@ -820,6 +820,14 @@ flowchart TD
|
||||
pkg_subagent_spawn --> pkg_invariants
|
||||
pkg_subagent_spawn --> pkg_subagent
|
||||
pkg_subagent_spawn --> pkg_subagent_inprocess
|
||||
pkg_jsonrpc --> pkg_agent
|
||||
pkg_jsonrpc --> pkg_invariants
|
||||
pkg_jsonrpc --> pkg_llm
|
||||
pkg_jsonrpc --> pkg_llm_deepseek
|
||||
pkg_jsonrpc --> pkg_scope
|
||||
pkg_jsonrpc --> pkg_sdk_protocol
|
||||
pkg_jsonrpc --> pkg_session
|
||||
pkg_jsonrpc --> pkg_subagent
|
||||
pkg_acp_demo --> pkg_acp
|
||||
pkg_acp_demo --> pkg_agent_spine_demo
|
||||
pkg_acp_demo --> pkg_app_boot
|
||||
@@ -858,6 +866,17 @@ flowchart TD
|
||||
pkg_tui_demo --> pkg_tui
|
||||
pkg_tui_demo --> pkg_user_interaction
|
||||
pkg_tui_demo --> pkg_workspace_context
|
||||
pkg_sdk_client --> pkg_invariants
|
||||
pkg_sdk_client --> pkg_llm
|
||||
pkg_sdk_client --> pkg_sdk_protocol
|
||||
pkg_sdk_client --> pkg_session
|
||||
pkg_subagent_dsh_sdk --> pkg_agent
|
||||
pkg_subagent_dsh_sdk --> pkg_invariants
|
||||
pkg_subagent_dsh_sdk --> pkg_llm
|
||||
pkg_subagent_dsh_sdk --> pkg_sdk_client
|
||||
pkg_subagent_dsh_sdk --> pkg_session
|
||||
pkg_subagent_dsh_sdk --> pkg_subagent
|
||||
pkg_subagent_dsh_sdk --> pkg_subprocess
|
||||
```
|
||||
|
||||
| Package | Group | Depends on |
|
||||
@@ -998,13 +1017,16 @@ flowchart TD
|
||||
| [`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) |
|
||||
| [`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-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`tui`](../packages/ui/tui) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`commands`](../packages/ui/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference), [`session-title`](../packages/session-title/session-title), [`skill`](../packages/skill/skill), [`system-prompt`](../packages/core/system-prompt), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
|
||||
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`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-title/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) |
|
||||
| [`sdk-protocol`](../packages/sdk/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) |
|
||||
| [`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) |
|
||||
| [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`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/sdk-protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| [`cli-demo`](../packages/examples/cli-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| [`tui-demo`](../packages/examples/tui-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`command-goal`](../packages/goal/command-goal), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`session-reference`](../packages/context/session-reference), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`tui`](../packages/ui/tui), [`user-interaction`](../packages/ui/user-interaction), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| [`sdk-client`](../packages/sdk/sdk-client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/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/sdk-client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
+2
-2
@@ -14,8 +14,8 @@
|
||||
|
||||
# providerName is omitted: the composition exercises the shipped default
|
||||
# (`dsh-sdk`) through the real Loader.
|
||||
- id: subagent-sdk
|
||||
name: '@deepseek-ai/dsh-subagent-sdk'
|
||||
- id: subagent-dsh-sdk
|
||||
name: '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
config:
|
||||
command: !!js process.env.DSH_TEST_CHILD_COMMAND
|
||||
args: !!js JSON.parse(process.env.DSH_TEST_CHILD_ARGS ?? '[]')
|
||||
@@ -48,7 +48,7 @@
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-acp": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-sdk": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-dsh-sdk": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-spawn": "workspace:*",
|
||||
"@deepseek-ai/dsh-tasks-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-time-context": "workspace:*",
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
"tui-agent/tests/fixtures/tui-scripted-llm.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/driver.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/child-mock-llm.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/mock-delegating-llm.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/child-mock-llm.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts",
|
||||
"*/tests/**/*.e2e.ts",
|
||||
"*/tests/**/*.snapshot.ts"
|
||||
],
|
||||
@@ -600,7 +600,7 @@
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/subagent/subagent-sdk": {
|
||||
"packages/subagent/subagent-dsh-sdk": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
"tests/**/*.e2e.ts"
|
||||
|
||||
@@ -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 packages/sdk/sdk-client/README.md
|
||||
README.md: 3945f911990fa3df362581b0fb37389110bdd386
|
||||
README.zh.md: 3814b88aab1b10c96fdf809565994f29b8e8026b
|
||||
README.md: e2aaf08212307bfac0c73b5e838679a7a750a92a
|
||||
README.zh.md: cbefae59d95cc0cb9d89145ad3f2ee3248822714
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
The TypeScript client SDK for driving a DeepSeek Harness runtime as a subprocess over stdio JSON-RPC — the design twin of the [Python SDK](../../../python/README.md) (`deepseek-harness`), sharing the same runtime peer, protocol, and layering: `DeepSeekHarness` is the high-level turns API, `HarnessClient` the lower-level protocol client. A pure library: it registers nothing on a Cordis context; the runtime process it spawns is a complete harness whose composition its own `cordis.yml` decides.
|
||||
|
||||
Unlike the Python SDK, the launch spec is fully explicit (`command`/`args`): this package is for repo-adjacent TypeScript consumers — the [`dsh-subagent-sdk`](../../subagent/subagent-sdk/README.md) backend, tests, automation — which know which runtime they are launching. Bundled-runtime resolution (finding a packaged executable) remains the Python distribution's concern.
|
||||
Unlike the Python SDK, the launch spec is fully explicit (`command`/`args`): this package is for repo-adjacent TypeScript consumers — the [`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.md) backend, tests, automation — which know which runtime they are launching. Bundled-runtime resolution (finding a packaged executable) remains the Python distribution's concern.
|
||||
|
||||
## DeepSeekHarness
|
||||
|
||||
@@ -26,9 +26,9 @@ The subprocess starts lazily on first use and stays owned by the instance across
|
||||
|
||||
The protocol client under the turns API: explicit `start()`/`initialize()`/`prompt()`/`request()`/`close()`, plus notification subscriptions. `subscribe(filter?)` returns a `NotificationSubscription` (awaitable `next()`, non-blocking `tryNext()`, async iteration); `subscribeSessionTree(id)` scopes to one session and the descendants discovered from `subagent.started` lineage edges — the runtime notifies for every session in its context, and scoping is client-side, exactly like the Python SDK. Error surfaces are typed: `JsonRpcResponseError` (wire error response, code/data preserved), `RequestTimeoutError` (a configured bound elapsed; there is no wire-level cancel, so the request keeps running server-side until close), `SdkProtocolError` (a response outside the documented protocol), `TransportClosedError` (the runtime is gone — message carries the exit code and a bounded stderr tail).
|
||||
|
||||
`close()` requests protocol `shutdown` (bounded by `shutdownTimeoutMs`, default 1000 ms), then walks the shared stdin-EOF → SIGTERM → SIGKILL [dispose ladder](../../subagent/subagent-subprocess/README.md) (`disposeEofGraceMs` default 6000, `disposeGraceMs` default 3000) until the process has actually exited. It is idempotent, and a closed client refuses reuse.
|
||||
`close()` requests protocol `shutdown` (bounded by `shutdownTimeoutMs`, default 1000 ms), then walks a stdin-EOF → SIGTERM → SIGKILL ladder (`disposeEofGraceMs` default 6000, `disposeGraceMs` default 3000) until the process has actually exited. The ladder is private to this client: it runs outside any harness context, so it cannot ride the [`dsh-subprocess`](../../subprocess/README.md) service — the seam's documented exception for SDK-managed transports. It is idempotent, and a closed client refuses reuse.
|
||||
|
||||
`HarnessClientOptions.env` replaces the child environment entirely when given (`undefined` inherits the parent's); callers own credential policy — `buildChildEnv` from `dsh-subagent-subprocess` is the scrub-then-inject helper for isolation-minded launches.
|
||||
`HarnessClientOptions.env` replaces the child environment entirely when given (`undefined` inherits the parent's); callers own credential policy — `scrubbedParentEnv` from `dsh-subprocess` is the shared scrub base for isolation-minded launches.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层回合 API,`HarnessClient` 是低层协议客户端。纯库:不在任何 Cordis 上下文注册;它所生成的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
|
||||
|
||||
与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费者——[`dsh-subagent-sdk`](../../subagent/subagent-sdk/README.md) 后端、测试、自动化——它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
|
||||
与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费者——[`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.md) 后端、测试、自动化——它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
|
||||
|
||||
## DeepSeekHarness
|
||||
|
||||
@@ -26,9 +26,9 @@ console.log(result.status, result.finalResponse)
|
||||
|
||||
回合 API 之下的协议客户端:显式 `start()`/`initialize()`/`prompt()`/`request()`/`close()`,外加通知订阅。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。错误表面有类型:`JsonRpcResponseError`(线上错误响应,保留 code/data)、`RequestTimeoutError`(配置的时限已到;线上没有取消方法,请求在服务端继续运行直到 close)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。
|
||||
|
||||
`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走共享的 stdin-EOF → SIGTERM → SIGKILL [处置阶梯](../../subagent/subagent-subprocess/README.md)(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。幂等,已关闭的客户端拒绝复用。
|
||||
`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。该阶梯为本客户端私有:它运行在任何 harness 上下文之外,无法搭乘 [`dsh-subprocess`](../../subprocess/README.md) 服务——即该接缝记载的 SDK 托管传输例外。幂等,已关闭的客户端拒绝复用。
|
||||
|
||||
`HarnessClientOptions.env` 给定时整体替换子环境(`undefined` 原样继承父环境);凭据策略归调用方——`dsh-subagent-subprocess` 的 `buildChildEnv` 是面向隔离启动的先擦除后注入助手。
|
||||
`HarnessClientOptions.env` 给定时整体替换子环境(`undefined` 原样继承父环境);凭据策略归调用方——`dsh-subprocess` 的 `scrubbedParentEnv` 是面向隔离启动的共享擦除基底。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type SessionPromptParams,
|
||||
} from '@deepseek-ai/dsh-sdk-protocol'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { disposeRuntimeProcess } from './dispose.ts'
|
||||
import type { HarnessClientOptions, HarnessNotification, NotificationFilter } from './types.ts'
|
||||
|
||||
/** Retained stderr lines used to diagnose an unexpected runtime death. */
|
||||
@@ -447,91 +448,6 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Race the child's exit against a timer. Neither outcome leaves anything
|
||||
* behind on the child: the exit listener is removed on timeout and the timer
|
||||
* is cleared on exit, so the ladder's tiers never accumulate listeners.
|
||||
*/
|
||||
function exitsWithin(child: ChildProcess, ms: number): Promise<boolean> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true)
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const onExit = (): void => {
|
||||
clearTimeout(timer)
|
||||
resolve(true)
|
||||
}
|
||||
// `.unref()` so a pending grace timer never keeps the parent's loop alive.
|
||||
const timer = setTimeout(() => {
|
||||
child.removeListener('exit', onExit)
|
||||
resolve(false)
|
||||
}, ms).unref()
|
||||
child.once('exit', onExit)
|
||||
})
|
||||
}
|
||||
|
||||
/** Force-terminate the runtime and reject if no exit edge arrives within the grace. */
|
||||
function forceTerminateWithin(child: ChildProcess, ms: number): Promise<void> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let accepted = false
|
||||
let settled = false
|
||||
const cleanup = (): void => {
|
||||
clearTimeout(timer)
|
||||
child.off('exit', onExit)
|
||||
child.off('error', onError)
|
||||
}
|
||||
const settle = (complete: () => void): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
complete()
|
||||
}
|
||||
const onExit = (): void => { settle(resolve) }
|
||||
const onError = (error: Error): void => { settle(() => { reject(error) }) }
|
||||
child.once('exit', onExit)
|
||||
child.once('error', onError)
|
||||
const timer = setTimeout(() => {
|
||||
const disposition = accepted ? 'accepted' : 'refused'
|
||||
settle(() => {
|
||||
reject(new Error(`runtime process did not exit within ${ms}ms after SIGKILL was ${disposition}`))
|
||||
})
|
||||
}, ms).unref()
|
||||
try {
|
||||
accepted = child.kill('SIGKILL')
|
||||
if (child.exitCode !== null || child.signalCode !== null) settle(resolve)
|
||||
} catch (error: unknown) {
|
||||
settle(() => { reject(new Error('SIGKILL failed', { cause: error })) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear the runtime down to quiescence, resolving only after exit: close stdin
|
||||
* and allow cooperative flush, then use the host's graceful and forced
|
||||
* termination semantics. POSIX sends `SIGTERM` before `SIGKILL`; Windows
|
||||
* skips directly to forced termination because Node maps both signals to
|
||||
* `TerminateProcess`.
|
||||
* @throws When forced termination errors or the child does not report exit
|
||||
* within `disposeGraceMs`.
|
||||
*/
|
||||
async function disposeRuntimeProcess(
|
||||
child: ChildProcess,
|
||||
graces: { disposeEofGraceMs: number; disposeGraceMs: number },
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Promise<void> {
|
||||
// Already gone: nothing to reap.
|
||||
if (child.exitCode !== null || child.signalCode !== null) return
|
||||
// 1. Close stdin and allow cooperative teardown and durable-state flush.
|
||||
child.stdin?.end()
|
||||
if (await exitsWithin(child, graces.disposeEofGraceMs)) return
|
||||
// 2. POSIX gets a catchable graceful signal; Windows signals all force-terminate.
|
||||
if (platform !== 'win32') {
|
||||
child.kill('SIGTERM')
|
||||
if (await exitsWithin(child, graces.disposeGraceMs)) return
|
||||
}
|
||||
// 3. Force-kill and await a bounded exit edge.
|
||||
await forceTerminateWithin(child, graces.disposeGraceMs)
|
||||
}
|
||||
|
||||
/** The message of a thrown value (the transport only throws `Error`s; `String` covers the rest). */
|
||||
function errorMessage(error: unknown): string {
|
||||
/* v8 ignore next -- the transport and dispose ladder reject only with Errors */
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Private teardown ladder for the runtime subprocess: stdin EOF (cooperative
|
||||
* quiesce), then SIGTERM, then SIGKILL, resolving only after the process has
|
||||
* actually exited. The SDK client runs OUTSIDE any harness context, so it
|
||||
* cannot ride the `dsh-subprocess` service — this module is the seam's
|
||||
* documented exception for SDK-managed transports.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-sdk-client/dispose
|
||||
*/
|
||||
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
|
||||
/**
|
||||
* Race the child's exit against a timer. Neither outcome leaves anything
|
||||
* behind on the child: the exit listener is removed on timeout and the timer
|
||||
* is cleared on exit, so the ladder's tiers never accumulate listeners.
|
||||
*/
|
||||
function exitsWithin(child: ChildProcess, ms: number): Promise<boolean> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true)
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const onExit = (): void => {
|
||||
clearTimeout(timer)
|
||||
resolve(true)
|
||||
}
|
||||
// `.unref()` so a pending grace timer never keeps the parent's loop alive.
|
||||
const timer = setTimeout(() => {
|
||||
child.removeListener('exit', onExit)
|
||||
resolve(false)
|
||||
}, ms).unref()
|
||||
child.once('exit', onExit)
|
||||
})
|
||||
}
|
||||
|
||||
/** Force-terminate the runtime and reject if no exit edge arrives within the grace. */
|
||||
function forceTerminateWithin(child: ChildProcess, ms: number): Promise<void> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let accepted = false
|
||||
let settled = false
|
||||
const cleanup = (): void => {
|
||||
clearTimeout(timer)
|
||||
child.off('exit', onExit)
|
||||
child.off('error', onError)
|
||||
}
|
||||
const settle = (complete: () => void): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
complete()
|
||||
}
|
||||
const onExit = (): void => { settle(resolve) }
|
||||
const onError = (error: Error): void => { settle(() => { reject(error) }) }
|
||||
child.once('exit', onExit)
|
||||
child.once('error', onError)
|
||||
const timer = setTimeout(() => {
|
||||
const disposition = accepted ? 'accepted' : 'refused'
|
||||
settle(() => {
|
||||
reject(new Error(`runtime process did not exit within ${ms}ms after SIGKILL was ${disposition}`))
|
||||
})
|
||||
}, ms).unref()
|
||||
try {
|
||||
accepted = child.kill('SIGKILL')
|
||||
if (child.exitCode !== null || child.signalCode !== null) settle(resolve)
|
||||
} catch (error: unknown) {
|
||||
settle(() => { reject(new Error('SIGKILL failed', { cause: error })) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear the runtime down to quiescence, resolving only after exit: close stdin
|
||||
* and allow cooperative flush, then use the host's graceful and forced
|
||||
* termination semantics. POSIX sends `SIGTERM` before `SIGKILL`; Windows
|
||||
* skips directly to forced termination because Node maps both signals to
|
||||
* `TerminateProcess`.
|
||||
* @param child - the runtime child process to tear down.
|
||||
* @param graces - the EOF and termination-confirmation windows (ms).
|
||||
* @param platform - the host platform, injectable for unit coverage.
|
||||
* @throws When forced termination errors or the child does not report exit
|
||||
* within `disposeGraceMs`.
|
||||
*/
|
||||
export async function disposeRuntimeProcess(
|
||||
child: ChildProcess,
|
||||
graces: { disposeEofGraceMs: number; disposeGraceMs: number },
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Promise<void> {
|
||||
// Already gone: nothing to reap.
|
||||
if (child.exitCode !== null || child.signalCode !== null) return
|
||||
// 1. Close stdin and allow cooperative teardown and durable-state flush.
|
||||
child.stdin?.end()
|
||||
if (await exitsWithin(child, graces.disposeEofGraceMs)) return
|
||||
// 2. POSIX gets a catchable graceful signal; Windows signals all force-terminate.
|
||||
if (platform !== 'win32') {
|
||||
child.kill('SIGTERM')
|
||||
if (await exitsWithin(child, graces.disposeGraceMs)) return
|
||||
}
|
||||
// 3. Force-kill and await a bounded exit edge.
|
||||
await forceTerminateWithin(child, graces.disposeGraceMs)
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Deterministic ladder coverage against a scriptable fake child: each
|
||||
* escalation tier's timing is driven exactly (the client suite exercises the
|
||||
* same ladder against real subprocesses end to end).
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events'
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { disposeRuntimeProcess } from '../src/dispose.ts'
|
||||
|
||||
/** What fells a scripted {@link FakeChild}. */
|
||||
type LethalTrigger = 'eof' | NodeJS.Signals
|
||||
|
||||
/** Per-scenario script for a {@link FakeChild}. */
|
||||
interface FakeChildScript {
|
||||
/**
|
||||
* The one trigger that makes the child exit (SIGKILL always does,
|
||||
* uncatchable, like a real process). Omitted: only SIGKILL fells it.
|
||||
*/
|
||||
diesOn?: LethalTrigger
|
||||
/** Delay (ms) between the lethal trigger and the exit event. */
|
||||
delayMs?: number
|
||||
/** Complete the scripted exit inside the triggering call. */
|
||||
synchronousExit?: boolean
|
||||
/** `false` models a child spawned without a stdin pipe. */
|
||||
stdin?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A scriptable stand-in for a ChildProcess carrying exactly the surface the
|
||||
* ladder reads: `exitCode`/`signalCode`, `stdin.end()`, `kill()`, and the
|
||||
* `exit` event.
|
||||
*/
|
||||
class FakeChild extends EventEmitter {
|
||||
exitCode: number | null = null
|
||||
signalCode: NodeJS.Signals | null = null
|
||||
readonly kills: NodeJS.Signals[] = []
|
||||
stdinEnded = false
|
||||
readonly stdin: { end: () => void } | null
|
||||
|
||||
constructor(private readonly script: FakeChildScript = {}) {
|
||||
super()
|
||||
this.stdin = script.stdin === false
|
||||
? null
|
||||
: { end: () => { this.stdinEnded = true; this.maybeDie('eof') } }
|
||||
}
|
||||
|
||||
kill(signal: NodeJS.Signals): boolean {
|
||||
this.kills.push(signal)
|
||||
this.maybeDie(signal)
|
||||
return true
|
||||
}
|
||||
|
||||
private maybeDie(trigger: LethalTrigger): void {
|
||||
// SIGKILL is uncatchable — it always fells the child; any other trigger
|
||||
// only when the scenario scripts it as the lethal one.
|
||||
if (trigger !== 'SIGKILL' && this.script.diesOn !== trigger) return
|
||||
const exit = (): void => {
|
||||
if (trigger === 'eof') this.exitCode = 0
|
||||
else this.signalCode = trigger
|
||||
this.emit('exit', this.exitCode, this.signalCode)
|
||||
}
|
||||
if (this.script.synchronousExit === true) exit()
|
||||
else setTimeout(exit, this.script.delayMs ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
/** The ladder takes a real ChildProcess; the fake carries the read surface. */
|
||||
function asChild(fake: FakeChild): ChildProcess {
|
||||
return fake as unknown as ChildProcess
|
||||
}
|
||||
|
||||
describe('disposeRuntimeProcess', () => {
|
||||
it('returns immediately for an already-exited child (no EOF, no signals)', async () => {
|
||||
const fake = new FakeChild()
|
||||
fake.exitCode = 0
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 1000, disposeGraceMs: 1000 })
|
||||
expect(fake.stdinEnded).toBe(false)
|
||||
expect(fake.kills).toEqual([])
|
||||
})
|
||||
|
||||
it('returns immediately for a child already dead by signal', async () => {
|
||||
const fake = new FakeChild()
|
||||
fake.signalCode = 'SIGKILL'
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 1000, disposeGraceMs: 1000 })
|
||||
expect(fake.stdinEnded).toBe(false)
|
||||
expect(fake.kills).toEqual([])
|
||||
})
|
||||
|
||||
it('tier 1: a cooperative child quiesces on stdin EOF — no signal is ever sent', async () => {
|
||||
const fake = new FakeChild({ diesOn: 'eof', delayMs: 5 })
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 1000, disposeGraceMs: 1000 })
|
||||
expect(fake.stdinEnded).toBe(true)
|
||||
expect(fake.kills).toEqual([])
|
||||
expect(fake.exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it('recognizes a child that exits synchronously on stdin EOF', async () => {
|
||||
const fake = new FakeChild({ diesOn: 'eof', synchronousExit: true })
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 1000, disposeGraceMs: 1000 })
|
||||
expect(fake.exitCode).toBe(0)
|
||||
expect(fake.listenerCount('exit')).toBe(0)
|
||||
})
|
||||
|
||||
it('tier 2: a child that ignores EOF but honors SIGTERM dies on the middle rung', async () => {
|
||||
const fake = new FakeChild({ diesOn: 'SIGTERM', delayMs: 5 })
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 20, disposeGraceMs: 1000 }, 'linux')
|
||||
expect(fake.stdinEnded).toBe(true)
|
||||
expect(fake.kills).toEqual(['SIGTERM'])
|
||||
expect(fake.signalCode).toBe('SIGTERM')
|
||||
expect(fake.listenerCount('exit')).toBe(0)
|
||||
})
|
||||
|
||||
it('recognizes a child that exits synchronously on SIGTERM', async () => {
|
||||
const fake = new FakeChild({ diesOn: 'SIGTERM', synchronousExit: true })
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 20, disposeGraceMs: 1000 }, 'linux')
|
||||
expect(fake.kills).toEqual(['SIGTERM'])
|
||||
expect(fake.signalCode).toBe('SIGTERM')
|
||||
expect(fake.listenerCount('exit')).toBe(0)
|
||||
})
|
||||
|
||||
it('tier 3: a SIGTERM-trapping child is SIGKILLed, and dispose resolves only after the exit', async () => {
|
||||
const fake = new FakeChild({ delayMs: 5 }) // only SIGKILL fells it
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 20, disposeGraceMs: 20 }, 'linux')
|
||||
expect(fake.kills).toEqual(['SIGTERM', 'SIGKILL'])
|
||||
// Quiescence, not a request: at resolution the child has ACTUALLY exited
|
||||
// (the exit event landed, despite the scripted post-SIGKILL delay).
|
||||
expect(fake.signalCode).toBe('SIGKILL')
|
||||
})
|
||||
|
||||
it('recognizes a child already gone when the final exit wait begins', async () => {
|
||||
const fake = new FakeChild({ synchronousExit: true })
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 20, disposeGraceMs: 20 }, 'linux')
|
||||
expect(fake.kills).toEqual(['SIGTERM', 'SIGKILL'])
|
||||
expect(fake.signalCode).toBe('SIGKILL')
|
||||
})
|
||||
|
||||
it.each(['exitCode', 'signalCode'] as const)('accepts a late OS %s marker before the final forced wait', async (marker) => {
|
||||
const fake = new FakeChild()
|
||||
vi.spyOn(fake, 'kill').mockImplementation((signal) => {
|
||||
fake.kills.push(signal)
|
||||
queueMicrotask(() => {
|
||||
if (marker === 'exitCode') fake.exitCode = 0
|
||||
else fake.signalCode = 'SIGTERM'
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 1, disposeGraceMs: 10 }, 'linux')
|
||||
expect(fake.kills).toEqual(['SIGTERM'])
|
||||
})
|
||||
|
||||
it('walks the ladder for a child spawned without a stdin pipe', async () => {
|
||||
const fake = new FakeChild({ stdin: false, diesOn: 'SIGTERM', delayMs: 5 })
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 20, disposeGraceMs: 1000 }, 'linux')
|
||||
expect(fake.kills).toEqual(['SIGTERM'])
|
||||
})
|
||||
|
||||
it('skips the redundant SIGTERM tier on Windows and awaits forced exit', async () => {
|
||||
const fake = new FakeChild({ diesOn: 'SIGTERM', delayMs: 5 })
|
||||
await disposeRuntimeProcess(asChild(fake), { disposeEofGraceMs: 20, disposeGraceMs: 1000 }, 'win32')
|
||||
expect(fake.kills).toEqual(['SIGKILL'])
|
||||
expect(fake.signalCode).toBe('SIGKILL')
|
||||
})
|
||||
|
||||
it('propagates a forced-termination error without waiting for the grace', async () => {
|
||||
const fake = new FakeChild()
|
||||
const failure = Object.assign(new Error('kill EPERM'), { code: 'EPERM' })
|
||||
vi.spyOn(fake, 'kill').mockImplementation((signal) => {
|
||||
fake.kills.push(signal)
|
||||
fake.emit('error', failure)
|
||||
return false
|
||||
})
|
||||
|
||||
await expect(disposeRuntimeProcess(
|
||||
asChild(fake),
|
||||
{ disposeEofGraceMs: 1, disposeGraceMs: 1000 },
|
||||
'win32',
|
||||
)).rejects.toBe(failure)
|
||||
expect(fake.kills).toEqual(['SIGKILL'])
|
||||
expect(fake.listenerCount('error')).toBe(0)
|
||||
expect(fake.listenerCount('exit')).toBe(0)
|
||||
})
|
||||
|
||||
it('wraps a synchronous forced-termination exception and removes its listeners', async () => {
|
||||
const fake = new FakeChild()
|
||||
const failure = new Error('invalid signal state')
|
||||
vi.spyOn(fake, 'kill').mockImplementation(() => { throw failure })
|
||||
|
||||
await expect(disposeRuntimeProcess(
|
||||
asChild(fake),
|
||||
{ disposeEofGraceMs: 1, disposeGraceMs: 1000 },
|
||||
'win32',
|
||||
)).rejects.toMatchObject({ message: 'SIGKILL failed', cause: failure })
|
||||
expect(fake.listenerCount('error')).toBe(0)
|
||||
expect(fake.listenerCount('exit')).toBe(0)
|
||||
})
|
||||
|
||||
it('bounds a refused forced termination that produces no error or exit', async () => {
|
||||
const fake = new FakeChild()
|
||||
vi.spyOn(fake, 'kill').mockImplementation((signal) => {
|
||||
fake.kills.push(signal)
|
||||
return false
|
||||
})
|
||||
|
||||
await expect(disposeRuntimeProcess(
|
||||
asChild(fake),
|
||||
{ disposeEofGraceMs: 1, disposeGraceMs: 10 },
|
||||
'win32',
|
||||
)).rejects.toThrow('runtime process did not exit within 10ms after SIGKILL was refused')
|
||||
expect(fake.listenerCount('error')).toBe(0)
|
||||
expect(fake.listenerCount('exit')).toBe(0)
|
||||
})
|
||||
|
||||
it('bounds an accepted forced termination that never reports exit', async () => {
|
||||
const fake = new FakeChild()
|
||||
vi.spyOn(fake, 'kill').mockImplementation((signal) => {
|
||||
fake.kills.push(signal)
|
||||
return true
|
||||
})
|
||||
|
||||
await expect(disposeRuntimeProcess(
|
||||
asChild(fake),
|
||||
{ disposeEofGraceMs: 1, disposeGraceMs: 10 },
|
||||
'win32',
|
||||
)).rejects.toThrow('runtime process did not exit within 10ms after SIGKILL was accepted')
|
||||
expect(fake.listenerCount('error')).toBe(0)
|
||||
expect(fake.listenerCount('exit')).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +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: 8414836efd756f60258566ae3e4e00de2d4110d7
|
||||
README.zh.md: d32228495cd6c57398c88cea92ce168ecf278188
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/README.md
|
||||
README.md: fed0c3d6b252f5eeb8355c3b544066765999120a
|
||||
README.zh.md: 45f3c83f57613c16ba00063c9da9ac720a57727e
|
||||
@@ -11,9 +11,9 @@ The subagent seam: an agent delegating work to a child agent. Like the [bash](..
|
||||
| `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-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`) |
|
||||
| `subagent-dsh-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` / `subagent-sdk` backends spawn their children through the [`subprocess/`](../subprocess/README.md) seam (the shared credential scrub, tree-scoped teardown, and dispose ladder). 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-dsh-sdk` backends spawn their children through the [`subprocess/`](../subprocess/README.md) seam (the shared credential scrub, tree-scoped teardown, and dispose ladder). 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).
|
||||
@@ -11,9 +11,9 @@ subagent seam 允许 agent(智能体)把工作委派给子 agent。与 [bash
|
||||
| `subagent-spawn/` | 进程内后端:全新的子 agent | (注册到 `ctx.subagents`) |
|
||||
| `subagent-fork/` | 进程内后端:以父 agent 已完成轮次的前缀作为初始内容的子 agent | (注册到 `ctx.subagents`) |
|
||||
| `subagent-acp/` | 进程外后端:在派生子进程中运行并通过 ACP(Agent Client Protocol)驱动的子 agent | (注册到 `ctx.subagents`) |
|
||||
| `subagent-sdk/` | 进程外后端:在派生子进程中运行的子 harness 运行时,经 TypeScript SDK 客户端走 stdio JSON-RPC 驱动 | (注册到 `ctx.subagents`) |
|
||||
| `subagent-dsh-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-sdk` 后端则经由 [`subprocess/`](../subprocess/README.md) seam spawn 其子进程(共享的凭据清除、以进程树为范围的拆卸、dispose(资源释放)阶梯)。测试只用包内 fixture(测试前置数据)替换子 agent 边界。
|
||||
接口位于 `subagent/subagent/`。进程内 `subagent-spawn` / `subagent-fork` 后端共享 `subagent-inprocess` 驱动器(一个自身不提供提供方的库:两者都依赖它,彼此不依赖),进程外 `subagent-acp` / `subagent-dsh-sdk` 后端则经由 [`subprocess/`](../subprocess/README.md) seam spawn 其子进程(共享的凭据清除、以进程树为范围的拆卸、dispose(资源释放)阶梯)。测试只用包内 fixture(测试前置数据)替换子 agent 边界。
|
||||
|
||||
提案与设计理由见 [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)。
|
||||
+3
-3
@@ -1,6 +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 packages/subagent/subagent-sdk/README.md
|
||||
README.md: 92c31e4823c4f8a3b2526441ab01dc732fcecbdd
|
||||
README.zh.md: 3610bfcf93ddd2f458640e6149d398f0f7f4b173
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-dsh-sdk/README.md
|
||||
README.md: 904b70f4d197f1d5082521b519dde89b165324ef
|
||||
README.zh.md: f5879e7ae0924ac5ec2786b115bd4b9f9215c9da
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
# @deepseek-ai/dsh-subagent-sdk
|
||||
# @deepseek-ai/dsh-subagent-dsh-sdk
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
@@ -8,7 +8,7 @@ The SDK provider runs each subagent as a complete DeepSeek Harness runtime in a
|
||||
|
||||
`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 working directory resolves exactly like the ACP backend, through the seam's shared out-of-process helpers ([`dsh-subagent`](../subagent/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.
|
||||
|
||||
@@ -38,8 +38,8 @@ The provider advertises no start-time capabilities (`outputSchema`/`depthLimit`/
|
||||
| `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'
|
||||
- id: subagent-dsh-sdk
|
||||
name: '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
config:
|
||||
providerName: dsh-sdk
|
||||
command: node
|
||||
@@ -53,7 +53,7 @@ The provider advertises no start-time capabilities (`outputSchema`/`depthLimit`/
|
||||
|
||||
## 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 child environment is the [`dsh-subprocess`](../../subprocess/README.md) seam's `scrubbedParentEnv()` base — ambient credential-shaped and `DSH_*` names dropped — with explicit `config.env` values merged after the scrub. The child is spawned by the SDK client rather than through `ctx.subprocess` (the subprocess README's documented exception for SDK-managed transports), which is why this backend applies the scrub itself. 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).
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
# @deepseek-ai/dsh-subagent-sdk
|
||||
# @deepseek-ai/dsh-subagent-dsh-sdk
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
@@ -8,7 +8,7 @@ SDK provider 把每个子代理作为一个完整的 DeepSeek Harness 运行时
|
||||
|
||||
`start(request)` 先解析子进程工作目录,经 `DeepSeekHarness` 生成运行时,并在履行前完成 `initialize` 握手(携带配置的 `provider`/`model` 路由)。因此履行意味着子运行时已就绪、所有权已移交调用方。生成、握手或发布前取消的失败只在子进程被收割之后拒绝;工作目录解析失败在生成任何东西之前拒绝。
|
||||
|
||||
工作目录的解析与 ACP 后端完全一致,经由共享的 [`subagent-subprocess` 助手](../subagent-subprocess/README.md):设置了 `cwd` 覆盖则用之(加载时校验一次),否则用发起委托的父会话 cwd——绝不用服务器进程自己的 cwd。解析出的路径同时成为子进程 cwd 与其 SDK 会话的工作区 cwd。
|
||||
工作目录的解析与 ACP 后端完全一致,经由接缝共享的进程外助手([`dsh-subagent`](../subagent/README.md)):设置了 `cwd` 覆盖则用之(加载时校验一次),否则用发起委托的父会话 cwd——绝不用服务器进程自己的 cwd。解析出的路径同时成为子进程 cwd 与其 SDK 会话的工作区 cwd。
|
||||
|
||||
返回的 run id 铸造于父命名空间;子运行时的会话 id 只存在于子进程内部。发布之后,provider 跑一个 SDK 回合,并从子会话事件中读取答案:最后一条完整 `assistant/message`,或回合被截断时已累积的 `text-delta` 流——部分答案在取消与错误路径上都得以保留。
|
||||
|
||||
@@ -38,8 +38,8 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte
|
||||
| `disposeGraceMs` | `3000` | 终止后的退出确认窗口;POSIX 在 SIGTERM 之后、SIGKILL 之前也等待同样时长。 |
|
||||
|
||||
```yaml
|
||||
- id: subagent-sdk
|
||||
name: '@deepseek-ai/dsh-subagent-sdk'
|
||||
- id: subagent-dsh-sdk
|
||||
name: '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
config:
|
||||
providerName: dsh-sdk
|
||||
command: node
|
||||
@@ -53,7 +53,7 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte
|
||||
|
||||
## 进程边界
|
||||
|
||||
子环境由 [`buildChildEnv`](../subagent-subprocess/README.md) 构建:先移除形似凭据的环境变量,再应用显式 `config.env` 值。JSON-RPC 线就是真实的序列化边界。
|
||||
子环境以 [`dsh-subprocess`](../../subprocess/README.md) 接缝的 `scrubbedParentEnv()` 为基底——移除形似凭据与 `DSH_*` 的环境变量——再在擦除之后合并显式 `config.env` 值。子进程由 SDK 客户端生成而非经 `ctx.subprocess`(subprocess README 记载的 SDK 托管传输例外),因此本后端自行应用该擦除。JSON-RPC 线就是真实的序列化边界。
|
||||
|
||||
本包没有默认导出。否则 Cordis loader 解包会隐藏具名 `inject` 元数据;见[事后分析 0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)。
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-subagent-sdk",
|
||||
"name": "@deepseek-ai/dsh-subagent-dsh-sdk",
|
||||
"description": "Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
+8
-8
@@ -7,7 +7,7 @@
|
||||
* `request.parent` is the session's workspace cwd. This plugin uses named
|
||||
* exports only; a default would hide its loader metadata (see
|
||||
* `docs/postmortem/0001-acp-default-export-drops-inject.md`).
|
||||
* @module @deepseek-ai/dsh-subagent-sdk
|
||||
* @module @deepseek-ai/dsh-subagent-dsh-sdk
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type SdkRunSpec,
|
||||
} from './run.ts'
|
||||
|
||||
export const name = 'subagent-sdk'
|
||||
export const name = 'subagent-dsh-sdk'
|
||||
export const inject = ['subagents']
|
||||
|
||||
/** Config: how to spawn and drive the child SDK runtime process. */
|
||||
@@ -98,7 +98,7 @@ class SdkProvider implements SubagentProvider {
|
||||
const spec: SdkRunSpec = {
|
||||
command: this.config.command,
|
||||
args: this.config.args,
|
||||
cwd: resolveChildCwd('subagent-sdk', this.config.cwd, request.parent.session.header.cwd),
|
||||
cwd: resolveChildCwd('subagent-dsh-sdk', this.config.cwd, request.parent.session.header.cwd),
|
||||
provider: this.config.provider,
|
||||
model: this.config.model,
|
||||
env: this.config.env,
|
||||
@@ -108,7 +108,7 @@ class SdkProvider implements SubagentProvider {
|
||||
onError: (error, stopReason) => {
|
||||
// The seam forbids `result` rejecting, so a child-level failure is
|
||||
// flattened to a stop reason — preserve it here rather than losing it.
|
||||
this.ctx.logger.warn(`subagent-sdk "${this.name}": child run failed (${stopReason}): ${error.message}`)
|
||||
this.ctx.logger.warn(`subagent-dsh-sdk "${this.name}": child run failed (${stopReason}): ${error.message}`)
|
||||
},
|
||||
}
|
||||
return startSdkRun(request, spec)
|
||||
@@ -118,12 +118,12 @@ class SdkProvider implements SubagentProvider {
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
// schemastery (Config) has already filled every defaulted field.
|
||||
const resolved = config as ResolvedConfig
|
||||
assertPositiveFinite('subagent-sdk', 'shutdownTimeoutMs', resolved.shutdownTimeoutMs)
|
||||
assertPositiveFinite('subagent-sdk', 'disposeEofGraceMs', resolved.disposeEofGraceMs)
|
||||
assertPositiveFinite('subagent-sdk', 'disposeGraceMs', resolved.disposeGraceMs)
|
||||
assertPositiveFinite('subagent-dsh-sdk', 'shutdownTimeoutMs', resolved.shutdownTimeoutMs)
|
||||
assertPositiveFinite('subagent-dsh-sdk', 'disposeEofGraceMs', resolved.disposeEofGraceMs)
|
||||
assertPositiveFinite('subagent-dsh-sdk', 'disposeGraceMs', resolved.disposeGraceMs)
|
||||
// Interpret a relative configured cwd against the harness launch directory
|
||||
// ONCE, at load, and fail a misconfigured directory here — not per start.
|
||||
const configuredCwd = validateConfiguredCwd('subagent-sdk', resolved.cwd)
|
||||
const configuredCwd = validateConfiguredCwd('subagent-dsh-sdk', resolved.cwd)
|
||||
const validated: ResolvedConfig = configuredCwd === undefined
|
||||
? resolved
|
||||
: { ...resolved, cwd: configuredCwd }
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-sdk`.
|
||||
* @module @deepseek-ai/dsh-subagent-sdk/invariant
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-dsh-sdk`.
|
||||
* @module @deepseek-ai/dsh-subagent-dsh-sdk/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-sdk'
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'subagent-sdk-invariant'
|
||||
export const name = 'subagent-dsh-sdk-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
* the subprocess seam's documented exception for SDK-managed transports —
|
||||
* so this driver applies the seam's shared env scrub itself.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent-sdk/run
|
||||
* @module @deepseek-ai/dsh-subagent-dsh-sdk/run
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
+3
-3
@@ -17,7 +17,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { resolveExampleLaunch, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const fixtureDir = new URL('../../../../examples/jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/', import.meta.url)
|
||||
const fixtureDir = new URL('../../../../examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/', import.meta.url)
|
||||
const driver = fileURLToPath(new URL('driver.ts', fixtureDir))
|
||||
const configPath = fileURLToPath(new URL('cordis.yml', fixtureDir))
|
||||
const childConfigPath = fileURLToPath(new URL('child.cordis.yml', fixtureDir))
|
||||
@@ -54,8 +54,8 @@ describe('SDK subagent cwd inheritance through a real cordis.yml', () => {
|
||||
let childEvents: SessionEvent[] = []
|
||||
let workspace = ''
|
||||
const { stderr } = await runLoaderSmoke({
|
||||
label: 'sdk-subagent cwd composition smoke',
|
||||
tempDirPrefix: 'sdk-subagent-cwd-e2e-',
|
||||
label: 'dsh-sdk-subagent cwd composition smoke',
|
||||
tempDirPrefix: 'dsh-sdk-subagent-cwd-e2e-',
|
||||
binScript: driver,
|
||||
libBinScript: driver,
|
||||
configPath,
|
||||
+7
-7
@@ -84,7 +84,7 @@ describe('sdkStopReason', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('dsh-subagent-sdk provider', () => {
|
||||
describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
it('runs a child turn end to end with a parent-unique run id', async () => {
|
||||
const ctx = await setup({ FAKE_TEXT: 'hello from sdk child' })
|
||||
const run = await ctx.subagents.start('dsh-sdk', request('do X'))
|
||||
@@ -105,7 +105,7 @@ describe('dsh-subagent-sdk provider', () => {
|
||||
})
|
||||
|
||||
it('initializes the child with the configured provider/model and the parent cwd', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-sdk-init-'))
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-dsh-sdk-init-'))
|
||||
const recordFile = join(tmp, 'init.jsonl')
|
||||
try {
|
||||
const ctx = await setup({ FAKE_RECORD_INIT: recordFile })
|
||||
@@ -185,7 +185,7 @@ describe('dsh-subagent-sdk provider', () => {
|
||||
// handshake window): the fake touches READY, we abort, then GO lets the
|
||||
// handshake complete — so the post-race `flags.cancelled` recheck must
|
||||
// reject even though the handshake itself succeeded.
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-sdk-midcancel-'))
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-dsh-sdk-midcancel-'))
|
||||
const ready = join(tmp, 'ready')
|
||||
const go = join(tmp, 'go')
|
||||
try {
|
||||
@@ -235,7 +235,7 @@ describe('dsh-subagent-sdk provider', () => {
|
||||
})
|
||||
|
||||
it('rejects WITHOUT spawning when the signal is already aborted', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-sdk-preabort-'))
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-dsh-sdk-preabort-'))
|
||||
const sentinel = join(tmp, 'spawned')
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
@@ -324,7 +324,7 @@ describe('dsh-subagent-sdk provider', () => {
|
||||
const run = await ctx.subagents.start('dsh-sdk', request())
|
||||
expect((await run.result).stopReason).toBe('error')
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0]).toContain('subagent-sdk "dsh-sdk": child run failed (error)')
|
||||
expect(warnings[0]).toContain('subagent-dsh-sdk "dsh-sdk": child run failed (error)')
|
||||
await run.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
@@ -379,7 +379,7 @@ describe('dsh-subagent-sdk provider', () => {
|
||||
})
|
||||
|
||||
it('uses a validated config cwd override instead of the parent session cwd', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-sdk-cwd-'))
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'subagent-dsh-sdk-cwd-'))
|
||||
try {
|
||||
const ctx = await setup({ FAKE_ECHO_CWD: '1', FAKE_TEXT: 'done' }, { cwd: tmp })
|
||||
const run = await ctx.subagents.start('dsh-sdk', request())
|
||||
@@ -402,7 +402,7 @@ describe('dsh-subagent-sdk provider', () => {
|
||||
})
|
||||
|
||||
it('keeps named plugin exports with no default export (loader shape)', () => {
|
||||
expect(sdk.name).toBe('subagent-sdk')
|
||||
expect(sdk.name).toBe('subagent-dsh-sdk')
|
||||
expect(sdk.inject).toEqual(['subagents'])
|
||||
expect(typeof sdk.apply).toBe('function')
|
||||
expect(typeof sdk.Config).toBe('function')
|
||||
@@ -4,7 +4,7 @@
|
||||
* under their never-reject and idempotence contracts.
|
||||
*/
|
||||
|
||||
import { chmodSync, mkdtempSync, rmSync } from 'node:fs'
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, relative, resolve } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
@@ -43,6 +43,17 @@ describe('child cwd resolution', () => {
|
||||
expect(() => assertUsableCwd('p', 'config cwd', join(tmpdir(), 'dsh-no-such-dir-xyz'))).toThrow('not an accessible directory')
|
||||
})
|
||||
|
||||
it('rejects an existing path that is a file, not a directory', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'oop-file-'))
|
||||
const file = join(tmp, 'plain.txt')
|
||||
try {
|
||||
writeFileSync(file, 'not a dir\n')
|
||||
expect(() => assertUsableCwd('p', 'config cwd', file)).toThrow('not an accessible directory')
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
// Windows ACLs do not expose the POSIX directory search-bit state this fixture creates.
|
||||
it.skipIf(process.platform === 'win32')('rejects a directory without search permission', () => {
|
||||
// statSync().isDirectory() is true for a mode-600 directory, but a
|
||||
|
||||
Generated
+43
-43
@@ -508,12 +508,12 @@ importers:
|
||||
'@deepseek-ai/dsh-subagent-acp':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/subagent/subagent-acp
|
||||
'@deepseek-ai/dsh-subagent-dsh-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/subagent/subagent-dsh-sdk
|
||||
'@deepseek-ai/dsh-subagent-fork':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/subagent/subagent-fork
|
||||
'@deepseek-ai/dsh-subagent-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/subagent/subagent-sdk
|
||||
'@deepseek-ai/dsh-subagent-spawn':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/subagent/subagent-spawn
|
||||
@@ -3848,6 +3848,46 @@ importers:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||
|
||||
packages/subagent/subagent-dsh-sdk:
|
||||
dependencies:
|
||||
schemastery:
|
||||
specifier: ^3.18.0
|
||||
version: 3.18.0
|
||||
devDependencies:
|
||||
'@cordisjs/plugin-loader':
|
||||
specifier: ^1.0.0-rc.5
|
||||
version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0)
|
||||
'@deepseek-ai/dsh-agent':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/agent
|
||||
'@deepseek-ai/dsh-invariants':
|
||||
specifier: workspace:^
|
||||
version: link:../../support/invariants
|
||||
'@deepseek-ai/dsh-llm':
|
||||
specifier: workspace:^
|
||||
version: link:../../llm/llm
|
||||
'@deepseek-ai/dsh-loader-smoke':
|
||||
specifier: workspace:^
|
||||
version: link:../../support/loader-smoke
|
||||
'@deepseek-ai/dsh-sdk-client':
|
||||
specifier: workspace:^
|
||||
version: link:../../sdk/sdk-client
|
||||
'@deepseek-ai/dsh-sdk-protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../../sdk/sdk-protocol
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:^
|
||||
version: link:../subagent
|
||||
'@deepseek-ai/dsh-subprocess':
|
||||
specifier: workspace:^
|
||||
version: link:../../subprocess/subprocess
|
||||
cordis:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||
|
||||
packages/subagent/subagent-fork:
|
||||
dependencies:
|
||||
schemastery:
|
||||
@@ -3921,46 +3961,6 @@ importers:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||
|
||||
packages/subagent/subagent-sdk:
|
||||
dependencies:
|
||||
schemastery:
|
||||
specifier: ^3.18.0
|
||||
version: 3.18.0
|
||||
devDependencies:
|
||||
'@cordisjs/plugin-loader':
|
||||
specifier: ^1.0.0-rc.5
|
||||
version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0)
|
||||
'@deepseek-ai/dsh-agent':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/agent
|
||||
'@deepseek-ai/dsh-invariants':
|
||||
specifier: workspace:^
|
||||
version: link:../../support/invariants
|
||||
'@deepseek-ai/dsh-llm':
|
||||
specifier: workspace:^
|
||||
version: link:../../llm/llm
|
||||
'@deepseek-ai/dsh-loader-smoke':
|
||||
specifier: workspace:^
|
||||
version: link:../../support/loader-smoke
|
||||
'@deepseek-ai/dsh-sdk-client':
|
||||
specifier: workspace:^
|
||||
version: link:../../sdk/sdk-client
|
||||
'@deepseek-ai/dsh-sdk-protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../../sdk/sdk-protocol
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:^
|
||||
version: link:../subagent
|
||||
'@deepseek-ai/dsh-subprocess':
|
||||
specifier: workspace:^
|
||||
version: link:../../subprocess/subprocess
|
||||
cordis:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||
|
||||
packages/subagent/subagent-spawn:
|
||||
dependencies:
|
||||
schemastery:
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@
|
||||
{ "path": "./packages/subagent/subagent-spawn" },
|
||||
{ "path": "./packages/subagent/subagent-fork" },
|
||||
{ "path": "./packages/subagent/subagent-acp" },
|
||||
{ "path": "./packages/subagent/subagent-sdk" },
|
||||
{ "path": "./packages/subagent/subagent-dsh-sdk" },
|
||||
{ "path": "./packages/tasks/tasks" },
|
||||
{ "path": "./packages/tasks/tasks-local" },
|
||||
{ "path": "./packages/tasks/tool-tasks" },
|
||||
|
||||
Reference in New Issue
Block a user