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 own cordis.yml decides.

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({
  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 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 {
  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

LimitationImplication
No bundled-runtime resolutioncaller names the runtime executable explicitly; packaged-executable discovery is Python-sided
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

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