@deepseek-ai/dsh-sdk-client 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/sdk/client/README.md
5
- README.md: b33457875f81d11d09bab2e5aa5ce730e233c78a
6
- README.zh.md: b89e56629fefe572394751bc1bee38aaba6f3300
5
+ README.md: 176bddad402a21fd5cdc92aa0316a0716053dbfb
6
+ README.zh.md: c650dd7fa9498c30c32bc66141ab023354dc70c7
package/README.md CHANGED
@@ -1,49 +1,135 @@
1
+ ---
2
+ description: "The TypeScript SDK client for callers that spawn a DeepSeek Harness runtime subprocess and drive agent turns over stdio JSON-RPC: the DeepSeekHarness run API and the lower-level HarnessClient."
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-sdk-client
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- 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 owned-run API, `HarnessClient` the lower-level protocol client. The package root enumerates the consumer interface: the two client layers, caller-facing types, and `JsonRpcResponseError`; source modules, normalization helpers, and subscription-delivery machinery are not consumer imports. 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.
10
+ ## Summary
11
+
12
+ `dsh-sdk-client` lets TypeScript programs drive a DeepSeek Harness runtime as a subprocess over stdio JSON-RPC. With `DeepSeekHarness` you can spawn the runtime, open sessions, send prompts, and collect the final response plus the event and notification streams; `HarnessClient` gives explicit control over the protocol layer. It is the design twin of the [Python SDK](../../../python/README.md), which shares the same runtime peer and protocol. The launch spec is explicit — callers may name the runtime executable via `dshBin`, omitted resolves the same-version `@deepseek-ai/dsh` package's bin, and the client constructs the arguments — so this client suits repository-adjacent TypeScript consumers such as the SDK subagent backend and automation that know which runtime they are launching. It is a pure library: it registers nothing on a Cordis context, and the runtime it spawns is a complete harness whose composition its own `cordis.yml` decides.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
6
22
 
