- examples/jsonrpc-agent gains its first snapshot suite (sdk.snapshot.ts): the real dsh-jsonrpc-agent runtime driven through the real dsh-sdk-client, keyless llm-replay behind a new cordis.snapshot.yml overlay; three recorded scenarios (text turn, bash tool, spawn subagent) pin the notification stream, the SDK turn result, and the persisted parent+child session logs. - Bilingual READMEs for dsh-sdk-protocol / dsh-sdk-client / dsh-subagent-sdk; sdk/ and subagent/ group tables extended; dsh-jsonrpc README points at the extracted protocol package; Agent Note (en+zh) owns the decision. - The proposed make-jsonrpc-directional note is updated for the transport's new home and its second (client) consumer. - Model Experience sentence allowlist entries for the two client-side packages; module graph + config catalog regenerated; i18n pairings recorded. doc-sync passes 24/24.
@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-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 (cwd + provider/model route); a failed handshake closes the runtime and resets, so a later call may retry. 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 the shared stdin-EOF → SIGTERM → SIGKILL dispose ladder (disposeEofGraceMs default 6000, disposeGraceMs default 3000) until the process has actually 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 — buildChildEnv from dsh-subagent-subprocess is the scrub-then-inject helper 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.