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 profile (default sdk) decides — launch resolves a same-version dsh executable and runs --profile <profile> (plus any patches).
| 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({
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
maxTokens: 49_152,
})
const result = await harness.run('say hi')
console.log(result.finalResponse)Launch options (packages/sdk/client/src/types.ts): dshBin? (absolute or caller-relative dsh CLI module; omitted resolves this package's same-version @deepseek-ai/dsh dependency), profile? (named profile serving the SDK protocol, default 'sdk'), patches? (ordered per-launch profile patches), dshHome? (explicit harness home; relative paths resolve before spawn), processCwd? (working directory for the dsh process itself), and the timeout knobs initializeTimeoutMs? (default 10 s), requestTimeoutMs?, shutdownTimeoutMs?, disposeEofGraceMs? (default 6000), disposeGraceMs? (default 3000). cwd, provider, model, and maxTokens are the session route: cwd defaults to the process 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 {
dshBin?: string // omitted: resolves this package's same-version dsh dependency
profile?: string // default 'sdk'
patches?: string[] // ordered per-launch profile patches
dshHome?: string // explicit Harness home; relative paths resolve before spawn
processCwd?: string // working directory for the dsh process itself
env?: NodeJS.ProcessEnv // REPLACES child env entirely when given
initializeTimeoutMs?: number // default 10000 (bound on the initial profile handshake)
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.
Runtime resolution
There is no caller-names-the-executable requirement any more: when dshBin is omitted, the client auto-resolves the same-version @deepseek-ai/dsh bin and version-checks it against its own manifest — resolveDshBinFromManifests (packages/sdk/client/src/launch.ts) throws unless the dsh version equals the client's, then returns the absolute executable path. resolveDshLaunch builds the argv as <node> <dsh bin> --profile <profile> [--patch <path>…], feeding an explicit dshHome through DSH_HOME. A caller may still pin a specific runtime with dshBin / processCwd / dshHome.
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; the Python side runs the same profile-driven launch (via its own bundled-runtime resolution).
Known limitations
| Limitation | Implication |
|---|---|
| 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 |
| Same-version runtime required | auto-resolved @deepseek-ai/dsh is version-checked against the client; mixing revisions is out of contract |
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 compositions:
packages/bundle/sdk-app(dsh --profile sdk) andbundle/sdk-minimal. - Repo-relative:
packages/sdk/client/src/api.ts,packages/sdk/client/src/client.ts,packages/sdk/client/src/types.ts,packages/sdk/client/src/launch.ts.