7
- Unlike the Python SDK, the launch spec is fully explicit (`command`/`args`): this package is for repo-adjacent TypeScript consumers — including the [`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.md) backend and automation — that know which runtime they are launching. Bundled-runtime resolution (finding a packaged executable) remains the Python distribution's concern.
23
+ -----
8
24
 
9
- ## DeepSeekHarness
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Use this client when TypeScript code must drive a complete Harness runtime from another process and you can name the runtime executable explicitly. The common path is minimal: construct a `DeepSeekHarness` with a launch spec, run prompts, and close it so the child process is always reaped.
29
+
30
+ ### Running agent turns with DeepSeekHarness
10
31
 
11
32
  ```ts
12
33
  import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
34
+ import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
13
35
 
14
36
  await using harness = new DeepSeekHarness({
15
- launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
37
+ profile: 'sdk',
38
+ patches: ['./automation.cordis.yml'],
16
39
  provider: 'deepseek-official',
17
40
  model: 'deepseek-v4-flash',
41
+ reasoningEffort: ReasoningEffortId('max'),
18
42
  maxTokens: 49_152,
19
43
  })
20
44
  const result = await harness.run('say hi')
21
45
  console.log(result.finalResponse)
22
46
  ```
23
47
 
24
- The subprocess starts lazily on first use and stays owned by the instance across `run()` calls; `close()` (or `await using`) is required so the child is always reaped. `start()` memoizes the `initialize` handshake (the workspace cwd resolved absolute before it crosses the wire plus the provider/model route and optional positive `maxTokens` output cap); a failed handshake reaps the runtime and swaps in a fresh client, so a later call retries with a new subprocess (until `close()`, which is terminal). The cap applies to each root-agent request and is inherited by in-process descendants; compaction plugins own their separate summary limits. `session(id?)` opens a named or fresh session handle.
48
+ The subprocess starts lazily on first use and stays owned by the instance across `run()` calls; call `close()` (or use `await using`) so the child is always reaped. `start()` memoizes the bounded `initialize` handshake, which carries the workspace cwd, provider/model route, optional adapter-owned `reasoningEffort`, and optional positive `maxTokens` output cap. The server validates that exact route before it accepts prompts; an omitted effort preserves the model's default. `initializeTimeoutMs` defaults to 10 seconds, and its diagnostic names the selected profile with the retained stderr tail. `run(input, { sessionId?, onNotification? })` accepts text or `SdkPromptContentBlock[]`; an inline raster block carries canonical base64 plus `mimeType` and becomes a durable attachment inside the runtime. The call owns one activity interval: it queues the prompt, waits until its message id appears in a durable inbox receipt, then collects through the next whole-agent `idle`. It returns `RunResult { sessionId, finalResponse, events, notifications }`, where `finalResponse` is the last committed root-session assistant text in that interval — not a response causally assigned to the prompt, because steering, injected context, and other queued work may contribute before idle. `session(id?)` opens a named or fresh session handle. When a failed handshake is cleaned up successfully, the instance installs a fresh client so a later call retries with a new process until terminal `close()`; if initialization and cleanup both fail, `start()` returns an ordered `AggregateError` and retains the failed client instead of spawning beside a process whose exit is unproved. `maxTokens` caps each root-agent request output and is inherited by in-process descendants; compaction plugins own their separate summary limits.
49
+
50
+ ### Lower-level control with HarnessClient
51
+
52
+ `HarnessClient` is the protocol client under the run API: explicit `start()`, `initialize()`, `prompt()`, `request()`, and `close()`, plus notification subscriptions. `prompt()` returns the queued message id as soon as the runtime accepts it and never waits for agent activity. `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.
53
+
54
+ The client exports typed errors for every failure mode: `JsonRpcResponseError` (a wire error response, code and data preserved), `RequestTimeoutError` (a configured bound elapsed), `SdkProtocolError` (a response outside the documented protocol), and `TransportClosedError` (the runtime is gone — the message carries the exit code and a bounded stderr tail). `close()` requests protocol `shutdown` (bounded by `shutdownTimeoutMs`, default 1000 ms), then walks a stdin-EOF → SIGTERM → SIGKILL ladder until the process has exited; 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 — `scrubbedParentEnv` from `dsh-subprocess` is the shared scrub base for isolation-minded launches.
55
+
56
+ -----
57
+
58
+ <a id="understand-the-implementation"></a>
59
+ ## Understand the implementation
60
+
61
+ <details>
62
+ <summary>Implementation internals — click to expand</summary>
63
+
64
+ This section explains the design behind the client; the observable behavior is fully covered in [Use this package](#use-this-package).
65
+
66
+ ### Design concept
67
+
68
+ The client is two layers over one wire: `DeepSeekHarness` (owned runs) over `HarnessClient` (the protocol client), mirroring the Python SDK's layering. It runs outside any harness context, so it spawns the runtime directly rather than through the `dsh-subprocess` service — the seam's documented exception for SDK-managed transports — and its teardown ladder lives in this package. The runtime notifies for every session in its context; session-tree scoping is a client-side filter over `subagent.started` lineage edges.
25
69
 
26
- `run(input, { sessionId?, onNotification? })` owns one activity interval: it queues the prompt, waits until its `MessageId` appears in a durable `agent/inbox/spliced` receipt, then collects through the next whole-agent `idle`. It returns `RunResult { sessionId, finalResponse, events, notifications }`. `finalResponse` is the last committed root-session assistant text in that interval, not a response causally assigned to the prompt; steering, injected context, and other queued work may contribute before idle. `events` contains root-session events, while `notifications` also contains descendants discovered from `subagent.started`, all in wire order. The result carries no prompt-level status or turn reason. Transport loss, timeout, and protocol violations reject; model outcomes remain observable in the event stream without being attributed to one input.
70
+ ### Source map
27
71
 
28
- ## HarnessClient
72
+ | File | Role |
73
+ |---|---|
74
+ | [`src/api.ts`](src/api.ts) | `DeepSeekHarness` + `HarnessSession`: owned runs, receipt-to-idle collection, `finalResponse` |
75
+ | [`src/client.ts`](src/client.ts) | `HarnessClient`: spawn, handshake, requests, subscription fan-out, typed errors |
76
+ | [`src/dispose.ts`](src/dispose.ts) | Private teardown ladder: stdin EOF → SIGTERM → SIGKILL to actual exit |
77
+ | [`src/types.ts`](src/types.ts) | Launch and timeout options, notification shapes, `RunResult` |
78
+ | [`src/index.ts`](src/index.ts) | Consumer interface: the two client layers and caller-facing types |
79
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant — the peer is a separate runtime process) |
29
80
 
30
- The protocol client under the owned-run API: explicit `start()`/`initialize()`/`prompt()`/`request()`/`close()`, plus notification subscriptions. `prompt()` returns the queued message id as soon as the runtime accepts it; it never waits for agent activity. `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 and exported from this package: `JsonRpcResponseError` (wire error response, code/data preserved), `RequestTimeoutError` (a configured bound elapsed), `SdkProtocolError` (a response outside the documented protocol), `TransportClosedError` (the runtime is gone — message carries the exit code and a bounded stderr tail).
81
+ ### Owned activity flow
31
82
 
32
- `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.
83
+ A run subscribes to the session tree, queues the prompt, waits until the prompt's message id appears in a durable `agent/inbox/spliced` receipt, then collects notifications until the whole agent reports `idle`. `finalResponse` is derived from the last `assistant/message` in the collected events. Transport loss, timeout, and protocol violations reject the run; model outcomes remain observable in the event stream without being attributed to one input.
33
84
 
34
- `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.
85
+ ### Errors and teardown
35
86
 
87
+ Every failure mode maps to one exported error class — a wire error response, an elapsed request bound, a response outside the documented protocol, or a dead runtime — so callers branch on failure type; the four classes are exported from [src/index.ts](src/index.ts). Teardown is a private, idempotent escalation (stdin EOF → SIGTERM → SIGKILL) in [src/dispose.ts](src/dispose.ts) that ends only at actual process exit.
88
+
89
+ </details>
90
+
91
+ -----
92
+
93
+ <a id="further-exploration"></a>
94
+ ## Further Exploration
95
+
96
+ Read these pages when the client contract is not enough. They move from the wire protocol to the serving plugin and the applications that use this client.
97
+
98
+ - [SDK wire protocol](../protocol/README.md) — the JSON-RPC methods and payload shapes this client speaks.
99
+ - [JSON-RPC serving plugin](../server/README.md) — the runtime plugin that serves this client.
100
+ - [Python SDK](../../../python/README.md) — the design twin that shares the same runtime peer and protocol.
101
+ - [SDK subagent backend](../../subagent/subagent-dsh-sdk/README.md) — a harness-internal consumer of this client.
102
+ - [SDK application bundle](../../bundle/sdk-app/README.md) — the `dsh --profile sdk` runtime application this client launches.
103
+
104
+ -----
105
+
106
+ <a id="model-experience"></a>
36
107
  ## Model Experience
37
108
 
38
- None, as this is a client-process library; the model runs in the spawned runtime, whose experience is owned by the plugins its `cordis.yml` composes.
109
+ None, as this is a client-process library; model-facing behavior lives in the spawned runtime's composed plugins.
39
110
 
40
111
  #### KV Cache effect
41
112
 
42
- None; this package neither assembles nor sends a provider request.
113
+ None in the client process. Profile, patch, provider, model, and history choices determine cache reuse in the child.
43
114
 
44
115
  ## Known Limitations and Deferred Work
45
116
 
46
- - **No bundled-runtime resolution** — callers name the runtime executable explicitly; packaged-executable discovery stays Python-side until a TypeScript distribution consumer exists.
47
- - **No mid-turn cancel** — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the protocol's [Known Limitations](../protocol/README.md)).
48
- - **No per-prompt result or cancel** — low-level `prompt()` returns only an enqueue receipt; high-level `run()` owns receipt-to-idle collection, and abandoning it means closing the runtime.
117
+ <a id="known-limitations-and-deferred-work"></a>
118
+
119
+
120
+ These limits define when the client is a poor fit or needs special care. They are current package constraints, not a comparison with other SDK clients or a task backlog.
121
+
122
+ - **No bundled-runtime resolution** — the client resolves the same-version `@deepseek-ai/dsh` package (or a caller-provided `dshBin`); packaged-executable discovery stays Python-side until a TypeScript distribution consumer exists.
123
+ - **No mid-turn cancel** — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the [protocol limitations](../protocol/README.md#known-limitations-and-deferred-work)).
124
+ - **No per-prompt result** — low-level `prompt()` returns only an enqueue receipt; high-level `run()` owns receipt-to-idle collection, and abandoning it means closing the runtime.
49
125
  - **Client→server notifications and server→client requests are unimplemented** on both wire ends; the transport carries them for future approval flows.
126
+
127
+ <a id="dev-note"></a>
128
+ ### Dev Note
129
+
130
+ <details>
131
+ <summary>Working context for maintainers — click to expand</summary>
132
+
133
+ This Dev Note is working context for maintainers and is explicitly non-authoritative — shipped behavior and limits live in the sections above and in the code. The launch spec is intentionally fully explicit: no bundled-runtime resolution is planned for TypeScript until a distribution consumer exists. Keep the dispose ladder and the error vocabulary in sync with the Python client, which drives the same runtime. No other unresolved design questions are recorded.
134
+
135
+ </details>
package/README.zh.md CHANGED
@@ -1,49 +1,135 @@
1
+ ---
2
+ description: "面向以子进程方式启动 DeepSeek Harness 运行时、并通过 stdio JSON-RPC 驱动 agent 轮次的调用方的 TypeScript SDK 客户端:DeepSeekHarness 运行 API 与低层 HarnessClient。"
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-sdk-client
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.zh.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层自有运行 API,`HarnessClient` 是低层协议客户端。包(package)根枚举消费方接口:两层客户端、面向调用方的类型和 `JsonRpcResponseError`;源模块、规范化辅助函数与订阅投递机制不供消费方导入。纯库:不在任何 Cordis 上下文注册;它所 spawn 的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
10
+ ## 概述
11
+
12
+ `dsh-sdk-client` 让 TypeScript 程序以子进程方式、通过 stdio JSON-RPC 驱动 DeepSeek Harness 运行时。使用 `DeepSeekHarness` 你可以启动运行时、打开会话、发送提示词,并收集最终响应以及事件与通知流;`HarnessClient` 提供对协议层的显式控制。它是 [Python SDK](../../../python/README.zh.md) 的设计孪生,共享同一个运行时对端与协议。启动说明是显式的——调用方可通过 `dshBin` 指定运行时可执行文件,省略时解析同版本 `@deepseek-ai/dsh` 包的 bin,参数由客户端构造——因此本客户端适合仓库近旁的 TypeScript 消费方,如 SDK subagent 后端和知道自己要启动哪个运行时的自动化。它是纯库:不在任何 Cordis 上下文注册,而且它启动的运行时是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
6
22
 
7
- 与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费方,包括 [`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.zh.md) 后端和自动化;它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
23
+ -----
8
24
 
9
- ## DeepSeekHarness
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 当 TypeScript 代码需要从另一进程驱动完整 Harness 运行时、且你能显式指名运行时可执行文件时,使用本客户端。常用路径极简:用启动规格构造 `DeepSeekHarness`,运行提示词,然后关闭它,使子进程总能被回收。
29
+
30
+ ### 用 DeepSeekHarness 运行 agent 轮次
10
31
 
11
32
  ```ts
12
33
  import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
34
+ import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
13
35
 
14
36
  await using harness = new DeepSeekHarness({
15
- launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
37
+ profile: 'sdk',
38
+ patches: ['./automation.cordis.yml'],
16
39
  provider: 'deepseek-official',
17
40
  model: 'deepseek-v4-flash',
41
+ reasoningEffort: ReasoningEffortId('max'),
18
42
  maxTokens: 49_152,
19
43
  })
20
44
  const result = await harness.run('say hi')
21
45
  console.log(result.finalResponse)
22
46
  ```
23
47
 
