The server is the runtime-side half of the SDK. It is a Cordis plugin, named sdk-jsonrpc-server, implemented by @deepseek-ai/dsh-sdk-jsonrpc-server. It serves the wire protocol over stdio so out-of-process SDK clients (TypeScript and Python) can drive harness agents headlessly — no terminal UI, no approval surface, no listening port. This page cites packages/sdk/server/src and the surrounding composition.
Package and version
| Field | Value |
|---|---|
| name | @deepseek-ai/dsh-sdk-jsonrpc-server |
| role | Stdio JSON-RPC server plugin for out-of-process SDK clients |
| proto peer | @deepseek-ai/dsh-sdk-protocol |
| dependency | @deepseek-ai/schemastery (Config typing) |
| peer deps | dsh-agent, dsh-llm, dsh-llm-deepseek, dsh-scope, dsh-session, dsh-subagent, dsh-sdk-protocol, dsh-invariants, plus cordis |
The server re-exports HarnessSdkJsonRpcServer from ./server.ts, and src/index.ts declares the plugin (name, inject, Config, apply). Named exports only — no default export — so the Loader's unwrapExports preserves the plugin shape.
The plugin contract
// packages/sdk/server/src/index.ts (abridged)
export const name = 'sdk-jsonrpc-server'
export const inject = ['agents'] // only the Agent factory is required
export interface JsonRpcConfig {
maxTokensAsSuccess?: boolean // report max-token termination as 'ok'
input?: Readable // runtime-only hook; production = process.stdin
output?: Writable // runtime-only hook; production = process.stdout
exit?: (code: number) => void // runtime-only hook; production = process.exit
}
export const Config: Schema<JsonRpcConfig> = Schema.object({
maxTokensAsSuccess: Schema.boolean().default(false),
})input/output/exit are runtime-only transport hooks for tests; production uses process stdio and process.exit. Only maxTokensAsSuccess is settable from cordis.yml.
The surrounding cordis.yml decides whether the plugin is loaded at all. examples/jsonrpc-agent/cordis.yml is the reference deployment: it stacks the server next to the DeepSeek adapter, a bash executor, the agent spine, JSONL persistence, compaction, and an in-process subagent spawn provider.
Stdout IS the protocol
Stdout carries only JSON-RPC frames. The deployment must not compose a stdout logger; diagnostics belong on stderr. This is enforced by composition, not by inspection — the plugin does not veto sibling loggers, so a surrounding config that loads a stdout logger will corrupt the channel.
Server lifecycle
apply wires one JsonRpcLineTransport over the chosen streams and constructs the server:
// packages/sdk/server/src/index.ts (abridged)
const transport = new JsonRpcLineTransport(input, output)
const server = new HarnessSdkJsonRpcServer(ctx, transport, {
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
})
transport.onRequest(async (method, params) => {
const result = await server.handleRequest(method, params)
if (method === 'shutdown') setImmediate(() => { void disposeAndExit() })
return result
})
ctx.effect(() => {
transport.start()
return async () => { await server.shutdown(); transport.close() }
}, 'jsonrpc.serve')Shutdown semantics are precise:
- The
shutdownrequest's handler runs, thensetImmediateschedulesdisposeAndExit, which flushes the response, disposes the root context (ctx.root.fiber) so SDK-owned agents, subscriptions, and persistence reach quiescence, then exits with code0. Protocol shutdown therefore owns the whole runtime process. - EOF and signal exits belong to the app bin — the loader runner (
dsh-jsonrpc-agentinpackages/examples/jsonrpc-demo/src/runner.ts) handlesstdin 'end',SIGTERM,SIGINTby disposing the root fiber and exiting. - Unloading only this plugin (e.g. via HMR) stops serving without exiting the process — the
ctx.effectdisposal path runsserver.shutdown()thentransport.close().
A shared exitTask ensures racing shutdown requests cannot dispose the root or exit more than once.
HarnessSdkJsonRpcServer
The class (src/server.ts) owns the protocol methods, session registry, and event re-projection. Construction subscribes to session/agent/subagent lifecycle events until shutdown; reinitialization is unsupported.
export class HarnessSdkJsonRpcServer {
async initialize(params: InitializeParams): Promise<InitializeResult>
async prompt(params: SessionPromptParams): Promise<SessionPromptResult>
shutdown(): Promise<Record<string, never>>
async handleRequest(method, params): Promise<unknown> // dispatch switch
// private: getOrCreateSession / createSession / hasAdapterFor
}handleRequest is the JSON-RPC dispatch switch: initialize, session/prompt, shutdown, else unknown DeepSeek Harness SDK runtime method.
Model + agent-loop wiring
Each SDK session maps to one live agent registered on ctx.agents. getOrCreateSession is a memoizing factory that dedupes in-flight creations:
// packages/sdk/server/src/server.ts (abridged)
private async createSession(sessionId: string): Promise<SessionRecord> {
const handle = await this.ctx.agents.create({
sessionId: SessionId(sessionId),
meta: { cwd: this.cwd },
agentOptions: {
provider: this.provider,
model: this.model,
...(this.maxTokens === undefined ? {} : { maxTokens: this.maxTokens }),
},
})
// record the handle in this.sessions
}initialize picks the provider/model route and handles the adapter seam:
- If a registered adapter already serves the requested provider, it is reused.
- An unowned
deepseek-officialroute mountsdsh-llm-deepseekas anllmFiber. - Any other unowned provider fails initialization.
The agent factory is the only hard inject; the optional LLM seam is read via ctx.get('llm') (hasAdapterFor lists providers). The composition keeps the model-facing rows in the host plane, so SDK-created agents read them from the global layer rather than a preset roster (per the agent-presets README note about composing a child agent).
prompt validates the live registry before delivery (an agent-loop-only reload can dispose an agent while this record survives, in which case the SDK agent no longer matches ctx.agents.get(...)), then enqueues:
const message = createUserMessage({ content: params.contentBlocks, source: { kind: 'user' } })
rec.handle.agent.followup(message)
return { messageId: message.id }This is the "queue one identified user message, return immediately" receipt. followup wakes the agent loop; the response has no per-prompt result.
Event re-projection
The constructor registers four ctx.on subscriptions that fan out to notifications:
| Cordis event | Notification |
|---|---|
session/event | session.event (every session in the runtime, unfiltered) |
agent/status | session.status (whole-agent running/idle) |
session/created with a parentSession | subagent.started |
subagent/end when info.local | subagent.finished |
The locality rule for subagent.finished is subtle: the service snapshots the run's local flag (via subagentParentOf recovering the delegating parent from the scoped carrier Scoped<SubagentRuntime> with carrierKeyOf), and the notification is only emitted for in-process child sessions. Provider names, child ids, and durable lineage never establish locality by themselves.
Config keys
Key (cordis.yml, config:) | Default | Meaning |
|---|---|---|
maxTokensAsSuccess | false | Report max-tokens termination as status: 'ok' on subagent.finished; root-session prompts have no prompt-level status |
initialize.maxTokens (a wire param, not a config key) is an optional positive output-token cap inherited by SDK-created agents and their in-process descendants; invalid values reject initialization, and omission sends no SDK cap so the adapter or provider route default applies.
One-shot vs persistent: dsh-headless cross-reference
The SDK server is one of two "headless server-side" patterns in the repo. Contrast:
| Aspect | dsh-sdk-jsonrpc-server (persistent) | dsh-headless (@deepseek-ai/dsh-headless) |
|---|---|---|
| Transport | newline-delimited JSON-RPC over stdio | none — direct in-process Agent drive |
| Interaction model | many session/prompt turns, sessions, subscribe | exactly one task, one fresh Agent, exit |
| Outcome | no per-prompt result; events stream to the client | write last assistant text to stdout, exit 0/1 |
| Composition | mounted by an external cordis.yml | cordis.patch.yml over dsh-base, no Host/HTTP |
| Exit | shutdown request, EOF, or signal (bin-owned) | launcher-provided ctx.appExit hook |
dsh-headless (packages/bundle/headless/src) creates one Agent through the core registry, drives the task to quiescence, flushes its Session, prints the final assistant text, and exits through the launcher-provided appExit host hook — never opening a listening port. It is the "one-shot" sibling; the SDK server is the "persistent, protocol-driven" sibling. apps/cli (@deepseek-ai/dsh) depends on dsh-headless for the dsh --profile headless "<task>" one-shot mode.
Known limitations
| Limitation | Consequence |
|---|---|
| No per-session close or prompt-cancel on the wire | SDK-created agents stay live until process shutdown |
| No per-prompt result | messageId identifies inbox admission only; clients owning an automation interval define and observe that interval themselves |
| stdout purity is deployment-enforced | a surrounding config can still load a stdout logger and corrupt the channel |
| Automatic adapter mounting is DeepSeek-specific | initialize reuses any pre-registered adapter, but its only fallback mounts dsh-llm-deepseek |
| In-process subagents only | subagent.finished reports local child runs; remote runs are not reported |
Package version table
| Package | name | version |
|---|---|---|
| SDK server | @deepseek-ai/dsh-sdk-jsonrpc-server | |
| SDK wire protocol (peer) | @deepseek-ai/dsh-sdk-protocol | |
| Example bin | @deepseek-ai/dsh-sdk-jsonrpc-demo (dsh-jsonrpc-agent) | |
| One-shot bundle | @deepseek-ai/dsh-headless | |
| CLI | @deepseek-ai/dsh | |
The reference composition examples/jsonrpc-agent/package.json is a private example package (jsonrpc-agent-example) — not a published SDK artifact. |
Further reading
- SDK Protocol — the framing, methods, and types this plugin serves.
- SDK Client — the other end of the wire.
- SDK: The Wire Protocol → shutdown semantics and the bin lifecycle at
packages/examples/jsonrpc-demo/src/runner.ts. - One-shot contrast:
@deepseek-ai/dsh-headlessatpackages/bundle/headless/src/startup.ts. - Runtime, agent, and session concepts: Runtime & Agent Lifecycle and Session Management.
- Repo-relative:
packages/sdk/server/src/server.ts,packages/sdk/server/src/index.ts,examples/jsonrpc-agent/cordis.yml.