The client SDK is what a consumer process spawns and talks through. It wraps the wire protocol so a caller does not manipulate JSON-RPC frames by hand: you create a DeepSeekHarness, call run('…'), and get a RunResult with the final answer and the event stream that produced it. It is the design twin of the Python SDK (deepseek_harness), sharing the same runtime peer, protocol, and layering. This page cites packages/sdk/client/src.
Package and version
@deepseek-ai/dsh-sdk-client is a pure library: it registers nothing on a Cordis context. The subprocess it spawns is a complete harness whose composition its own cordis.yml decides.
| Field | Value |
|---|---|
| name | @deepseek-ai/dsh-sdk-client |
| role | TypeScript client SDK — drives a Harness runtime subprocess |
| proto peer | @deepseek-ai/dsh-sdk-protocol |
| peer deps | dsh-invariants, dsh-llm, dsh-sdk-protocol, dsh-session, cordis |
Two layers
The package root (src/index.ts) exposes a deliberately small surface:
| Layer | Symbol | Job |
|---|---|---|
| High-level run API | DeepSeekHarness, HarnessSession | own a runtime process; queue a prompt; collect through idle |
| Low-level protocol client | HarnessClient | explicit start/initialize/prompt/request/close + notification subscriptions |
| Errors | JsonRpcResponseError, RequestTimeoutError, SdkProtocolError, TransportClosedError | typed failures off the wire |
Normalization helpers (normalizeInput, finalResponse, isRecord, validatedSessionEvent) and the subscription-delivery machinery are internal, not consumer imports.
Minimal usage (the real API)
From DeepSeekHarness.test-style usage in src/api.ts, a minimal run is:
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
await using harness = new DeepSeekHarness({
launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
maxTokens: 49_152,
})
const result = await harness.run('say hi')
console.log(result.finalResponse)Notes on the launch spec: launch.command/launch.args are fully explicit (this package is for repo-adjacent TypeScript consumers that know which runtime they are launching). Bundled-runtime resolution — finding a packaged executable — remains the Python distribution's concern. cwd, provider, model, and maxTokens are the session route: cwd defaults to the launch cwd then process.cwd(), provider to deepseek-official, model to deepseek-v4-flash.
DeepSeekHarness: the owned-run API
DeepSeekHarness (src/api.ts) is an AsyncDisposable that owns one runtime subprocess across many sessions.
- Lazy start.
start()memoizes theinitializehandshake and is called on first use. On failure it reaps the runtime (HarnessClient.close) and swaps in a fresh client, so a later call retries with a new subprocess — untilclose()makes the instance terminal. session(sessionId?)opens a named or fresh session handle (session-<uuid>). This performs no wire traffic; the runtime creates the session on its first prompt.run(input, { sessionId?, onNotification? })forwards tothis.session(sessionId).run(...).close()setsclosed = trueand tears the client down;await usingcalls it automatically.get client(): HarnessClientexposes the low-level client.
The client getter is the one caching caveat: after a failed handshake the instance is replaced, so never cache it across a failed start().
HarnessSession.run
run owns one activity interval on one session:
queue the prompt → wait until that
MessageIdappears in a durableagent/inbox/splicedreceipt → collect every notification until the next whole-agentidle.
async run(input: string | ContentBlock[], options?): Promise<RunResult> {
await this.harness.start()
const client = this.harness.client
const contentBlocks = normalizeInput(input) // string -> [{type:'text',text}]
// subscribeSessionTree scopes to this session plus descendants
const subscription = client.subscribeSessionTree(this.id)
const messageId = await client.prompt(this.id, contentBlocks)
// ... wait for the inbox receipt, then collect until session.status === 'idle'
}The returned RunResult (src/types.ts):
export interface RunResult {
sessionId: string
finalResponse: string // concatenated text of the LAST assistant/message in the interval
events: SessionEvent[] // root-session session.event payloads, wire order
notifications: HarnessNotification[] // root + descendants (from subagent.started), wire order
}Important semantics: finalResponse is the last committed root-session assistant text in the interval, not a response causally assigned to the prompt — steering, injected context, and other queued work may contribute before idle. events holds root-session events; notifications also contains descendants discovered from subagent.started. The result carries no prompt-level status or turn reason.
HarnessClient: the protocol client
HarnessClient (src/client.ts) owns the child process directly — it runs outside any harness context, so it spawns through node:child_process rather than the dsh-subprocess service (the seam's documented exception for SDK-managed transports).
export class HarnessClient {
start(): void // spawn + begin reading frames
async initialize(params: InitializeParams) // process-wide handshake
async prompt(sessionId, contentBlocks): Promise<string> // returns messageId
async request(method, params?, timeoutMs?): Promise<unknown>
subscribe(filter?): NotificationSubscription
subscribeSessionTree(sessionId): NotificationSubscription
close(): Promise<void> // bounded shutdown + dispose ladder
}prompt() returns the queued message id as soon as the runtime accepts it; it never waits for agent activity.
Async event handling
subscribe(filter?) returns a NotificationSubscription (NotificationSubscriptionImpl is the internal producer):
export interface NotificationSubscription extends AsyncIterable<HarnessNotification> {
next(): Promise<HarnessNotification> // await the next matching notification
tryNext(): HarnessNotification | undefined // drain one already-delivered, without waiting
close(): void
}Because it is AsyncIterable, you can for await (const n of subscription) until the subscription or runtime closes. A throwing filter fails only that subscription (detached, the throw becomes its terminal error); it never disturbs siblings or the transport read loop.
subscribeSessionTree(id) scopes to one session plus every descendant discovered from subagent.started lineage edges. The runtime notifies for every session in its context; scoping is client-side, exactly like the Python SDK. The client keeps a sessionParents map (child -> parent) built from subagent.started, and an isDescendantOf walk resolves membership.
Error handling
The client normalizes all wire, transport, and timeout failure into typed errors.
| Error | When |
|---|---|
JsonRpcResponseError | peer responded with a JSON-RPC error; preserves wire code and data |
RequestTimeoutError | a configured per-request bound elapsed ({method} timed out after …) |
SdkProtocolError | response outside the documented protocol (e.g. session/prompt with no messageId) |
TransportClosedError | runtime is gone — message carries the exit code and a bounded (400-line) stderr tail |
There is no wire-level cancel: a timed-out request stays running server-side until the runtime is closed. The timeout uses an AbortController whose abort drops the transport's pending entry, so repeated bounded requests against a hung method retain no per-call state.
Shutdown and the dispose ladder
close() requests protocol shutdown (bounded by shutdownTimeoutMs, default 1000 ms), then walks a stdin-EOF → SIGTERM → SIGKILL ladder until the process has actually exited: disposeEofGraceMs (default 6000) after EOF, disposeGraceMs (default 3000) after SIGTERM on POSIX before SIGKILL. The ladder is private to this client — it runs outside any harness context, so it cannot ride the dsh-subprocess service. It is idempotent, and a closed client refuses reuse.
export interface HarnessClientOptions {
command: string // runtime executable (dsh-jsonrpc-agent, packaged exe, or node)
args?: string[]
cwd?: string
env?: NodeJS.ProcessEnv // REPLACES child env entirely when given
requestTimeoutMs?: number // undefined = wait indefinitely
shutdownTimeoutMs?: number // default 1000
disposeEofGraceMs?: number // default 6000
disposeGraceMs?: number // default 3000
}env replaces the environment when given and inherits the parent's when undefined; callers own credential policy — scrubbedParentEnv from dsh-subprocess is the shared scrub base for isolation-minded launches.
Relationship to the SDK ecosystem
The client is the vehicle the subagent-dsh-sdk (see Subagents) backend uses to run each subagent as a full runtime in a fresh subprocess — the second out-of-process subagent backend beside subagent-acp. That provider spawns through DeepSeekHarness, completes the initialize handshake, then reads the child's answer from its session events. The wire and the layering are shared 1:1 with the Python SDK; only the launch spec differs (explicit command/args here, bundled-runtime resolution in Python).
Known limitations
| Limitation | Implication |
|---|---|
| No bundled-runtime resolution | caller names the runtime executable explicitly; packaged-executable discovery is Python-sided |
| No mid-turn cancel | the wire has no prompt-cancel method; abandoning a turn means closing the runtime |
| No per-prompt result | low-level prompt() returns an enqueue receipt; high-level run() owns receipt→idle collection |
| Client→server notifications and server→client requests | unimplemented on both wire ends; the transport carries them for future approval flows |
| No model-facing surface | the client contributes no prompt/tool/session event; the model runs in the spawned runtime |
Package version table
| Package | name | version |
|---|---|---|
| SDK client | @deepseek-ai/dsh-sdk-client | |
| SDK wire protocol (peer) | @deepseek-ai/dsh-sdk-protocol | |
| Python mirror | deepseek-harness-sdk (PyPI) | same train as runtime bin |
Further reading
- SDK Protocol — the framing and named types this client drives.
- SDK Server — the runtime-side plugin that answers
initialize/session/prompt/shutdown. - The subagent backend that consumes the client:
packages/subagent/subagent-dsh-sdk/README.md. - The distribution's working composition:
examples/jsonrpc-agent/cordis.yml. - Repo-relative:
packages/sdk/client/src/api.ts,packages/sdk/client/src/client.ts,packages/sdk/client/src/types.ts.