Skip to content

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).

FieldValue
name@deepseek-ai/dsh-sdk-client
roleTypeScript client SDK — drives a Harness runtime subprocess
proto peer@deepseek-ai/dsh-sdk-protocol
peer depsdsh-invariants, dsh-llm, dsh-sdk-protocol, dsh-session, cordis

Two layers ​

The package root (src/index.ts) exposes a deliberately small surface:

LayerSymbolJob
High-level run APIDeepSeekHarness, HarnessSessionown a runtime process; queue a prompt; collect through idle
Low-level protocol clientHarnessClientexplicit start/initialize/prompt/request/close + notification subscriptions
ErrorsJsonRpcResponseError, RequestTimeoutError, SdkProtocolError, TransportClosedErrortyped 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:

ts
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 the initialize handshake 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 — until close() 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 to this.session(sessionId).run(...).
  • close() sets closed = true and tears the client down; await using calls it automatically.
  • get client(): HarnessClient exposes 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 MessageId appears in a durable agent/inbox/spliced receipt → collect every notification until the next whole-agent idle.

ts
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):

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).

ts
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):

ts
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.

ErrorWhen
JsonRpcResponseErrorpeer responded with a JSON-RPC error; preserves wire code and data
RequestTimeoutErrora configured per-request bound elapsed ({method} timed out after …)
SdkProtocolErrorresponse outside the documented protocol (e.g. session/prompt with no messageId)
TransportClosedErrorruntime 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.

ts
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 ​

LimitationImplication
No mid-turn cancelthe wire has no prompt-cancel method; abandoning a turn means closing the runtime
No per-prompt resultlow-level prompt() returns an enqueue receipt; high-level run() owns receipt→idle collection
Client→server notifications and server→client requestsunimplemented on both wire ends; the transport carries them for future approval flows
No model-facing surfacethe client contributes no prompt/tool/session event; the model runs in the spawned runtime
Same-version runtime requiredauto-resolved @deepseek-ai/dsh is version-checked against the client; mixing revisions is out of contract

Package version table ​

Packagenameversion
SDK client@deepseek-ai/dsh-sdk-client
SDK wire protocol (peer)@deepseek-ai/dsh-sdk-protocol
Python mirrordeepseek-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) and bundle/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.