24
- 子进程在首次使用时惰性启动,并在多次 `run()` 之间持续归实例所有;必须 `close()`(或 `await using`),子进程才总能被回收。`start()` 记忆化 `initialize` 握手(工作区 cwd——在通过协议传输之前解析为绝对路径——加 provider/model 路由和可选的正整数 `maxTokens` 输出上限);握手失败会回收运行时并换入全新客户端,后续调用用新子进程重试(直到终结性的 `close()`)。该上限作用于根 agent(智能体)的每次请求,并由进程内后代继承;压缩(compaction)插件单独持有摘要上限。`session(id?)` 打开具名或全新的会话句柄。
48
+ 子进程在首次使用时惰性启动,并在多次 `run()` 调用之间持续归实例所有;请调用 `close()`(或使用 `await using`),子进程才总能被回收。`start()` 会记忆化有界的 `initialize` 握手,其中包含工作区 cwd、提供方/模型路由、可选且由适配器持有的 `reasoningEffort`,以及可选的正整数 `maxTokens` 输出上限。服务器会在接受提示词前校验该确切路由;省略推理强度时保留模型自身的默认值。`initializeTimeoutMs` 默认 10 秒,诊断会写明所选 profile 并附带保留的 stderr 尾部。`run(input, { sessionId?, onNotification? })` 接受文本或 `SdkPromptContentBlock[]`;内联栅格图像块携带规范 base64 与 `mimeType`,并在运行时内变成持久附件。该调用拥有一个活动区间:它将提示词排入队列,等待其消息 id 出现在持久入队回执中,然后持续收集到整个 agent 下一次进入 `idle`。它返回 `RunResult { sessionId, finalResponse, events, notifications }`,其中 `finalResponse` 是该区间内根会话最后提交的助手文本——并非因果上归属于该提示词的响应,因为 steering(中途引导)、注入的上下文和其他排队工作都可能在 idle 前参与其中。`session(id?)` 打开具名或全新的会话句柄。握手失败且清理成功时,实例会换入全新客户端,使后续调用用新进程重试,直到终结性的 `close()`;如果初始化和清理均失败,`start()` 会返回保留两个原因的有序 `AggregateError`,并继续保留失败的客户端,避免在原进程退出尚未得到证明时启动另一个进程。`maxTokens` 限制每个根 agent 请求的输出量,并由进程内后代继承;压缩(compaction)插件单独持有摘要上限。
49
+
50
+ ### 用 HarnessClient 做低层控制
51
+
52
+ `HarnessClient` 是运行 API 之下的协议客户端:显式 `start()`、`initialize()`、`prompt()`、`request()` 与 `close()`,外加通知订阅。`prompt()` 在运行时接受排队消息后立即返回该消息的 id,绝不等待 agent 活动。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。
53
+
54
+ 本客户端为每种失败模式导出类型化错误:`JsonRpcResponseError`(协议错误响应,保留 code 与 data)、`RequestTimeoutError`(配置的时限已到)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯直到进程退出;幂等,已关闭的客户端拒绝复用。`HarnessClientOptions.env` 给定时整体替换子进程环境(`undefined` 原样继承父进程环境);凭据策略归调用方——`dsh-subprocess` 的 `scrubbedParentEnv` 是面向隔离启动的共享擦除基底。
55
+
56
+ -----
57
+
58
+ <a id="understand-the-implementation"></a>
59
+ ## 理解实现
60
+
61
+ <details>
62
+ <summary>实现细节——点击展开</summary>
63
+
64
+ 本节解释客户端背后的设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
65
+
66
+ ### 设计理念
67
+
68
+ 客户端是同一协议上的两层:`DeepSeekHarness`(自有运行)叠加在 `HarnessClient`(协议客户端)之上,与 Python SDK 的分层一致。它运行在任何 harness 上下文之外,因此直接 spawn 运行时而非经由 `dsh-subprocess` 服务——即该 seam 记录的 SDK 托管传输例外——其关闭阶梯也位于本包。运行时对上下文内每个会话都发通知;会话树范围限定是客户端对 `subagent.started` 血缘边的过滤。
25
69
 
26
- `run(input, { sessionId?, onNotification? })` 拥有一个活动区间:它将提示词排入队列,等待其 `MessageId` 出现在持久的 `agent/inbox/spliced` 回执中,然后持续收集到整个 agent 下一次进入 `idle`。它返回 `RunResult { sessionId, finalResponse, events, notifications }`。`finalResponse` 是该区间内根会话最后提交的助手文本,并非因果上归属于该提示词的响应;steering(中途引导)、注入的上下文和其他排队工作都可能在 idle 前参与其中。`events` 包含根会话事件,`notifications` 还包含通过 `subagent.started` 发现的后代,均按协议传输顺序排列。结果不携带提示词级状态或轮次原因。传输丢失、超时和协议违例会导致 Promise 被拒绝;模型结果仍可在事件流中观察,但不会归属于某一输入。
70
+ ### 源码地图
27
71
 
28
- ## HarnessClient
72
+ | 文件 | 职责 |
73
+ |---|---|
74
+ | [`src/api.ts`](src/api.ts) | `DeepSeekHarness` + `HarnessSession`:自有运行、回收到 idle 的收集、`finalResponse` |
75
+ | [`src/client.ts`](src/client.ts) | `HarnessClient`:spawn、握手、请求、订阅扇出、类型化错误 |
76
+ | [`src/dispose.ts`](src/dispose.ts) | 私有关闭阶梯:stdin EOF → SIGTERM → SIGKILL 直到真正退出 |
77
+ | [`src/types.ts`](src/types.ts) | 启动与超时选项、通知结构、`RunResult` |
78
+ | [`src/index.ts`](src/index.ts) | 消费方接口:两层客户端与面向调用方的类型 |
79
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式配套插件(无运行时不变式——对端是独立运行时进程) |
29
80
 
30
- 自有运行 API 之下的协议客户端:显式 `start()`/`initialize()`/`prompt()`/`request()`/`close()`,外加通知订阅。`prompt()` 在运行时接受排队消息后立即返回该消息的 ID,绝不等待 agent 活动。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。本包导出有明确类型的错误:`JsonRpcResponseError`(协议错误响应,保留 code/data)、`RequestTimeoutError`(配置的时限已到)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。
81
+ ### 自有活动流程
31
82
 
