Keep DeepSeekHarness.run() reusable, but make ownership of its lazy runtime process explicit. Document the context-manager/close contract and update every construction example to use a context manager so repeated runs remain valid without encouraging leaked subprocesses. Contain notification predicate failures at the subscription boundary. Remove only the subscriber whose callback raised, deliver that exception through its queue, and continue dispatching to healthy subscribers so arbitrary callback code cannot terminate the shared reader thread or strand later requests. Enforce one in-flight prompt per server session with an atomic activePrompt guard. Route overlap through the existing -32603 handler-error response and clear the guard in finally, preserving parallel prompts across sessions and sequential reuse without changing JSON-RPC request or notification shapes. Use StringDecoder for line framing so a UTF-8 code point split across Buffer chunks is not corrupted. Add a queued-write flush barrier, and make memoized shutdown await it before disposal and exit while retaining exactly-once cleanup when shutdown calls race or flushing fails. Cover callback isolation, same-session exclusion, cross-session concurrency, split multibyte input, delayed writes, racing shutdown, and flush failure with deterministic tests.
3.5 KiB
@deepseek-ai/dsh-jsonrpc
The SDK server plugin (jsonrpc): mounting it serves a stdio JSON-RPC server that lets an out-of-process SDK client (e.g. the Python deepseek_harness package) drive DeepSeek Harness agents without touching Cordis. The client speaks newline-delimited JSON-RPC on the process stdin/stdout (HarnessSdkServer: initialize → session/prompt → shutdown, with session.event / session.finished / subagent.* notifications over JsonRpcLineTransport). The SDK-client analogue of the acp bridge, split the same way: this package is the protocol plugin, jsonrpc-agent is the app bin that boots a cordis.yml around it — which process serves this protocol is a config decision, not a hardcoded bin. This plugin is the serving face of the single-exe distribution plan — see docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
Wiring
inject: ['agents'] — the server creates one agent per SDK sessionId (get-or-create on session/prompt) and demuxes subagent/end through the registry. The LLM seam is read opportunistically via ctx.get('llm') (not injected): when initialize.model has no registered adapter, the plugin mounts dsh-llm-deepseek for it (credentials from $DEEPSEEK_API_KEY / $DEEPSEEK_BASE_URL); a config-registered adapter for the model wins. Everything else — persistence, the tool stacks, the adapter set — comes from the surrounding cordis.yml.
Config
No cordis.yml-settable keys. The JsonRpcConfig fields (input, output, exit) are runtime-only test seams so a spec can drive the server over in-memory streams without a subprocess or a killed test process; production always serves the process stdio and exits via process.exit.
stdout is the protocol
The process stdout this plugin runs in carries only JSON-RPC frames. The tree that loads it must load NO stdout logger (a console logger corrupts the frames) — the guarantee is config-only, same as the ACP bridge. Diagnostics go to stderr.
Shutdown and exit semantics
The plugin owns the PROTOCOL-level exit: a shutdown request is answered first (the response frame flushes), then the plugin disposes its own fiber — running the effect disposer: an idempotent server.shutdown() (every SDK-created agent disposed to quiescence, event subscriptions detached) plus transport.close() — and exits the process with code 0. Own-fiber disposal is deliberate: the request's server.shutdown() already flushed all SDK-owned session state, and the process exit that follows is the teardown of the rest of the tree. Process-level exits (stdin EOF → 0, SIGTERM → 0, SIGINT → 130) belong to the app bin, which disposes the whole root context. Fiber disposal WITHOUT a shutdown request (HMR-style unload) just stops serving — it never exits the process.
Wire notes
initialize.serverInfo.name is the wire-stable deepseek-harness-sdk-runtime (SDK clients key on it, independent of this package's name). A session accepts at most one in-flight session/prompt; an overlapping prompt for the same sessionId fails immediately through the standard handler-error response, while other sessions remain independent and the same session can be reused after the active prompt settles. Persistence roots and the deployment persona come from cordis.yml; the wire exposes only parameters the server applies.