The group's convention is package suffix == provider default (subagent-acp/'acp', subagent-spawn/'spawn', subagent-fork/'fork'), and the provider default became dsh-sdk in the last review round — so the package follows: @deepseek-ai/dsh-subagent-dsh-sdk at packages/subagent/subagent-dsh-sdk, plugin name subagent-dsh-sdk, diagnostics prefixed subagent-dsh-sdk:. The dsh echo has precedent (dsh-llm-deepseek). Directory, fixture path, knip/tsconfig/examples registrations, catalogs, READMEs (en+zh), and the Agent Note follow; the sdk-client dispose ladder moves to its own module (src/dispose.ts) with the deterministic FakeChild tier tests restored alongside it.
@deepseek-ai/dsh-sdk-client
English | 中文
The TypeScript client SDK for driving a DeepSeek Harness runtime as a subprocess over stdio JSON-RPC — the design twin of the Python SDK (deepseek-harness), sharing the same runtime peer, protocol, and layering: DeepSeekHarness is the high-level turns API, HarnessClient the lower-level protocol client. A pure library: it registers nothing on a Cordis context; the runtime process it spawns is a complete harness whose composition its own cordis.yml decides.
Unlike the Python SDK, the launch spec is fully explicit (command/args): this package is for repo-adjacent TypeScript consumers — the dsh-subagent-dsh-sdk backend, tests, automation — which know which runtime they are launching. Bundled-runtime resolution (finding a packaged executable) remains the Python distribution's concern.
DeepSeekHarness
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
await using harness = new DeepSeekHarness({
launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
provider: 'deepseek',
model: 'deepseek-v4-flash',
})
const result = await harness.run('say hi')
console.log(result.status, result.finalResponse)
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); 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). session(id?) opens a named or fresh session handle; run(input, { sessionId?, onNotification? }) sends one prompt turn and settles when the paired session.finished arrives, returning a TurnResult: status (ok/error as the deployment maps it), the structured reason (TurnEndReason), finalResponse (last assistant message text), plus every session.event envelope and raw notification observed for that session tree, in wire order. Model-level failure is a status: 'error' result, never a rejection; rejections mean transport loss, timeout, or protocol violation.
HarnessClient
The protocol client under the turns API: explicit start()/initialize()/prompt()/request()/close(), plus notification subscriptions. subscribe(filter?) returns a NotificationSubscription (awaitable next(), non-blocking tryNext(), async iteration); subscribeSessionTree(id) scopes to one session and the descendants discovered from subagent.started lineage edges — the runtime notifies for every session in its context, and scoping is client-side, exactly like the Python SDK. Error surfaces are typed: JsonRpcResponseError (wire error response, code/data preserved), RequestTimeoutError (a configured bound elapsed; there is no wire-level cancel, so the request keeps running server-side until close), SdkProtocolError (a response outside the documented protocol), TransportClosedError (the runtime is gone — message carries the exit code and a bounded stderr tail).
close() requests protocol shutdown (bounded by shutdownTimeoutMs, default 1000 ms), then walks 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 service — the seam's documented exception for SDK-managed transports. It is idempotent, and a closed client refuses reuse.
HarnessClientOptions.env replaces the child environment entirely when given (undefined inherits the parent's); callers own credential policy — scrubbedParentEnv from dsh-subprocess is the shared scrub base for isolation-minded launches.
Testing
Keyless unit tests drive a scripted fake runtime subprocess (tests/fake-runtime.ts, protocol-only, env-scripted) over real stdio: turn loop, session-tree scoping, timeout/death/malformed-response surfaces, and the dispose ladder. The SDK snapshot suite drives the real dsh-jsonrpc-agent runtime through this client keylessly via llm-replay, pinning the notification stream, the turn result, and the persisted logs; DSH_SNAPSHOT=record re-records against the live API.
Model Experience
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.
KV Cache effect
None; this package neither assembles nor sends a provider request.
Known Limitations and Deferred Work
- No bundled-runtime resolution — callers name the runtime executable explicitly; packaged-executable discovery stays Python-side until a TypeScript distribution consumer exists.
- No mid-turn cancel — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the protocol's Known Limitations).
- One in-flight prompt per session — a server-side rule this client surfaces as a
JsonRpcResponseError; independent sessions run concurrently on one runtime. - Client→server notifications and server→client requests are unimplemented on both wire ends; the transport carries them for future approval flows.