32
- `close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。该阶梯为本客户端私有:它运行在任何 harness 上下文之外,无法搭乘 [`dsh-subprocess`](../../subprocess/README.zh.md) 服务——即该 seam 所记录的 SDK 托管传输例外。幂等,已关闭的客户端拒绝复用。
83
+ 一次运行会订阅会话树、把提示词排入队列,等待提示词的消息 id 出现在持久的 `agent/inbox/spliced` 回执中,然后持续收集通知,直到整个 agent 报告 `idle`。`finalResponse` 从收集到的事件中最后一条 `assistant/message` 派生。传输丢失、超时与协议违例会使本次运行被拒绝;模型结果仍可在事件流中观察,但不会归属于某一输入。
33
84
 
34
- `HarnessClientOptions.env` 给定时整体替换子进程环境(`undefined` 原样继承父进程环境);凭据策略归调用方——`dsh-subprocess` 的 `scrubbedParentEnv` 是面向隔离启动的共享擦除基底。
85
+ ### 错误与关闭
35
86
 
87
+ 每种失败模式都映射到一个导出的错误类——协议错误响应、请求时限已到、响应超出文档化协议、运行时死亡——调用方可以按失败类型分支处理;这四个类从 [src/index.ts](src/index.ts) 导出。关闭采用私有的幂等阶梯(stdin EOF → SIGTERM → SIGKILL),位于 [src/dispose.ts](src/dispose.ts),只在进程真正退出时结束。
88
+
89
+ </details>
90
+
91
+ -----
92
+
93
+ <a id="further-exploration"></a>
94
+ ## 进一步探索
95
+
96
+ 当客户端约定不够用时阅读以下页面。它们从协议格式进入服务插件与使用本客户端的应用。
97
+
98
+ - [SDK 协议格式](../protocol/README.zh.md) — 本客户端所说的 JSON-RPC 方法与载荷结构。
99
+ - [JSON-RPC 服务插件](../server/README.zh.md) — 服务本客户端的运行时插件。
100
+ - [Python SDK](../../../python/README.zh.md) — 共享同一运行时对端与协议的设计孪生。
101
+ - [SDK subagent 后端](../../subagent/subagent-dsh-sdk/README.zh.md) — harness 内部消费本客户端的例子。
102
+ - [SDK 应用组合包](../../bundle/sdk-app/README.zh.md) — 本客户端启动的 `dsh --profile sdk` 运行时应用。
103
+
104
+ -----
105
+
106
+ <a id="model-experience"></a>
36
107
  ## 模型体验
37
108
 
38
- 无,因为这是一个客户端进程库;模型运行在 spawn 出的运行时中,其体验由该运行时的 `cordis.yml` 所组合的插件决定。
109
+ 无,因为这是客户端进程库;模型可见行为存在于所 spawn 运行时组合的插件中。
39
110
 
40
111
  #### KV Cache 影响
41
112
 
42
- 无;本包既不组装也不发送提供方请求。
113
+ client 进程中无影响。子进程的 profile、patch、provider、model 与历史决定缓存复用。
114
+
115
+ ## 已知限制与延期工作
116
+
117
+ <a id="known-limitations-and-deferred-work"></a>
118
+
119
+
120
+ 这些限制说明本客户端何时不合适或需要特别注意。它们是当前包约束,不是与其他 SDK 客户端的对比或任务积压。
121
+
122
+ - **无捆绑运行时解析**——客户端解析同版本 `@deepseek-ai/dsh` 包(或调用方提供的 `dshBin`);打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费方。
123
+ - **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见[协议限制](../protocol/README.zh.md#known-limitations-and-deferred-work))。
124
+ - **没有逐提示词结果**——低层 `prompt()` 只返回入队回执;高层 `run()` 负责从回收到 idle 的收集,放弃该过程意味着关闭运行时。
125
+ - **客户端→服务端通知与服务端→客户端请求**在协议两端都未实现;传输层为未来审批流程保留了承载能力。
126
+
127
+ <a id="dev-note"></a>
128
+ ### 开发备注
129
+
130
+ <details>
131
+ <summary>维护者的工作上下文——点击展开</summary>
43
132
 
44
- ## 已知限制与暂缓事项
133
+ 本开发备注是维护者的工作上下文,明确不具权威性——已交付的行为与限制见上文各节与代码。启动规格有意保持完全显式:在出现 TypeScript 发行版消费方之前,不计划做捆绑运行时解析。请让关闭阶梯与错误词汇与驱动同一运行时的 Python 客户端保持同步。没有记录其他未解决的开放设计问题。
45
134
 
46
- - **无捆绑运行时解析**——调用方显式指定运行时可执行文件;打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费方。
47
- - **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见协议的 [已知限制](../protocol/README.zh.md))。
48
- - **没有逐提示词结果或取消**——低层 `prompt()` 只返回入队回执;高层 `run()` 负责从回执收集到 idle,放弃该过程意味着关闭运行时。
49
- - **客户端→服务端通知与服务端→客户端请求**在协议两端都未实现;传输层为未来审批流保留了承载能力。
135
+ </details>
package/lib/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { resolve } from "node:path";
2
+ import { dirname, resolve } from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  import { JsonRpcLineTransport, JsonRpcResponseError, JsonRpcResponseError as JsonRpcResponseError$1 } from "@deepseek-ai/dsh-sdk-protocol";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
5
7
  //#region lib/types/dispose.js
6
8
  /**
7
9
  * Private teardown ladder for the runtime subprocess: stdin EOF (cooperative
@@ -96,6 +98,97 @@ async function disposeRuntimeProcess(child, graces, platform = process.platform)
96
98
  }
97
99
  await forceTerminateWithin(child, graces.disposeGraceMs);
98
100
  }
101
+ /** Read a package manifest from one resolved package.json URL. */
102
+ function manifest(url) {
103
+ return JSON.parse(readFileSync(fileURLToPath(url), "utf8"));
104
+ }
105
+ /**
106
+ * Resolve and version-check a dsh executable from package manifests.
107
+ * @param dshManifestUrl - resolved URL of the dsh package manifest.
108
+ * @param clientManifestUrl - resolved URL of the SDK client manifest.
109
+ * @returns the absolute dsh executable path.
110
+ */
111
+ function resolveDshBinFromManifests(dshManifestUrl, clientManifestUrl) {
112
+ const dshManifest = manifest(dshManifestUrl);
113
+ const clientManifest = manifest(clientManifestUrl);
114
+ if (typeof dshManifest.version !== "string" || dshManifest.version !== clientManifest.version) throw new Error(`dsh SDK client ${String(clientManifest.version)} requires the same dsh version, got ${String(dshManifest.version)}`);
115
+ const bin = typeof dshManifest.bin === "object" && dshManifest.bin !== null ? dshManifest.bin.dsh : dshManifest.bin;
116
+ if (typeof bin !== "string" || bin === "") throw new Error("@deepseek-ai/dsh declares no dsh executable");
117
+ return resolve(dirname(fileURLToPath(dshManifestUrl)), bin);
118
+ }
119
+ /**
120
+ * Resolve the Node launch for one same-version dsh package.
121
+ * @param dshManifestUrl - resolved URL of the dsh package manifest.
122
+ * @param clientManifestUrl - resolved URL of the SDK client manifest.
123
+ * @param sourceLoaderUrl - optional absolute tsx loader URL for deterministic tests.
124
+ * @returns built output, or the source entry plus its compatibility patch and tsx environment.
125
+ */
126
+ function resolveDshNodeLaunchFromManifests(dshManifestUrl, clientManifestUrl, sourceLoaderUrl) {
127
+ const bin = resolveDshBinFromManifests(dshManifestUrl, clientManifestUrl);
128
+ if (existsSync(bin)) return {
129
+ nodeArgs: [bin],
130
+ patches: [],
131
+ environment: {}
132
+ };
133
+ const packageDir = dirname(fileURLToPath(dshManifestUrl));
134
+ const sourceBin = resolve(packageDir, "src/bin.ts");
135
+ const sourcePatch = resolve(packageDir, "src/sdk-source.cordis.patch.yml");
136
+ const sourceTsconfig = resolve(packageDir, "tsconfig.json");
137
+ if (!existsSync(sourceBin) || !existsSync(sourcePatch) || !existsSync(sourceTsconfig)) throw new Error(`@deepseek-ai/dsh is missing its built executable ${bin} and complete source launch files ${sourceBin}, ${sourcePatch}, ${sourceTsconfig}`);
138
+ return {
139
+ nodeArgs: [
140
+ "--import",
141
+ sourceLoaderUrl ?? import.meta.resolve("tsx/esm"),
142
+ sourceBin
143
+ ],
144
+ patches: [sourcePatch],
145
+ environment: { TSX_TSCONFIG_PATH: sourceTsconfig }
146
+ };
147
+ }
148
+ /**
149
+ * Resolve the installed dsh package to a built or source Node launch.
150
+ * @returns the launch descriptor for the current checkout or installed package.
151
+ */
152
+ function installedDshNodeLaunch() {
153
+ return resolveDshNodeLaunchFromManifests(import.meta.resolve("@deepseek-ai/dsh/package.json"), new URL("../package.json", import.meta.url).href);
154
+ }
155
+ /**
156
+ * Resolve caller-relative filesystem inputs and construct canonical dsh argv.
157
+ * @param options - public SDK launch options.
158
+ * @param callerCwd - parent-process directory used for lexical resolution.
159
+ * @returns one generic subprocess spec for the JSON-RPC transport.
160
+ */
161
+ function resolveDshLaunch(options = {}, callerCwd = process.cwd()) {
162
+ const profile = options.profile ?? "sdk";
163
+ const dshLaunch = options.dshBin === void 0 ? installedDshNodeLaunch() : {
164
+ nodeArgs: [resolve(callerCwd, options.dshBin)],
165
+ patches: [],
166
+ environment: {}
167
+ };
168
+ const patches = [...dshLaunch.patches, ...(options.patches ?? []).map((path) => resolve(callerCwd, path))];
169
+ const dshHome = options.dshHome === void 0 ? void 0 : resolve(callerCwd, options.dshHome);
170
+ return {
171
+ command: process.execPath,
172
+ args: [
173
+ ...dshLaunch.nodeArgs,
174
+ "--profile",
175
+ profile,
176
+ ...patches.flatMap((path) => ["--patch", path])
177
+ ],
178
+ ...options.processCwd === void 0 ? {} : { cwd: resolve(callerCwd, options.processCwd) },
179
+ environment: () => ({
180
+ ...options.env ?? process.env,
181
+ ...dshLaunch.environment,
182
+ ...dshHome === void 0 ? {} : { DSH_HOME: dshHome }
183
+ }),
184
+ description: `dsh profile ${JSON.stringify(profile)}`,
185
+ initializeTimeoutMs: options.initializeTimeoutMs ?? 1e4,
186
+ ...options.requestTimeoutMs === void 0 ? {} : { requestTimeoutMs: options.requestTimeoutMs },
187
+ ...options.shutdownTimeoutMs === void 0 ? {} : { shutdownTimeoutMs: options.shutdownTimeoutMs },
188
+ ...options.disposeEofGraceMs === void 0 ? {} : { disposeEofGraceMs: options.disposeEofGraceMs },
189
+ ...options.disposeGraceMs === void 0 ? {} : { disposeGraceMs: options.disposeGraceMs }
190
+ };
191
+ }
99
192
  //#endregion
100
193
  //#region lib/types/client.js
101
194
  /**
@@ -197,7 +290,7 @@ var NotificationSubscriptionImpl = class {
197
290
  * Deliver one notification to a waiter or the queue when the filter
198
291
  * matches. A throwing filter fails only THIS subscription (detached, the
199
292
  * throw becomes its terminal error) — it never disturbs sibling
200
- * subscriptions or the transport's read loop, mirroring the Python client.
293
+ * subscriptions or the transport's read loop.
201
294
  * @param notification - the wire notification to deliver.
202
295
  */
203
296
  push(notification) {
@@ -233,7 +326,9 @@ var NotificationSubscriptionImpl = class {
233
326
  * runtime is closed.
234
327
  */
235
328
  var HarnessClient = class {
329
+ /** Original public dsh launch and timeout options for this client. */
236
330
  options;
331
+ runtime;
237
332
  child;
238
333
  transport;
239
334
  stderrTail = [];
@@ -244,9 +339,9 @@ var HarnessClient = class {
244
339
  spawnError;
245
340
  streamsSettled = Promise.resolve();
246
341
  closeTask;
247
- /** @param options - launch spec, complete child environment, and timeouts. */
248
- constructor(options) {
342
+ constructor(options = {}, runtime) {
249
343
  this.options = options;
344
+ this.runtime = runtime ?? resolveDshLaunch(options);
250
345
  }
251
346
  /**
252
347
  * Spawn the runtime subprocess and start reading frames. Idempotent while
@@ -255,9 +350,9 @@ var HarnessClient = class {
255
350
  start() {
256
351
  if (this.closeTask !== void 0) throw new TransportClosedError("DeepSeek Harness runtime client is closed");
257
352
  if (this.child !== void 0) return;
258
- const child = spawn(this.options.command, this.options.args ?? [], {
259
- cwd: this.options.cwd,
260
- env: this.options.env ?? process.env,
353
+ const child = spawn(this.runtime.command, this.runtime.args, {
354
+ cwd: this.runtime.cwd,
355
+ env: this.runtime.environment(),
261
356
  stdio: [
262
357
  "pipe",
263
358
  "pipe",
@@ -323,7 +418,7 @@ var HarnessClient = class {
323
418
  * @returns the runtime's wire identity.
324
419
  */
325
420
  async initialize(params) {
326
- const result = await this.request("initialize", { ...params });
421
+ const result = await this.request("initialize", { ...params }, this.runtime.initializeTimeoutMs);
327
422
  if (!isRecord(result) || !isRecord(result.serverInfo) || typeof result.serverInfo.name !== "string" || typeof result.serverInfo.version !== "string") throw new SdkProtocolError(`initialize returned no server identity: ${JSON.stringify(result)}`);
328
423
  return { serverInfo: {
329
424
  name: result.serverInfo.name,
@@ -363,12 +458,13 @@ var HarnessClient = class {
363
458
  const transport = this.transport;
364
459
  /* v8 ignore next -- start() either sets the transport or throws */
365
460
  if (transport === void 0) throw new TransportClosedError("DeepSeek Harness runtime is not running");
366
- const timeout = timeoutMs ?? this.options.requestTimeoutMs;
461
+ const timeout = timeoutMs ?? this.runtime.requestTimeoutMs;
367
462
  try {
368
463
  if (timeout === void 0) return await transport.request(method, params ?? {});
369
464
  const abandon = new AbortController();
370
465
  const timer = setTimeout(() => {
371
- abandon.abort(new RequestTimeoutError(`${method} timed out after ${timeout}ms waiting for the DeepSeek Harness runtime`));
466
+ const stderr = this.stderrTail.length === 0 ? "" : `; stderr tail:\n${this.stderrTail.join("\n")}`;
467
+ abandon.abort(new RequestTimeoutError(`${method} timed out after ${timeout}ms waiting for ${this.runtime.description}${stderr}`));
372
468
  }, timeout);
373
469
  try {
374
470
  return await transport.request(method, params ?? {}, abandon.signal);
@@ -407,8 +503,8 @@ var HarnessClient = class {
407
503
  }
408
504
  /**
409
505
  * Subscribe to one session and the descendants discovered from
410
- * `subagent.started` lineage edges (the runtime notifies for every session
411
- * in its context; scoping is client-side, mirroring the Python SDK).
506
+ * `subagent.started` lineage edges. The runtime notifies for every session
507
+ * in its context, so this client applies the scope.
412
508
  * @param sessionId - the root session id.
413
509
  * @returns the filtered subscription handle.
414
510
  */
@@ -438,13 +534,13 @@ var HarnessClient = class {
438
534
  const child = this.child;
439
535
  if (child === void 0) return;
440
536
  try {
441
- await this.request("shutdown", void 0, this.options.shutdownTimeoutMs ?? 1e3);
537
+ await this.request("shutdown", void 0, this.runtime.shutdownTimeoutMs ?? 1e3);
442
538
  } catch (error) {
443
539
  this.appendStderr([`shutdown request failed: ${errorMessage(error)}`]);
444
540
  }
445
541
  await disposeRuntimeProcess(child, {
446
- disposeEofGraceMs: this.options.disposeEofGraceMs ?? 6e3,
447
- disposeGraceMs: this.options.disposeGraceMs ?? 3e3
542
+ disposeEofGraceMs: this.runtime.disposeEofGraceMs ?? 6e3,
543
+ disposeGraceMs: this.runtime.disposeGraceMs ?? 3e3
448
544
  });
449
545
  this.transport?.close();
450
546
  this.failSubscriptions(this.closedError("DeepSeek Harness runtime closed"));
@@ -486,7 +582,7 @@ var HarnessClient = class {
486
582
  })]);
487
583
  }
488
584
  closedError(reason) {
489
- const parts = [reason];
585
+ const parts = [`${this.runtime.description}: ${reason}`];
490
586
  if (this.spawnError !== void 0) parts.push(`spawn error: ${this.spawnError.message}`);
491
587
  if (this.exitCode !== void 0) parts.push(`exit code: ${String(this.exitCode)}`);
492
588
  if (this.stderrTail.length > 0) parts.push(`stderr tail:\n${this.stderrTail.join("\n")}`);
@@ -512,7 +608,6 @@ function errorMessage(error) {
512
608
  * High-level run API over {@link HarnessClient}: `DeepSeekHarness` owns one
513
609
  * runtime subprocess across many sessions; `HarnessSession.run` sends a
514
610
  * prompt and settles when the whole agent next becomes idle.
515
- * Mirrors the Python SDK's `DeepSeekHarness`/`Session` pair.
516
611
  *
517
612
  * @module @deepseek-ai/dsh-sdk-client/api
518
613
  */
@@ -524,26 +619,28 @@ function errorMessage(error) {
524
619
  */
525
620
  var DeepSeekHarness = class {
526
621
  clientInstance;
527
- launch;
622
+ createClient;
528
623
  cwd;
529
624
  provider;
530
625
  model;
626
+ reasoningEffort;
531
627
  maxTokens;
532
628
  initialized;
533
629
  closed = false;
534
- /** @param options - runtime launch spec plus the session route (cwd/provider/model). */
535
- constructor(options) {
536
- this.launch = options.launch;
537
- this.clientInstance = new HarnessClient(options.launch);
538
- this.cwd = resolve(options.cwd ?? options.launch.cwd ?? process.cwd());
630
+ constructor(options = {}, clientFactory) {
631
+ this.createClient = clientFactory ?? (() => new HarnessClient(options));
632
+ this.clientInstance = this.createClient();
633
+ this.cwd = resolve(options.cwd ?? options.processCwd ?? process.cwd());
539
634
  this.provider = options.provider ?? "deepseek-official";
540
635
  this.model = options.model ?? "deepseek-v4-flash";
636
+ this.reasoningEffort = options.reasoningEffort;
541
637
  this.maxTokens = options.maxTokens;
542
638
  }
543
639
  /**
544
640
  * The underlying JSON-RPC client (exposed for low-level access). A failed
545
- * handshake reaps its runtime and swaps in a fresh instance, so do not
546
- * cache this across a failed {@link start}.
641
+ * handshake swaps in a fresh instance only after cleanup proves the runtime
642
+ * exited; cleanup failure retains this client, so do not cache it across a
643
+ * failed {@link start}.
547
644
  * @returns the client currently owning the runtime subprocess.
548
645
  */
549
646
  get client() {
@@ -551,9 +648,12 @@ var DeepSeekHarness = class {
551
648
  }
552
649
  /**
553
650
  * Start the subprocess and perform the `initialize` handshake once. On
554
- * failure the runtime is reaped and a fresh client replaces it
555
- * (`HarnessClient.close` is permanent), so a later call retries with a new
556
- * subprocess unless {@link close} already ended this harness.
651
+ * failure, successful SDK-owned cleanup reaps the runtime and installs a
652
+ * fresh client (`HarnessClient.close` is permanent), so a later call retries
653
+ * with a new subprocess unless {@link close} already ended this harness. If
654
+ * cleanup also fails, rejects with an `AggregateError` whose ordered errors
655
+ * preserve both causes and retains the failed client rather than spawning
656
+ * alongside a process whose exit was not proved.
557
657
  * @returns settlement of the (memoized) handshake.
558
658
  */
559
659
  start() {
@@ -564,12 +664,17 @@ var DeepSeekHarness = class {
564
664
  cwd: this.cwd,
565
665
  provider: this.provider,
566
666
  model: this.model,
667
+ ...this.reasoningEffort === void 0 ? {} : { reasoningEffort: this.reasoningEffort },
567
668
  ...this.maxTokens === void 0 ? {} : { maxTokens: this.maxTokens }
568
669
  });
569
670
  } catch (error) {
570
671
  this.initialized = void 0;
571
- await this.clientInstance.close();
572
- if (!this.closed) this.clientInstance = new HarnessClient(this.launch);
672
+ try {
673
+ await this.clientInstance.close();
674
+ } catch (cleanupError) {
675
+ throw new AggregateError([error, cleanupError], "DeepSeek Harness initialization and cleanup failed");
676
+ }
677
+ if (!this.closed) this.clientInstance = this.createClient();
573
678
  throw error;
574
679
  }
575
680
  })();
@@ -683,6 +788,24 @@ function normalizeInput(input) {
683
788
  text: input
684
789
  }] : input;
685
790
  }
791
+ /** Validate the provider-read fields of one wire turn-end reason. */
792
+ function validatedTurnEndReason(value) {
793
+ if (!isRecord(value) || typeof value.kind !== "string") throw new SdkProtocolError(`turn/end carried no reason envelope: ${JSON.stringify(value)}`);
794
+ if (value.kind === "aborted") {
795
+ if (!isRecord(value.reason) || typeof value.reason.kind !== "string") throw new SdkProtocolError(`turn/end carried a malformed aborted reason: ${JSON.stringify(value)}`);
796
+ switch (value.reason.kind) {
797
+ case "user":
798
+ case "parent":
799
+ case "disposed":
800
+ case "legacy": break;
801
+ case "hook":
802
+ if (typeof value.reason.reason !== "string") throw new SdkProtocolError(`turn/end carried a malformed hook abort reason: ${JSON.stringify(value)}`);
803
+ break;
804
+ default: throw new SdkProtocolError(`turn/end carried an unknown abort reason: ${JSON.stringify(value)}`);
805
+ }
806
+ }
807
+ return value;
808
+ }
686
809
  /** Validate the fields in a wire `session.event` envelope before returning the typed result. */
687
810
  function validatedSessionEvent(value) {
688
811
  if (!isRecord(value) || typeof value.type !== "string") throw new SdkProtocolError(`session.event carried no event envelope: ${JSON.stringify(value)}`);
@@ -691,6 +814,11 @@ function validatedSessionEvent(value) {
691
814
  const content = isRecord(message) ? message.content : void 0;
692
815
  if (!Array.isArray(content) || !content.every((block) => isRecord(block) && typeof block.type === "string")) throw new SdkProtocolError(`assistant/message event carried malformed content: ${JSON.stringify(value)}`);
693
816
  }
817
+ if (value.type === "turn/end") {
818
+ const data = isRecord(value.data) ? value.data : void 0;
819
+ if (data === void 0) throw new SdkProtocolError(`turn/end event carried malformed data: ${JSON.stringify(value)}`);
820
+ validatedTurnEndReason(data.reason);
821
+ }
694
822
  return value;
695
823
  }
696
824
  /** Whether a raw session event is the durable enqueue receipt for `messageId`. */
@@ -2,13 +2,13 @@
2
2
  * High-level run API over {@link HarnessClient}: `DeepSeekHarness` owns one
3
3
  * runtime subprocess across many sessions; `HarnessSession.run` sends a
4
4
  * prompt and settles when the whole agent next becomes idle.
5
- * Mirrors the Python SDK's `DeepSeekHarness`/`Session` pair.
6
5
  *
7
6
  * @module @deepseek-ai/dsh-sdk-client/api
8
7
  */
9
8
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
10
9
  import { HarnessClient } from './client.ts';
11
- import type { ContentBlock, DeepSeekHarnessOptions, HarnessNotification, RunResult } from './types.ts';
10
+ import type { RuntimeProcessOptions } from './launch.ts';
11
+ import type { DeepSeekHarnessOptions, HarnessNotification, RunResult, SdkPromptContentBlock } from './types.ts';
12
12
  /**
13
13
  * Reusable SDK for running DeepSeek Harness agent turns in a runtime
14
14
  * subprocess. The subprocess starts lazily on first use and stays owned by
@@ -17,27 +17,32 @@ import type { ContentBlock, DeepSeekHarnessOptions, HarnessNotification, RunResu
17
17
  */
18
18
  export declare class DeepSeekHarness implements AsyncDisposable {
19
19
  private clientInstance;
20
- private readonly launch;
20
+ private readonly createClient;
21
21
  private readonly cwd;
22
22
  private readonly provider;
23
23
  private readonly model;
24
+ private readonly reasoningEffort;
24
25
  private readonly maxTokens;
25
26
  private initialized;
26
27
  private closed;
27
- /** @param options - runtime launch spec plus the session route (cwd/provider/model). */
28
- constructor(options: DeepSeekHarnessOptions);
28
+ /** @param options - dsh launch configuration plus the session route, effort, and output cap. */
29
+ constructor(options?: DeepSeekHarnessOptions);
29
30
  /**
30
31
  * The underlying JSON-RPC client (exposed for low-level access). A failed
31
- * handshake reaps its runtime and swaps in a fresh instance, so do not
32
- * cache this across a failed {@link start}.
32
+ * handshake swaps in a fresh instance only after cleanup proves the runtime
33
+ * exited; cleanup failure retains this client, so do not cache it across a
34
+ * failed {@link start}.
33
35
  * @returns the client currently owning the runtime subprocess.
34
36
  */
35
37
  get client(): HarnessClient;
36
38
  /**
37
39
  * Start the subprocess and perform the `initialize` handshake once. On
38
- * failure the runtime is reaped and a fresh client replaces it
39
- * (`HarnessClient.close` is permanent), so a later call retries with a new
40
- * subprocess unless {@link close} already ended this harness.
40
+ * failure, successful SDK-owned cleanup reaps the runtime and installs a
41
+ * fresh client (`HarnessClient.close` is permanent), so a later call retries
42
+ * with a new subprocess unless {@link close} already ended this harness. If
43
+ * cleanup also fails, rejects with an `AggregateError` whose ordered errors
44
+ * preserve both causes and retains the failed client rather than spawning
45
+ * alongside a process whose exit was not proved.
41
46
  * @returns settlement of the (memoized) handshake.
42
47
  */
43
48
  start(): Promise<void>;
@@ -54,7 +59,7 @@ export declare class DeepSeekHarness implements AsyncDisposable {
54
59
  * @param options - optional session id and per-notification observer.
55
60
  * @returns the owned activity interval.
56
61
  */
57
- run(input: string | ContentBlock[], options?: RunOptions): Promise<RunResult>;
62
+ run(input: string | SdkPromptContentBlock[], options?: RunOptions): Promise<RunResult>;
58
63
  /**
59
64
  * Shut down and reap the runtime subprocess. Idempotent and terminal —
60
65
  * a closed harness no longer retries a failed handshake.
@@ -67,6 +72,8 @@ export declare class DeepSeekHarness implements AsyncDisposable {
67
72
  */
68
73
  [Symbol.asyncDispose](): Promise<void>;
69
74
  }
75
+ /** Construct the high-level API against a generic process for package-local fake-runtime tests. */
76
+ export declare function createProcessDeepSeekHarness(runtime: RuntimeProcessOptions, options?: DeepSeekHarnessOptions): DeepSeekHarness;
70
77
  /** Per-run options: target session and streaming observer. */
71
78
  export interface RunOptions {
72
79
  /** Session id to run on; omitted mints a fresh session per call. */
@@ -92,14 +99,14 @@ export declare class HarnessSession {
92
99
  * @returns the owned activity interval; rejects on transport loss, timeout,
93
100
  * or a protocol error.
94
101
  */
95
- run(input: string | ContentBlock[], options?: Pick<RunOptions, 'onNotification'>): Promise<RunResult>;
102
+ run(input: string | SdkPromptContentBlock[], options?: Pick<RunOptions, 'onNotification'>): Promise<RunResult>;
96
103
  }
97
104
  /**
98
105
  * Normalize run input: a string becomes one text block; blocks pass verbatim.
99
106
  * @param input - prompt text or content blocks.
100
107
  * @returns the content blocks to send.
101
108
  */
102
- export declare function normalizeInput(input: string | ContentBlock[]): ContentBlock[];
109
+ export declare function normalizeInput(input: string | SdkPromptContentBlock[]): SdkPromptContentBlock[];
103
110
  /**
104
111
  * Extract the concatenated text of the last assistant message.
105
112
  * @param events - the activity interval's `session.event` payloads in wire order.
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * @module @deepseek-ai/dsh-sdk-client/client
13
13
  */
14
- import { type InitializeParams, type InitializeResult } from '@deepseek-ai/dsh-sdk-protocol';
15
- import type { ContentBlock } from '@deepseek-ai/dsh-llm';
14
+ import { type InitializeParams, type InitializeResult, type SdkPromptContentBlock } from '@deepseek-ai/dsh-sdk-protocol';
15
+ import { type RuntimeProcessOptions } from './launch.ts';
16
16
  import type { HarnessClientOptions, HarnessNotification, NotificationFilter } from './types.ts';
17
17
  /**
18
18
  * The runtime subprocess is gone or unusable: it exited, its stdio closed, or
@@ -63,7 +63,9 @@ export interface NotificationSubscription extends AsyncIterable<HarnessNotificat
63
63
  * runtime is closed.
64
64
  */
65
65
  export declare class HarnessClient {
66
+ /** Original public dsh launch and timeout options for this client. */
66
67
  readonly options: HarnessClientOptions;
68
+ private readonly runtime;
67
69
  private child;
68
70
  private transport;
69
71
  private readonly stderrTail;
@@ -74,8 +76,8 @@ export declare class HarnessClient {
74
76
  private spawnError;
75
77
  private streamsSettled;
76
78
  private closeTask;
77
- /** @param options - launch spec, complete child environment, and timeouts. */
78
- constructor(options: HarnessClientOptions);
79
+ /** @param options - dsh profile, patch, home, process, environment, and timeout options. */
80
+ constructor(options?: HarnessClientOptions);
79
81
  /**
80
82
  * Spawn the runtime subprocess and start reading frames. Idempotent while
81
83
  * the process is live; rejects reuse after {@link close}.
@@ -93,7 +95,7 @@ export declare class HarnessClient {
93
95
  * @param contentBlocks - the user message, sent verbatim.
94
96
  * @returns the queued message id.
95
97
  */
96
- prompt(sessionId: string, contentBlocks: ContentBlock[]): Promise<string>;
98
+ prompt(sessionId: string, contentBlocks: SdkPromptContentBlock[]): Promise<string>;
97
99
  /**
98
100
  * Send one JSON-RPC request and await its result.
99
101
  * @param method - the wire method name.
@@ -114,8 +116,8 @@ export declare class HarnessClient {
114
116
  subscribe(filter?: NotificationFilter): NotificationSubscription;
115
117
  /**
116
118
  * Subscribe to one session and the descendants discovered from
117
- * `subagent.started` lineage edges (the runtime notifies for every session
118
- * in its context; scoping is client-side, mirroring the Python SDK).
119
+ * `subagent.started` lineage edges. The runtime notifies for every session
120
+ * in its context, so this client applies the scope.
119
121
  * @param sessionId - the root session id.
120
122
  * @returns the filtered subscription handle.
121
123
  */
@@ -136,6 +138,8 @@ export declare class HarnessClient {
136
138
  private settleStreams;
137
139
  private closedError;
138
140
  }
141
+ /** Construct the transport against a generic process for package-local fake-runtime tests. */
142
+ export declare function createProcessHarnessClient(options: RuntimeProcessOptions): HarnessClient;
139
143
  /**
140
144
  * Whether `value` is a plain JSON object (the wire-boundary shape probe).
141
145
  * @param value - the wire value to probe.
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * TypeScript client SDK for the DeepSeek Harness runtime: spawn the
3
- * `dsh-jsonrpc-agent` runtime as a subprocess and drive agent turns over
4
- * stdio JSON-RPC. `DeepSeekHarness` is the high-level run API;
3
+ * same-version `dsh --profile sdk` runtime as a subprocess and drive agent
4
+ * turns over stdio JSON-RPC. `DeepSeekHarness` is the high-level run API;
5
5
  * `HarnessClient` is the lower-level protocol client. A pure library — it
6
- * registers nothing on a Cordis context; the runtime process it spawns is a
7
- * complete harness configured by its own `cordis.yml`.
6
+ * registers nothing on a Cordis context; named profiles and ordered patch
7
+ * files customize the runtime process it spawns.
8
8
  *
9
9
  * @module @deepseek-ai/dsh-sdk-client
10
10
  */
@@ -13,5 +13,5 @@ export type { RunOptions } from './api.ts';
13
13
  export { HarnessClient, RequestTimeoutError, SdkProtocolError, TransportClosedError, } from './client.ts';
14
14
  export type { NotificationSubscription } from './client.ts';
15
15
  export { JsonRpcResponseError } from '@deepseek-ai/dsh-sdk-protocol';
16
- export type { ContentBlock, DeepSeekHarnessOptions, HarnessClientOptions, HarnessNotification, NotificationFilter, RunResult, } from './types.ts';
16
+ export type { ContentBlock, SdkPromptContentBlock, DeepSeekHarnessOptions, HarnessClientOptions, HarnessNotification, NotificationFilter, RunResult, } from './types.ts';
17
17
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Resolve the public SDK launch configuration to one dsh subprocess.
3
+ * @module @deepseek-ai/dsh-sdk-client/launch
4
+ */
5
+ import type { HarnessClientOptions } from './types.ts';
6
+ /** Default bound for a profile to answer the SDK initialize handshake. */
7
+ export declare const DEFAULT_INITIALIZE_TIMEOUT_MS = 10000;
8
+ /** Internal generic process launch used by the transport and fake-runtime tests. */
9
+ export interface RuntimeProcessOptions {
10
+ command: string;
11
+ args: string[];
12
+ cwd?: string;
13
+ /** Materialize the complete child environment when the client starts its subprocess. */
14
+ environment: () => NodeJS.ProcessEnv;
15
+ description: string;
16
+ initializeTimeoutMs: number;
17
+ requestTimeoutMs?: number;
18
+ shutdownTimeoutMs?: number;
19
+ disposeEofGraceMs?: number;
20
+ disposeGraceMs?: number;
21
+ }
22
+ /** Node argv plus internal profile patches required by one resolved dsh entry. */
23
+ export interface DshNodeLaunch {
24
+ /** Arguments before the profile selector. */
25
+ nodeArgs: string[];
26
+ /** Internal patches applied below caller-supplied patches. */
27
+ patches: string[];
28
+ /** Environment values required by the resolved entry mode. */
29
+ environment: NodeJS.ProcessEnv;
30
+ }
31
+ /**
32
+ * Resolve and version-check a dsh executable from package manifests.
33
+ * @param dshManifestUrl - resolved URL of the dsh package manifest.
34
+ * @param clientManifestUrl - resolved URL of the SDK client manifest.
35
+ * @returns the absolute dsh executable path.
36
+ */
37
+ export declare function resolveDshBinFromManifests(dshManifestUrl: string, clientManifestUrl: string): string;
38
+ /**
39
+ * Resolve and version-check the built dsh executable installed with this SDK.
40
+ * @returns the absolute built executable path, whether or not it exists in a source checkout.
41
+ */
42
+ export declare function installedDshBin(): string;
43
+ /**
44
+ * Resolve the Node launch for one same-version dsh package.
45
+ * @param dshManifestUrl - resolved URL of the dsh package manifest.
46
+ * @param clientManifestUrl - resolved URL of the SDK client manifest.
47
+ * @param sourceLoaderUrl - optional absolute tsx loader URL for deterministic tests.
48
+ * @returns built output, or the source entry plus its compatibility patch and tsx environment.
49
+ */
50
+ export declare function resolveDshNodeLaunchFromManifests(dshManifestUrl: string, clientManifestUrl: string, sourceLoaderUrl?: string): DshNodeLaunch;
51
+ /**
52
+ * Resolve caller-relative filesystem inputs and construct canonical dsh argv.
53
+ * @param options - public SDK launch options.
54
+ * @param callerCwd - parent-process directory used for lexical resolution.
55
+ * @returns one generic subprocess spec for the JSON-RPC transport.
56
+ */
57
+ export declare function resolveDshLaunch(options?: HarnessClientOptions, callerCwd?: string): RuntimeProcessOptions;
58
+ //# sourceMappingURL=launch.d.ts.map
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * @module @deepseek-ai/dsh-sdk-client/types
6
6
  */
7
- import type { ContentBlock } from '@deepseek-ai/dsh-llm';
7
+ import type { ContentBlock, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
8
+ import type { SdkPromptContentBlock } from '@deepseek-ai/dsh-sdk-protocol';
8
9
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
9
10
  /** One server-to-client notification as received off the wire. */
10
11
  export interface HarnessNotification {
@@ -17,19 +18,26 @@ export interface HarnessNotification {
17
18
  export type NotificationFilter = (notification: HarnessNotification) => boolean;
18
19
  /** Launch and timeout options for {@link HarnessClient}. */
19
20
  export interface HarnessClientOptions {
20
- /** The runtime executable (the `dsh-jsonrpc-agent` bin, a packaged exe, or `node`). */
21
- command: string;
22
- /** Arguments passed to {@link command}. */
23
- args?: string[];
24
- /** Working directory for the runtime process itself. */
25
- cwd?: string;
21
+ /** Absolute or caller-relative dsh CLI module; omitted resolves this package's same-version dependency. */
22
+ dshBin?: string;
23
+ /** Named profile serving the SDK protocol (default `sdk`). */
24
+ profile?: string;
25
+ /** Ordered per-launch profile patches; relative paths resolve before spawn. */
26
+ patches?: string[];
27
+ /** Explicit Harness home for this child; relative paths resolve before spawn. */
28
+ dshHome?: string;
29
+ /** Working directory for the dsh process itself. */
30
+ processCwd?: string;
26
31
  /**
27
- * The complete child environment. `undefined` inherits the parent env
28
- * verbatim; passing an object replaces it entirely, so callers own
32
+ * The complete child environment, read when {@link HarnessClient.start}
33
+ * spawns. `undefined` reads the parent env at that time; passing an object
34
+ * reads that object at spawn and replaces the parent environment entirely, so callers own
29
35
  * credential policy (see `scrubbedParentEnv` in `@deepseek-ai/dsh-subprocess`
30
36
  * for the shared scrub-then-merge base).
31
37
  */
32
38
  env?: NodeJS.ProcessEnv;
39
+ /** Bound (ms) on the initial profile handshake (default 10000). */
40
+ initializeTimeoutMs?: number;
33
41
  /** Per-request timeout (ms); `undefined` waits indefinitely (a turn can legitimately run long). */
34
42
  requestTimeoutMs?: number;
35
43
  /** Bound (ms) on the protocol `shutdown` exchange inside `close()` (default 1000). */
@@ -40,15 +48,15 @@ export interface HarnessClientOptions {
40
48
  disposeGraceMs?: number;
41
49
  }
42
50
  /** Options for the high-level {@link DeepSeekHarness} wrapper. */
43
- export interface DeepSeekHarnessOptions {
44
- /** Launch spec for the runtime subprocess (command, args, cwd, env, timeouts). */
45
- launch: HarnessClientOptions;
46
- /** Workspace cwd recorded on every SDK-created session (default: the launch cwd, else `process.cwd()`). */
51
+ export interface DeepSeekHarnessOptions extends HarnessClientOptions {
52
+ /** Workspace cwd recorded on every SDK-created session (default: the process cwd, else `process.cwd()`). */
47
53
  cwd?: string;
48
54
  /** Provider route for SDK-created agents (default `deepseek-official`). */
49
55
  provider?: string;
50
56
  /** Model for SDK-created agents (default `deepseek-v4-flash`). */
51
57
  model?: string;
58
+ /** Adapter-owned reasoning effort for the selected provider/model route. */
59
+ reasoningEffort?: ReasoningEffortId;
52
60
  /** Maximum output tokens for each conversation-model request. */
53
61
  maxTokens?: number;
54
62
  }
@@ -65,4 +73,5 @@ export interface RunResult {
65
73
  }
66
74
  /** Re-exported content-block alias so SDK callers need no extra import. */
67
75
  export type { ContentBlock };
76
+ export type { SdkPromptContentBlock };
68
77
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-sdk-client",
3
3
  "description": "TypeScript client SDK for driving a DeepSeek Harness runtime subprocess over stdio JSON-RPC: the DeepSeekHarness high-level turns API and the lower-level HarnessClient",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,18 +30,21 @@
30
30
  "lib/types/**/*.d.ts"
31
31
  ],
32
32
  "license": "MIT",
33
+ "dependencies": {
34
+ "@deepseek-ai/dsh": "0.1.2-alpha.2"
35
+ },
33
36
  "peerDependencies": {
34
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
35
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
36
- "@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
37
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
38
- "@deepseek-ai/cordis": "^4.0.1"
37
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
38
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
39
+ "@deepseek-ai/dsh-sdk-protocol": "^0.1.2-alpha.2",
40
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
41
+ "@deepseek-ai/cordis": "^4.0.2"
39
42
  },
40
43
  "devDependencies": {
41
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
42
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
43
- "@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
44
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
45
- "@deepseek-ai/cordis": "^4.0.1"
44
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
45
+ "@deepseek-ai/dsh-sdk-protocol": "^0.1.2-alpha.2",
46
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
47
+ "@deepseek-ai/cordis": "^4.0.2",
48
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
46
49
  }
47
50
  }