@deepseek-ai/dsh-sdk-client 0.0.1-rc.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/scaffold/client/README.md
5
+ README.md: b33457875f81d11d09bab2e5aa5ce730e233c78a
6
+ README.zh.md: 271f07ffb0f97abe005971962beb517acfdc05a4
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @deepseek-ai/dsh-sdk-client
2
+
3
+ English | [中文](README.zh.md)
4
+
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.
6
+
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.
8
+
9
+ ## DeepSeekHarness
10
+
11
+ ```ts
12
+ import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
13
+
14
+ await using harness = new DeepSeekHarness({
15
+ launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
16
+ provider: 'deepseek-official',
17
+ model: 'deepseek-v4-flash',
18
+ maxTokens: 49_152,
19
+ })
20
+ const result = await harness.run('say hi')
21
+ console.log(result.finalResponse)
22
+ ```
23
+
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.
25
+
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.
27
+
28
+ ## HarnessClient
29
+
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).
31
+
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.
33
+
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.
35
+
36
+ ## Model Experience
37
+
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.
39
+
40
+ #### KV Cache effect
41
+
42
+ None; this package neither assembles nor sends a provider request.
43
+
44
+ ## Known Limitations and Deferred Work
45
+
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.
49
+ - **Client→server notifications and server→client requests are unimplemented** on both wire ends; the transport carries them for future approval flows.
package/README.zh.md ADDED
@@ -0,0 +1,49 @@
1
+ # @deepseek-ai/dsh-sdk-client
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层自有运行 API,`HarnessClient` 是低层协议客户端。包(package)根枚举消费方接口:两层客户端、面向调用方的类型和 `JsonRpcResponseError`;源模块、规范化辅助函数与订阅投递机制不供消费方导入。纯库:不在任何 Cordis 上下文注册;它所 spawn 的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
6
+
7
+ 与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费方,包括 [`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.md) 后端和自动化;它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
8
+
9
+ ## DeepSeekHarness
10
+
11
+ ```ts
12
+ import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
13
+
14
+ await using harness = new DeepSeekHarness({
15
+ launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
16
+ provider: 'deepseek-official',
17
+ model: 'deepseek-v4-flash',
18
+ maxTokens: 49_152,
19
+ })
20
+ const result = await harness.run('say hi')
21
+ console.log(result.finalResponse)
22
+ ```
23
+
24
+ 子进程在首次使用时惰性启动,并在多次 `run()` 之间持续归实例所有;必须 `close()`(或 `await using`),子进程才总能被回收。`start()` 记忆化 `initialize` 握手(工作区 cwd——在通过协议传输之前解析为绝对路径——加 provider/model 路由和可选的正整数 `maxTokens` 输出上限);握手失败会回收运行时并换入全新客户端,后续调用用新子进程重试(直到终结性的 `close()`)。该上限作用于根 agent(智能体)的每次请求,并由进程内后代继承;压缩(compaction)插件单独持有摘要上限。`session(id?)` 打开具名或全新的会话句柄。
25
+
26
+ `run(input, { sessionId?, onNotification? })` 拥有一个活动区间:它将提示词排入队列,等待其 `MessageId` 出现在持久的 `agent/inbox/spliced` 回执中,然后持续收集到整个 agent 下一次进入 `idle`。它返回 `RunResult { sessionId, finalResponse, events, notifications }`。`finalResponse` 是该区间内根会话最后提交的助手文本,并非因果上归属于该提示词的响应;steering(中途引导)、注入的上下文和其他排队工作都可能在 idle 前参与其中。`events` 包含根会话事件,`notifications` 还包含通过 `subagent.started` 发现的后代,均按协议传输顺序排列。结果不携带提示词级状态或轮次原因。传输丢失、超时和协议违例会导致 Promise 被拒绝;模型结果仍可在事件流中观察,但不会归属于某一输入。
27
+
28
+ ## HarnessClient
29
+
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 尾部)。
31
+
32
+ `close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。该阶梯为本客户端私有:它运行在任何 harness 上下文之外,无法搭乘 [`dsh-subprocess`](../../subprocess/README.md) 服务——即该 seam 所记录的 SDK 托管传输例外。幂等,已关闭的客户端拒绝复用。
33
+
34
+ `HarnessClientOptions.env` 给定时整体替换子进程环境(`undefined` 原样继承父进程环境);凭据策略归调用方——`dsh-subprocess` 的 `scrubbedParentEnv` 是面向隔离启动的共享擦除基底。
35
+
36
+ ## 模型体验
37
+
38
+ 无,因为这是一个客户端进程库;模型运行在 spawn 出的运行时中,其体验由该运行时的 `cordis.yml` 所组合的插件决定。
39
+
40
+ #### KV Cache 影响
41
+
42
+ 无;本包既不组装也不发送提供方请求。
43
+
44
+ ## 已知限制与暂缓事项
45
+
46
+ - **无捆绑运行时解析**——调用方显式指定运行时可执行文件;打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费方。
47
+ - **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见协议的 [已知限制](../protocol/README.md))。
48
+ - **没有逐提示词结果或取消**——低层 `prompt()` 只返回入队回执;高层 `run()` 负责从回执收集到 idle,放弃该过程意味着关闭运行时。
49
+ - **客户端→服务端通知与服务端→客户端请求**在协议两端都未实现;传输层为未来审批流保留了承载能力。
package/lib/index.js ADDED
@@ -0,0 +1,716 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { JsonRpcLineTransport, JsonRpcResponseError, JsonRpcResponseError as JsonRpcResponseError$1 } from "@deepseek-ai/dsh-sdk-protocol";
5
+ //#region lib/types/dispose.js
6
+ /**
7
+ * Private teardown ladder for the runtime subprocess: stdin EOF (cooperative
8
+ * quiesce), then SIGTERM, then SIGKILL, resolving only after the process has
9
+ * actually exited. The SDK client runs OUTSIDE any harness context, so it
10
+ * cannot ride the `dsh-subprocess` service — this module is the seam's
11
+ * documented exception for SDK-managed transports.
12
+ *
13
+ * @module @deepseek-ai/dsh-sdk-client/dispose
14
+ */
15
+ /**
16
+ * Race the child's exit against a timer. Neither outcome leaves anything
17
+ * behind on the child: the exit listener is removed on timeout and the timer
18
+ * is cleared on exit, so the ladder's tiers never accumulate listeners.
19
+ */
20
+ function exitsWithin(child, ms) {
21
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
22
+ return new Promise((resolve) => {
23
+ const onExit = () => {
24
+ clearTimeout(timer);
25
+ resolve(true);
26
+ };
27
+ const timer = setTimeout(() => {
28
+ child.removeListener("exit", onExit);
29
+ resolve(false);
30
+ }, ms).unref();
31
+ child.once("exit", onExit);
32
+ });
33
+ }
34
+ /** Force-terminate the runtime and reject if no exit edge arrives within the grace. */
35
+ function forceTerminateWithin(child, ms) {
36
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
37
+ return new Promise((resolve, reject) => {
38
+ let accepted = false;
39
+ let settled = false;
40
+ const cleanup = () => {
41
+ clearTimeout(timer);
42
+ child.off("exit", onExit);
43
+ child.off("error", onError);
44
+ };
45
+ const settle = (complete) => {
46
+ if (settled) return;
47
+ settled = true;
48
+ cleanup();
49
+ complete();
50
+ };
51
+ const onExit = () => {
52
+ settle(resolve);
53
+ };
54
+ const onError = (error) => {
55
+ settle(() => {
56
+ reject(error);
57
+ });
58
+ };
59
+ child.once("exit", onExit);
60
+ child.once("error", onError);
61
+ const timer = setTimeout(() => {
62
+ const disposition = accepted ? "accepted" : "refused";
63
+ settle(() => {
64
+ reject(/* @__PURE__ */ new Error(`runtime process did not exit within ${ms}ms after SIGKILL was ${disposition}`));
65
+ });
66
+ }, ms).unref();
67
+ try {
68
+ accepted = child.kill("SIGKILL");
69
+ if (child.exitCode !== null || child.signalCode !== null) settle(resolve);
70
+ } catch (error) {
71
+ settle(() => {
72
+ reject(new Error("SIGKILL failed", { cause: error }));
73
+ });
74
+ }
75
+ });
76
+ }
77
+ /**
78
+ * Tear the runtime down to quiescence, resolving only after exit: close stdin
79
+ * and allow cooperative flush, then use the host's graceful and forced
80
+ * termination semantics. POSIX sends `SIGTERM` before `SIGKILL`; Windows
81
+ * skips directly to forced termination because Node maps both signals to
82
+ * `TerminateProcess`.
83
+ * @param child - the runtime child process to tear down.
84
+ * @param graces - the EOF and termination-confirmation windows (ms).
85
+ * @param platform - the host platform, injectable for unit coverage.
86
+ * @throws When forced termination errors or the child does not report exit
87
+ * within `disposeGraceMs`.
88
+ */
89
+ async function disposeRuntimeProcess(child, graces, platform = process.platform) {
90
+ if (child.exitCode !== null || child.signalCode !== null) return;
91
+ child.stdin?.end();
92
+ if (await exitsWithin(child, graces.disposeEofGraceMs)) return;
93
+ if (platform !== "win32") {
94
+ child.kill("SIGTERM");
95
+ if (await exitsWithin(child, graces.disposeGraceMs)) return;
96
+ }
97
+ await forceTerminateWithin(child, graces.disposeGraceMs);
98
+ }
99
+ //#endregion
100
+ //#region lib/types/client.js
101
+ /**
102
+ * Low-level JSON-RPC client for a DeepSeek Harness SDK runtime subprocess.
103
+ * {@link HarnessClient} owns the child process: it spawns the runtime, speaks
104
+ * the `@deepseek-ai/dsh-sdk-protocol` wire over the child's stdio, fans
105
+ * server notifications out to subscriptions, and tears the child down to
106
+ * quiescence through a private EOF → SIGTERM → SIGKILL ladder. The design
107
+ * twin is the Python SDK's `HarnessClient` (`python/sdk`); both drive the
108
+ * same runtime protocol. This client runs OUTSIDE any harness context, so it
109
+ * spawns directly rather than through the `dsh-subprocess` service — the
110
+ * seam's documented exception for SDK-managed transports.
111
+ *
112
+ * @module @deepseek-ai/dsh-sdk-client/client
113
+ */
114
+ /** Retained stderr lines used to diagnose an unexpected runtime death. */
115
+ const STDERR_TAIL_LIMIT = 400;
116
+ /** Grace for the runtime's stdio streams to settle after its exit edge. */
117
+ const STREAM_SETTLE_MS = 100;
118
+ /**
119
+ * The runtime subprocess is gone or unusable: it exited, its stdio closed, or
120
+ * it was never launchable. The message carries the exit code and a stderr
121
+ * tail when available.
122
+ */
123
+ var TransportClosedError = class extends Error {
124
+ /** @param message - the failure description, including any stderr tail. */
125
+ constructor(message) {
126
+ super(message);
127
+ this.name = "TransportClosedError";
128
+ }
129
+ };
130
+ /** A request exceeded {@link HarnessClientOptions.requestTimeoutMs}. */
131
+ var RequestTimeoutError = class extends Error {
132
+ /** @param message - which method timed out. */
133
+ constructor(message) {
134
+ super(message);
135
+ this.name = "RequestTimeoutError";
136
+ }
137
+ };
138
+ /**
139
+ * The runtime answered outside its documented protocol (for example a
140
+ * `session/prompt` response without `accepted: true`).
141
+ */
142
+ var SdkProtocolError = class extends Error {
143
+ /** @param message - the protocol violation description. */
144
+ constructor(message) {
145
+ super(message);
146
+ this.name = "SdkProtocolError";
147
+ }
148
+ };
149
+ /** Internal producer side of a public notification subscription. */
150
+ var NotificationSubscriptionImpl = class {
151
+ state;
152
+ unsubscribe;
153
+ constructor(state, unsubscribe) {
154
+ this.state = state;
155
+ this.unsubscribe = unsubscribe;
156
+ }
157
+ /**
158
+ * Await the next matching notification.
159
+ * @returns the notification; after the runtime died, drains what was
160
+ * already delivered and then rejects; after {@link close}, rejects
161
+ * immediately (the queue is dropped).
162
+ */
163
+ next() {
164
+ const queued = this.state.queue.shift();
165
+ if (queued !== void 0) return Promise.resolve(queued);
166
+ if (this.state.failure !== void 0) return Promise.reject(this.state.failure);
167
+ return new Promise((resolve, reject) => {
168
+ this.state.waiters.push({
169
+ resolve,
170
+ reject
171
+ });
172
+ });
173
+ }
174
+ /**
175
+ * Drain one already-delivered notification without waiting.
176
+ * @returns the next queued notification, or `undefined` when none is queued.
177
+ */
178
+ tryNext() {
179
+ return this.state.queue.shift();
180
+ }
181
+ /** Detach from the client; queued items drop and pending waiters reject. */
182
+ close() {
183
+ this.unsubscribe();
184
+ this.state.queue.length = 0;
185
+ this.fail(new TransportClosedError("notification subscription closed"));
186
+ }
187
+ /**
188
+ * Reject pending and future waits (delivery stops; the first failure wins).
189
+ * Already-queued notifications remain drainable via {@link next}/{@link tryNext}.
190
+ * @param error - the terminal failure delivered to waiters.
191
+ */
192
+ fail(error) {
193
+ this.state.failure ??= error;
194
+ for (const waiter of this.state.waiters.splice(0)) waiter.reject(this.state.failure);
195
+ }
196
+ /**
197
+ * Deliver one notification to a waiter or the queue when the filter
198
+ * matches. A throwing filter fails only THIS subscription (detached, the
199
+ * throw becomes its terminal error) — it never disturbs sibling
200
+ * subscriptions or the transport's read loop, mirroring the Python client.
201
+ * @param notification - the wire notification to deliver.
202
+ */
203
+ push(notification) {
204
+ let matches;
205
+ try {
206
+ matches = this.state.filter === void 0 || this.state.filter(notification);
207
+ } catch (error) {
208
+ this.unsubscribe();
209
+ this.fail(error instanceof Error ? error : new Error(String(error)));
210
+ return;
211
+ }
212
+ if (!matches) return;
213
+ const waiter = this.state.waiters.shift();
214
+ if (waiter !== void 0) waiter.resolve(notification);
215
+ else this.state.queue.push(notification);
216
+ }
217
+ /**
218
+ * Iterate notifications until the subscription or runtime closes (the
219
+ * terminating rejection propagates).
220
+ * @returns an async iterator over {@link next} results.
221
+ */
222
+ async *[Symbol.asyncIterator]() {
223
+ for (;;) yield await this.next();
224
+ }
225
+ };
226
+ /**
227
+ * JSON-RPC client for the DeepSeek Harness SDK runtime over subprocess stdio.
228
+ *
229
+ * The subprocess starts lazily on {@link start} and is owned by this instance
230
+ * until {@link close}, which requests protocol `shutdown` and then walks the
231
+ * shared EOF → SIGTERM → SIGKILL dispose ladder to quiescence. There is no
232
+ * wire-level cancel: a timed-out request stays running server-side until the
233
+ * runtime is closed.
234
+ */
235
+ var HarnessClient = class {
236
+ options;
237
+ child;
238
+ transport;
239
+ stderrTail = [];
240
+ subscriptions = /* @__PURE__ */ new Map();
241
+ sessionParents = /* @__PURE__ */ new Map();
242
+ subscriptionSerial = 0;
243
+ exitCode;
244
+ spawnError;
245
+ streamsSettled = Promise.resolve();
246
+ closeTask;
247
+ /** @param options - launch spec, complete child environment, and timeouts. */
248
+ constructor(options) {
249
+ this.options = options;
250
+ }
251
+ /**
252
+ * Spawn the runtime subprocess and start reading frames. Idempotent while
253
+ * the process is live; rejects reuse after {@link close}.
254
+ */
255
+ start() {
256
+ if (this.closeTask !== void 0) throw new TransportClosedError("DeepSeek Harness runtime client is closed");
257
+ 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,
261
+ stdio: [
262
+ "pipe",
263
+ "pipe",
264
+ "pipe"
265
+ ]
266
+ });
267
+ this.child = child;
268
+ child.once("error", (error) => {
269
+ this.spawnError = error;
270
+ this.transport?.close();
271
+ this.failSubscriptions(this.closedError("DeepSeek Harness runtime failed to start"));
272
+ });
273
+ /* v8 ignore next */
274
+ child.stdin.on("error", () => {});
275
+ let stderrBuffer = "";
276
+ child.stderr.setEncoding("utf8");
277
+ child.stderr.on("data", (chunk) => {
278
+ stderrBuffer += chunk;
279
+ const newline = stderrBuffer.lastIndexOf("\n");
280
+ if (newline >= 0) {
281
+ this.appendStderr(stderrBuffer.slice(0, newline).split("\n"));
282
+ stderrBuffer = stderrBuffer.slice(newline + 1);
283
+ }
284
+ });
285
+ let signalStreamsSettled;
286
+ this.streamsSettled = new Promise((resolve) => {
287
+ signalStreamsSettled = resolve;
288
+ });
289
+ const settled = {
290
+ stderr: false,
291
+ exited: false
292
+ };
293
+ const maybeSettle = () => {
294
+ if (settled.stderr && settled.exited) signalStreamsSettled();
295
+ };
296
+ child.stderr.once("close", () => {
297
+ if (stderrBuffer.length > 0) this.appendStderr([stderrBuffer]);
298
+ settled.stderr = true;
299
+ maybeSettle();
300
+ });
301
+ child.once("exit", (code) => {
302
+ this.exitCode = code;
303
+ settled.exited = true;
304
+ maybeSettle();
305
+ this.failSubscriptions(this.closedError("DeepSeek Harness runtime exited"));
306
+ });
307
+ child.once("close", () => {
308
+ this.transport?.close();
309
+ });
310
+ const transport = new JsonRpcLineTransport(child.stdout, child.stdin);
311
+ transport.onNotification((method, params) => {
312
+ this.dispatchNotification({
313
+ method,
314
+ params
315
+ });
316
+ });
317
+ transport.start();
318
+ this.transport = transport;
319
+ }
320
+ /**
321
+ * Perform the process-wide handshake.
322
+ * @param params - workspace cwd plus the provider/model route.
323
+ * @returns the runtime's wire identity.
324
+ */
325
+ async initialize(params) {
326
+ const result = await this.request("initialize", { ...params });
327
+ 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
+ return { serverInfo: {
329
+ name: result.serverInfo.name,
330
+ version: result.serverInfo.version
331
+ } };
332
+ }
333
+ /**
334
+ * Queue one prompt and return its durable inbox identity.
335
+ * @param sessionId - target session; an unknown id creates it.
336
+ * @param contentBlocks - the user message, sent verbatim.
337
+ * @returns the queued message id.
338
+ */
339
+ async prompt(sessionId, contentBlocks) {
340
+ const params = {
341
+ sessionId,
342
+ contentBlocks
343
+ };
344
+ const result = await this.request("session/prompt", { ...params });
345
+ if (!isRecord(result) || typeof result.messageId !== "string") throw new SdkProtocolError(`session/prompt returned no message id: ${JSON.stringify(result)}`);
346
+ return result.messageId;
347
+ }
348
+ /**
349
+ * Send one JSON-RPC request and await its result.
350
+ * @param method - the wire method name.
351
+ * @param params - the params object; omitted params send `{}`.
352
+ * @param timeoutMs - per-call override of {@link HarnessClientOptions.requestTimeoutMs}.
353
+ * @returns the raw result; rejects with {@link JsonRpcResponseError} on a
354
+ * protocol error response, {@link RequestTimeoutError} on timeout, and
355
+ * {@link TransportClosedError} when the runtime is gone.
356
+ */
357
+ async request(method, params, timeoutMs) {
358
+ this.start();
359
+ if (this.exitCode !== void 0 || this.spawnError !== void 0) {
360
+ await this.settleStreams();
361
+ throw this.closedError("DeepSeek Harness runtime is not running");
362
+ }
363
+ const transport = this.transport;
364
+ /* v8 ignore next -- start() either sets the transport or throws */
365
+ if (transport === void 0) throw new TransportClosedError("DeepSeek Harness runtime is not running");
366
+ const timeout = timeoutMs ?? this.options.requestTimeoutMs;
367
+ try {
368
+ if (timeout === void 0) return await transport.request(method, params ?? {});
369
+ const abandon = new AbortController();
370
+ const timer = setTimeout(() => {
371
+ abandon.abort(new RequestTimeoutError(`${method} timed out after ${timeout}ms waiting for the DeepSeek Harness runtime`));
372
+ }, timeout);
373
+ try {
374
+ return await transport.request(method, params ?? {}, abandon.signal);
375
+ } finally {
376
+ clearTimeout(timer);
377
+ }
378
+ } catch (error) {
379
+ if (error instanceof JsonRpcResponseError$1 || error instanceof RequestTimeoutError) throw error;
380
+ await this.settleStreams();
381
+ throw this.closedError(errorMessage(error));
382
+ }
383
+ }
384
+ /**
385
+ * Subscribe to server notifications.
386
+ * @param filter - optional predicate; omitted means every notification.
387
+ * @returns the subscription handle; close it to stop delivery. After
388
+ * {@link close} or runtime death the handle is born failed — there is no
389
+ * producer left, so `next()` rejects instead of waiting forever.
390
+ */
391
+ subscribe(filter) {
392
+ const id = String(this.subscriptionSerial++);
393
+ const subscription = new NotificationSubscriptionImpl({
394
+ queue: [],
395
+ waiters: [],
396
+ filter,
397
+ failure: void 0
398
+ }, () => {
399
+ this.subscriptions.delete(id);
400
+ });
401
+ if (this.closeTask !== void 0 || this.exitCode !== void 0 || this.spawnError !== void 0) {
402
+ subscription.fail(this.closedError("DeepSeek Harness runtime closed"));
403
+ return subscription;
404
+ }
405
+ this.subscriptions.set(id, subscription);
406
+ return subscription;
407
+ }
408
+ /**
409
+ * 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).
412
+ * @param sessionId - the root session id.
413
+ * @returns the filtered subscription handle.
414
+ */
415
+ subscribeSessionTree(sessionId) {
416
+ return this.subscribe((notification) => {
417
+ const params = notification.params;
418
+ if (notification.method === "subagent.started" || notification.method === "subagent.finished") {
419
+ const parentId = params.parentSessionId;
420
+ if (typeof parentId === "string" && this.isDescendantOf(parentId, sessionId)) return true;
421
+ return params.childSessionId === sessionId;
422
+ }
423
+ const relatedId = params.sessionId;
424
+ return typeof relatedId === "string" && this.isDescendantOf(relatedId, sessionId);
425
+ });
426
+ }
427
+ /**
428
+ * Shut the runtime down and reap it: a best-effort protocol `shutdown`
429
+ * bounded by `shutdownTimeoutMs`, then the shared stdin-EOF → SIGTERM →
430
+ * SIGKILL ladder until the process actually exited. Idempotent.
431
+ * @returns settlement of the complete teardown.
432
+ */
433
+ close() {
434
+ this.closeTask ??= this.performClose();
435
+ return this.closeTask;
436
+ }
437
+ async performClose() {
438
+ const child = this.child;
439
+ if (child === void 0) return;
440
+ try {
441
+ await this.request("shutdown", void 0, this.options.shutdownTimeoutMs ?? 1e3);
442
+ } catch (error) {
443
+ this.appendStderr([`shutdown request failed: ${errorMessage(error)}`]);
444
+ }
445
+ await disposeRuntimeProcess(child, {
446
+ disposeEofGraceMs: this.options.disposeEofGraceMs ?? 6e3,
447
+ disposeGraceMs: this.options.disposeGraceMs ?? 3e3
448
+ });
449
+ this.transport?.close();
450
+ this.failSubscriptions(this.closedError("DeepSeek Harness runtime closed"));
451
+ }
452
+ dispatchNotification(notification) {
453
+ this.recordSessionRelationship(notification);
454
+ for (const subscription of this.subscriptions.values()) subscription.push(notification);
455
+ }
456
+ recordSessionRelationship(notification) {
457
+ if (notification.method !== "subagent.started") return;
458
+ const parentId = notification.params.parentSessionId;
459
+ const childId = notification.params.childSessionId;
460
+ if (typeof parentId === "string" && parentId !== "" && typeof childId === "string" && childId !== "" && parentId !== childId) this.sessionParents.set(childId, parentId);
461
+ }
462
+ isDescendantOf(sessionId, rootSessionId) {
463
+ const visited = /* @__PURE__ */ new Set();
464
+ let current = sessionId;
465
+ while (!visited.has(current)) {
466
+ if (current === rootSessionId) return true;
467
+ visited.add(current);
468
+ const parent = this.sessionParents.get(current);
469
+ if (parent === void 0) return false;
470
+ current = parent;
471
+ }
472
+ /* v8 ignore next */
473
+ return false;
474
+ }
475
+ failSubscriptions(error) {
476
+ for (const subscription of this.subscriptions.values()) subscription.fail(error);
477
+ }
478
+ appendStderr(lines) {
479
+ const kept = lines.filter((line) => line.length > 0);
480
+ this.stderrTail.push(...kept);
481
+ if (this.stderrTail.length > STDERR_TAIL_LIMIT) this.stderrTail.splice(0, this.stderrTail.length - STDERR_TAIL_LIMIT);
482
+ }
483
+ settleStreams() {
484
+ return Promise.race([this.streamsSettled, new Promise((resolve) => {
485
+ setTimeout(resolve, STREAM_SETTLE_MS);
486
+ })]);
487
+ }
488
+ closedError(reason) {
489
+ const parts = [reason];
490
+ if (this.spawnError !== void 0) parts.push(`spawn error: ${this.spawnError.message}`);
491
+ if (this.exitCode !== void 0) parts.push(`exit code: ${String(this.exitCode)}`);
492
+ if (this.stderrTail.length > 0) parts.push(`stderr tail:\n${this.stderrTail.join("\n")}`);
493
+ return new TransportClosedError(parts.join("\n"));
494
+ }
495
+ };
496
+ /**
497
+ * Whether `value` is a plain JSON object (the wire-boundary shape probe).
498
+ * @param value - the wire value to probe.
499
+ * @returns `true` iff `value` is a non-null, non-array object.
500
+ */
501
+ function isRecord(value) {
502
+ return typeof value === "object" && value !== null && !Array.isArray(value);
503
+ }
504
+ /** The message of a thrown value (the transport only throws `Error`s; `String` covers the rest). */
505
+ function errorMessage(error) {
506
+ /* v8 ignore next -- the transport and dispose ladder reject only with Errors */
507
+ return error instanceof Error ? error.message : String(error);
508
+ }
509
+ //#endregion
510
+ //#region lib/types/api.js
511
+ /**
512
+ * High-level run API over {@link HarnessClient}: `DeepSeekHarness` owns one
513
+ * runtime subprocess across many sessions; `HarnessSession.run` sends a
514
+ * prompt and settles when the whole agent next becomes idle.
515
+ * Mirrors the Python SDK's `DeepSeekHarness`/`Session` pair.
516
+ *
517
+ * @module @deepseek-ai/dsh-sdk-client/api
518
+ */
519
+ /**
520
+ * Reusable SDK for running DeepSeek Harness agent turns in a runtime
521
+ * subprocess. The subprocess starts lazily on first use and stays owned by
522
+ * this instance until {@link close}; always close (or `await using`) so the
523
+ * child is reaped.
524
+ */
525
+ var DeepSeekHarness = class {
526
+ clientInstance;
527
+ launch;
528
+ cwd;
529
+ provider;
530
+ model;
531
+ maxTokens;
532
+ initialized;
533
+ 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());
539
+ this.provider = options.provider ?? "deepseek-official";
540
+ this.model = options.model ?? "deepseek-v4-flash";
541
+ this.maxTokens = options.maxTokens;
542
+ }
543
+ /**
544
+ * 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}.
547
+ * @returns the client currently owning the runtime subprocess.
548
+ */
549
+ get client() {
550
+ return this.clientInstance;
551
+ }
552
+ /**
553
+ * 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.
557
+ * @returns settlement of the (memoized) handshake.
558
+ */
559
+ start() {
560
+ this.initialized ??= (async () => {
561
+ try {
562
+ this.clientInstance.start();
563
+ await this.clientInstance.initialize({
564
+ cwd: this.cwd,
565
+ provider: this.provider,
566
+ model: this.model,
567
+ ...this.maxTokens === void 0 ? {} : { maxTokens: this.maxTokens }
568
+ });
569
+ } catch (error) {
570
+ this.initialized = void 0;
571
+ await this.clientInstance.close();
572
+ if (!this.closed) this.clientInstance = new HarnessClient(this.launch);
573
+ throw error;
574
+ }
575
+ })();
576
+ return this.initialized;
577
+ }
578
+ /**
579
+ * Open a session handle (no wire traffic; the runtime creates the session
580
+ * on its first prompt).
581
+ * @param sessionId - explicit id to reuse; omitted mints a fresh one.
582
+ * @returns the session handle.
583
+ */
584
+ session(sessionId) {
585
+ return new HarnessSession(this, sessionId ?? `session-${randomUUID().replaceAll("-", "")}`);
586
+ }
587
+ /**
588
+ * Run one prompt on a fresh (or named) session.
589
+ * @param input - prompt text, or content blocks sent verbatim.
590
+ * @param options - optional session id and per-notification observer.
591
+ * @returns the owned activity interval.
592
+ */
593
+ run(input, options) {
594
+ return this.session(options?.sessionId).run(input, options);
595
+ }
596
+ /**
597
+ * Shut down and reap the runtime subprocess. Idempotent and terminal —
598
+ * a closed harness no longer retries a failed handshake.
599
+ * @returns settlement of the complete teardown.
600
+ */
601
+ close() {
602
+ this.closed = true;
603
+ return this.clientInstance.close();
604
+ }
605
+ /**
606
+ * `await using` support: {@link close}.
607
+ * @returns settlement of the teardown.
608
+ */
609
+ [Symbol.asyncDispose]() {
610
+ return this.close();
611
+ }
612
+ };
613
+ /**
614
+ * One SDK session: a stable id plus owned activity intervals.
615
+ */
616
+ var HarnessSession = class {
617
+ harness;
618
+ id;
619
+ /**
620
+ * @param harness - the owning harness (supplies the client and handshake).
621
+ * @param id - the wire session id this handle runs on.
622
+ */
623
+ constructor(harness, id) {
624
+ this.harness = harness;
625
+ this.id = id;
626
+ }
627
+ /**
628
+ * Queue one prompt, then observe the whole session through its next idle.
629
+ * @param input - prompt text, or content blocks sent verbatim.
630
+ * @param options - optional per-notification observer.
631
+ * @returns the owned activity interval; rejects on transport loss, timeout,
632
+ * or a protocol error.
633
+ */
634
+ async run(input, options) {
635
+ await this.harness.start();
636
+ const client = this.harness.client;
637
+ const contentBlocks = normalizeInput(input);
638
+ const events = [];
639
+ const notifications = [];
640
+ const subscription = client.subscribeSessionTree(this.id);
641
+ const collect = (notification) => {
642
+ if (notification.method === "session.event" && notification.params.sessionId === this.id) {
643
+ const event = validatedSessionEvent(notification.params.event);
644
+ notifications.push(notification);
645
+ options?.onNotification?.(notification);
646
+ events.push(event);
647
+ return;
648
+ }
649
+ notifications.push(notification);
650
+ options?.onNotification?.(notification);
651
+ };
652
+ try {
653
+ const messageId = await client.prompt(this.id, contentBlocks);
654
+ let received = false;
655
+ while (true) {
656
+ const notification = await subscription.next();
657
+ if (!received) {
658
+ if (notification.method !== "session.event" || notification.params.sessionId !== this.id || !isInboxReceipt(notification.params.event, messageId)) continue;
659
+ received = true;
660
+ }
661
+ collect(notification);
662
+ if (notification.method === "session.status" && notification.params.sessionId === this.id && notification.params.status === "idle") break;
663
+ }
664
+ } finally {
665
+ subscription.close();
666
+ }
667
+ return {
668
+ sessionId: this.id,
669
+ finalResponse: finalResponse(events),
670
+ events,
671
+ notifications
672
+ };
673
+ }
674
+ };
675
+ /**
676
+ * Normalize run input: a string becomes one text block; blocks pass verbatim.
677
+ * @param input - prompt text or content blocks.
678
+ * @returns the content blocks to send.
679
+ */
680
+ function normalizeInput(input) {
681
+ return typeof input === "string" ? [{
682
+ type: "text",
683
+ text: input
684
+ }] : input;
685
+ }
686
+ /** Validate the fields in a wire `session.event` envelope before returning the typed result. */
687
+ function validatedSessionEvent(value) {
688
+ if (!isRecord(value) || typeof value.type !== "string") throw new SdkProtocolError(`session.event carried no event envelope: ${JSON.stringify(value)}`);
689
+ if (value.type === "assistant/message") {
690
+ const message = isRecord(value.data) ? value.data.message : void 0;
691
+ const content = isRecord(message) ? message.content : void 0;
692
+ 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
+ }
694
+ return value;
695
+ }
696
+ /** Whether a raw session event is the durable enqueue receipt for `messageId`. */
697
+ function isInboxReceipt(value, messageId) {
698
+ if (!isRecord(value) || value.type !== "agent/inbox/spliced" || !isRecord(value.data)) return false;
699
+ const inserted = value.data.inserted;
700
+ return Array.isArray(inserted) && inserted.some((message) => isRecord(message) && message.id === messageId);
701
+ }
702
+ /**
703
+ * Extract the concatenated text of the last assistant message.
704
+ * @param events - the activity interval's `session.event` payloads in wire order.
705
+ * @returns the final response text, or `''` when no assistant message exists.
706
+ */
707
+ function finalResponse(events) {
708
+ for (let index = events.length - 1; index >= 0; index--) {
709
+ const event = events[index];
710
+ if (event?.type !== "assistant/message") continue;
711
+ return event.data.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
712
+ }
713
+ return "";
714
+ }
715
+ //#endregion
716
+ export { DeepSeekHarness, HarnessClient, HarnessSession, JsonRpcResponseError, RequestTimeoutError, SdkProtocolError, TransportClosedError };
@@ -0,0 +1,24 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-sdk-client`.
4
+ * @module @deepseek-ai/dsh-sdk-client/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-sdk-client";
7
+ /** Cordis companion plugin name. */
8
+ const name = "sdk-client-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this client library runs outside any harness context
13
+ * (its peer is a separate runtime process); the runtime's own packages own
14
+ * the event-stream relations.
15
+ */
16
+ const install = () => {};
17
+ /**
18
+ * Register this package's invariant companion.
19
+ * @param ctx - Cordis context carrying the invariant service.
20
+ * @returns the installed registration's disposer after setup succeeds.
21
+ */
22
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
23
+ //#endregion
24
+ export { apply, inject, name };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * High-level run API over {@link HarnessClient}: `DeepSeekHarness` owns one
3
+ * runtime subprocess across many sessions; `HarnessSession.run` sends a
4
+ * prompt and settles when the whole agent next becomes idle.
5
+ * Mirrors the Python SDK's `DeepSeekHarness`/`Session` pair.
6
+ *
7
+ * @module @deepseek-ai/dsh-sdk-client/api
8
+ */
9
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
10
+ import { HarnessClient } from './client.ts';
11
+ import type { ContentBlock, DeepSeekHarnessOptions, HarnessNotification, RunResult } from './types.ts';
12
+ /**
13
+ * Reusable SDK for running DeepSeek Harness agent turns in a runtime
14
+ * subprocess. The subprocess starts lazily on first use and stays owned by
15
+ * this instance until {@link close}; always close (or `await using`) so the
16
+ * child is reaped.
17
+ */
18
+ export declare class DeepSeekHarness implements AsyncDisposable {
19
+ private clientInstance;
20
+ private readonly launch;
21
+ private readonly cwd;
22
+ private readonly provider;
23
+ private readonly model;
24
+ private readonly maxTokens;
25
+ private initialized;
26
+ private closed;
27
+ /** @param options - runtime launch spec plus the session route (cwd/provider/model). */
28
+ constructor(options: DeepSeekHarnessOptions);
29
+ /**
30
+ * 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}.
33
+ * @returns the client currently owning the runtime subprocess.
34
+ */
35
+ get client(): HarnessClient;
36
+ /**
37
+ * 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.
41
+ * @returns settlement of the (memoized) handshake.
42
+ */
43
+ start(): Promise<void>;
44
+ /**
45
+ * Open a session handle (no wire traffic; the runtime creates the session
46
+ * on its first prompt).
47
+ * @param sessionId - explicit id to reuse; omitted mints a fresh one.
48
+ * @returns the session handle.
49
+ */
50
+ session(sessionId?: string): HarnessSession;
51
+ /**
52
+ * Run one prompt on a fresh (or named) session.
53
+ * @param input - prompt text, or content blocks sent verbatim.
54
+ * @param options - optional session id and per-notification observer.
55
+ * @returns the owned activity interval.
56
+ */
57
+ run(input: string | ContentBlock[], options?: RunOptions): Promise<RunResult>;
58
+ /**
59
+ * Shut down and reap the runtime subprocess. Idempotent and terminal —
60
+ * a closed harness no longer retries a failed handshake.
61
+ * @returns settlement of the complete teardown.
62
+ */
63
+ close(): Promise<void>;
64
+ /**
65
+ * `await using` support: {@link close}.
66
+ * @returns settlement of the teardown.
67
+ */
68
+ [Symbol.asyncDispose](): Promise<void>;
69
+ }
70
+ /** Per-run options: target session and streaming observer. */
71
+ export interface RunOptions {
72
+ /** Session id to run on; omitted mints a fresh session per call. */
73
+ sessionId?: string;
74
+ /** Observer invoked with every notification for this session tree, in wire order. */
75
+ onNotification?: (notification: HarnessNotification) => void;
76
+ }
77
+ /**
78
+ * One SDK session: a stable id plus owned activity intervals.
79
+ */
80
+ export declare class HarnessSession {
81
+ readonly harness: DeepSeekHarness;
82
+ readonly id: string;
83
+ /**
84
+ * @param harness - the owning harness (supplies the client and handshake).
85
+ * @param id - the wire session id this handle runs on.
86
+ */
87
+ constructor(harness: DeepSeekHarness, id: string);
88
+ /**
89
+ * Queue one prompt, then observe the whole session through its next idle.
90
+ * @param input - prompt text, or content blocks sent verbatim.
91
+ * @param options - optional per-notification observer.
92
+ * @returns the owned activity interval; rejects on transport loss, timeout,
93
+ * or a protocol error.
94
+ */
95
+ run(input: string | ContentBlock[], options?: Pick<RunOptions, 'onNotification'>): Promise<RunResult>;
96
+ }
97
+ /**
98
+ * Normalize run input: a string becomes one text block; blocks pass verbatim.
99
+ * @param input - prompt text or content blocks.
100
+ * @returns the content blocks to send.
101
+ */
102
+ export declare function normalizeInput(input: string | ContentBlock[]): ContentBlock[];
103
+ /**
104
+ * Extract the concatenated text of the last assistant message.
105
+ * @param events - the activity interval's `session.event` payloads in wire order.
106
+ * @returns the final response text, or `''` when no assistant message exists.
107
+ */
108
+ export declare function finalResponse(events: SessionEvent[]): string;
109
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Low-level JSON-RPC client for a DeepSeek Harness SDK runtime subprocess.
3
+ * {@link HarnessClient} owns the child process: it spawns the runtime, speaks
4
+ * the `@deepseek-ai/dsh-sdk-protocol` wire over the child's stdio, fans
5
+ * server notifications out to subscriptions, and tears the child down to
6
+ * quiescence through a private EOF → SIGTERM → SIGKILL ladder. The design
7
+ * twin is the Python SDK's `HarnessClient` (`python/sdk`); both drive the
8
+ * same runtime protocol. This client runs OUTSIDE any harness context, so it
9
+ * spawns directly rather than through the `dsh-subprocess` service — the
10
+ * seam's documented exception for SDK-managed transports.
11
+ *
12
+ * @module @deepseek-ai/dsh-sdk-client/client
13
+ */
14
+ import { type InitializeParams, type InitializeResult } from '@deepseek-ai/dsh-sdk-protocol';
15
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm';
16
+ import type { HarnessClientOptions, HarnessNotification, NotificationFilter } from './types.ts';
17
+ /**
18
+ * The runtime subprocess is gone or unusable: it exited, its stdio closed, or
19
+ * it was never launchable. The message carries the exit code and a stderr
20
+ * tail when available.
21
+ */
22
+ export declare class TransportClosedError extends Error {
23
+ /** @param message - the failure description, including any stderr tail. */
24
+ constructor(message: string);
25
+ }
26
+ /** A request exceeded {@link HarnessClientOptions.requestTimeoutMs}. */
27
+ export declare class RequestTimeoutError extends Error {
28
+ /** @param message - which method timed out. */
29
+ constructor(message: string);
30
+ }
31
+ /**
32
+ * The runtime answered outside its documented protocol (for example a
33
+ * `session/prompt` response without `accepted: true`).
34
+ */
35
+ export declare class SdkProtocolError extends Error {
36
+ /** @param message - the protocol violation description. */
37
+ constructor(message: string);
38
+ }
39
+ /** One client-side notification stream returned by {@link HarnessClient.subscribe}. */
40
+ export interface NotificationSubscription extends AsyncIterable<HarnessNotification> {
41
+ /**
42
+ * Await the next matching notification.
43
+ * @returns the notification; after the runtime died, drains what was
44
+ * already delivered and then rejects; after {@link close}, rejects
45
+ * immediately (the queue is dropped).
46
+ */
47
+ next(): Promise<HarnessNotification>;
48
+ /**
49
+ * Drain one already-delivered notification without waiting.
50
+ * @returns the next queued notification, or `undefined` when none is queued.
51
+ */
52
+ tryNext(): HarnessNotification | undefined;
53
+ /** Detach from the client; queued items drop and pending waiters reject. */
54
+ close(): void;
55
+ }
56
+ /**
57
+ * JSON-RPC client for the DeepSeek Harness SDK runtime over subprocess stdio.
58
+ *
59
+ * The subprocess starts lazily on {@link start} and is owned by this instance
60
+ * until {@link close}, which requests protocol `shutdown` and then walks the
61
+ * shared EOF → SIGTERM → SIGKILL dispose ladder to quiescence. There is no
62
+ * wire-level cancel: a timed-out request stays running server-side until the
63
+ * runtime is closed.
64
+ */
65
+ export declare class HarnessClient {
66
+ readonly options: HarnessClientOptions;
67
+ private child;
68
+ private transport;
69
+ private readonly stderrTail;
70
+ private readonly subscriptions;
71
+ private readonly sessionParents;
72
+ private subscriptionSerial;
73
+ private exitCode;
74
+ private spawnError;
75
+ private streamsSettled;
76
+ private closeTask;
77
+ /** @param options - launch spec, complete child environment, and timeouts. */
78
+ constructor(options: HarnessClientOptions);
79
+ /**
80
+ * Spawn the runtime subprocess and start reading frames. Idempotent while
81
+ * the process is live; rejects reuse after {@link close}.
82
+ */
83
+ start(): void;
84
+ /**
85
+ * Perform the process-wide handshake.
86
+ * @param params - workspace cwd plus the provider/model route.
87
+ * @returns the runtime's wire identity.
88
+ */
89
+ initialize(params: InitializeParams): Promise<InitializeResult>;
90
+ /**
91
+ * Queue one prompt and return its durable inbox identity.
92
+ * @param sessionId - target session; an unknown id creates it.
93
+ * @param contentBlocks - the user message, sent verbatim.
94
+ * @returns the queued message id.
95
+ */
96
+ prompt(sessionId: string, contentBlocks: ContentBlock[]): Promise<string>;
97
+ /**
98
+ * Send one JSON-RPC request and await its result.
99
+ * @param method - the wire method name.
100
+ * @param params - the params object; omitted params send `{}`.
101
+ * @param timeoutMs - per-call override of {@link HarnessClientOptions.requestTimeoutMs}.
102
+ * @returns the raw result; rejects with {@link JsonRpcResponseError} on a
103
+ * protocol error response, {@link RequestTimeoutError} on timeout, and
104
+ * {@link TransportClosedError} when the runtime is gone.
105
+ */
106
+ request(method: string, params?: object, timeoutMs?: number): Promise<unknown>;
107
+ /**
108
+ * Subscribe to server notifications.
109
+ * @param filter - optional predicate; omitted means every notification.
110
+ * @returns the subscription handle; close it to stop delivery. After
111
+ * {@link close} or runtime death the handle is born failed — there is no
112
+ * producer left, so `next()` rejects instead of waiting forever.
113
+ */
114
+ subscribe(filter?: NotificationFilter): NotificationSubscription;
115
+ /**
116
+ * 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
+ * @param sessionId - the root session id.
120
+ * @returns the filtered subscription handle.
121
+ */
122
+ subscribeSessionTree(sessionId: string): NotificationSubscription;
123
+ /**
124
+ * Shut the runtime down and reap it: a best-effort protocol `shutdown`
125
+ * bounded by `shutdownTimeoutMs`, then the shared stdin-EOF → SIGTERM →
126
+ * SIGKILL ladder until the process actually exited. Idempotent.
127
+ * @returns settlement of the complete teardown.
128
+ */
129
+ close(): Promise<void>;
130
+ private performClose;
131
+ private dispatchNotification;
132
+ private recordSessionRelationship;
133
+ private isDescendantOf;
134
+ private failSubscriptions;
135
+ private appendStderr;
136
+ private settleStreams;
137
+ private closedError;
138
+ }
139
+ /**
140
+ * Whether `value` is a plain JSON object (the wire-boundary shape probe).
141
+ * @param value - the wire value to probe.
142
+ * @returns `true` iff `value` is a non-null, non-array object.
143
+ */
144
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
145
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Private teardown ladder for the runtime subprocess: stdin EOF (cooperative
3
+ * quiesce), then SIGTERM, then SIGKILL, resolving only after the process has
4
+ * actually exited. The SDK client runs OUTSIDE any harness context, so it
5
+ * cannot ride the `dsh-subprocess` service — this module is the seam's
6
+ * documented exception for SDK-managed transports.
7
+ *
8
+ * @module @deepseek-ai/dsh-sdk-client/dispose
9
+ */
10
+ import type { ChildProcess } from 'node:child_process';
11
+ /**
12
+ * Tear the runtime down to quiescence, resolving only after exit: close stdin
13
+ * and allow cooperative flush, then use the host's graceful and forced
14
+ * termination semantics. POSIX sends `SIGTERM` before `SIGKILL`; Windows
15
+ * skips directly to forced termination because Node maps both signals to
16
+ * `TerminateProcess`.
17
+ * @param child - the runtime child process to tear down.
18
+ * @param graces - the EOF and termination-confirmation windows (ms).
19
+ * @param platform - the host platform, injectable for unit coverage.
20
+ * @throws When forced termination errors or the child does not report exit
21
+ * within `disposeGraceMs`.
22
+ */
23
+ export declare function disposeRuntimeProcess(child: ChildProcess, graces: {
24
+ disposeEofGraceMs: number;
25
+ disposeGraceMs: number;
26
+ }, platform?: NodeJS.Platform): Promise<void>;
27
+ //# sourceMappingURL=dispose.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
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;
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`.
8
+ *
9
+ * @module @deepseek-ai/dsh-sdk-client
10
+ */
11
+ export { DeepSeekHarness, HarnessSession } from './api.ts';
12
+ export type { RunOptions } from './api.ts';
13
+ export { HarnessClient, RequestTimeoutError, SdkProtocolError, TransportClosedError, } from './client.ts';
14
+ export type { NotificationSubscription } from './client.ts';
15
+ export { JsonRpcResponseError } from '@deepseek-ai/dsh-sdk-protocol';
16
+ export type { ContentBlock, DeepSeekHarnessOptions, HarnessClientOptions, HarnessNotification, NotificationFilter, RunResult, } from './types.ts';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-sdk-client`.
3
+ * @module @deepseek-ai/dsh-sdk-client/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "sdk-client-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Types for the TypeScript SDK client: launch options, notification shapes,
3
+ * and owned activity results.
4
+ *
5
+ * @module @deepseek-ai/dsh-sdk-client/types
6
+ */
7
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm';
8
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
9
+ /** One server-to-client notification as received off the wire. */
10
+ export interface HarnessNotification {
11
+ /** The JSON-RPC notification method name. */
12
+ method: string;
13
+ /** The raw params object; see `HarnessSdkNotificationMap` for the shapes per method. */
14
+ params: Record<string, unknown>;
15
+ }
16
+ /** Predicate deciding whether a subscription receives a notification. */
17
+ export type NotificationFilter = (notification: HarnessNotification) => boolean;
18
+ /** Launch and timeout options for {@link HarnessClient}. */
19
+ 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;
26
+ /**
27
+ * The complete child environment. `undefined` inherits the parent env
28
+ * verbatim; passing an object replaces it entirely, so callers own
29
+ * credential policy (see `scrubbedParentEnv` in `@deepseek-ai/dsh-subprocess`
30
+ * for the shared scrub-then-merge base).
31
+ */
32
+ env?: NodeJS.ProcessEnv;
33
+ /** Per-request timeout (ms); `undefined` waits indefinitely (a turn can legitimately run long). */
34
+ requestTimeoutMs?: number;
35
+ /** Bound (ms) on the protocol `shutdown` exchange inside `close()` (default 1000). */
36
+ shutdownTimeoutMs?: number;
37
+ /** Grace (ms) for the runtime's stdin-EOF quiesce during `close()` (default 6000). */
38
+ disposeEofGraceMs?: number;
39
+ /** Termination confirmation window (ms) after SIGTERM/SIGKILL during `close()` (default 3000). */
40
+ disposeGraceMs?: number;
41
+ }
42
+ /** 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()`). */
47
+ cwd?: string;
48
+ /** Provider route for SDK-created agents (default `deepseek-official`). */
49
+ provider?: string;
50
+ /** Model for SDK-created agents (default `deepseek-v4-flash`). */
51
+ model?: string;
52
+ /** Maximum output tokens for each conversation-model request. */
53
+ maxTokens?: number;
54
+ }
55
+ /** One owned session activity interval, from enqueue receipt through idle. */
56
+ export interface RunResult {
57
+ /** The session the activity ran on. */
58
+ sessionId: string;
59
+ /** Concatenated text of the interval's last assistant message (empty when none). */
60
+ finalResponse: string;
61
+ /** Every `session.event` payload for the root session, in wire order. */
62
+ events: SessionEvent[];
63
+ /** Every notification for the root session and discovered descendants, in wire order. */
64
+ notifications: HarnessNotification[];
65
+ }
66
+ /** Re-exported content-block alias so SDK callers need no extra import. */
67
+ export type { ContentBlock };
68
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-sdk-client",
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.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/scaffold/client"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/invariant.js",
30
+ "lib/types/**/*.d.ts"
31
+ ],
32
+ "license": "BSD-3-Clause",
33
+ "peerDependencies": {
34
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
35
+ "@deepseek-ai/dsh-sdk-protocol": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
37
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
38
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1"
39
+ },
40
+ "devDependencies": {
41
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
42
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
43
+ "@deepseek-ai/dsh-sdk-protocol": "^0.0.1-rc.1",
44
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
45
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
46
+ }
47
+ }