Files
deepseek-harness/packages/sdk/protocol/README.md
T

153 lines
13 KiB
Markdown

---
description: "The SDK wire protocol for client and server implementers: the newline-delimited JSON-RPC transport and the named request, result, and notification types spoken between a Harness runtime and its SDK clients."
kind: "package-library"
---
# @deepseek-ai/dsh-sdk-protocol
English | [中文](README.zh.md)
## Summary
`dsh-sdk-protocol` lets a DeepSeek Harness runtime and its SDK clients exchange JSON-RPC 2.0 messages over newline-delimited byte streams: one transport class plus the named request, result, and notification types both wire ends speak. The serving side is the [`dsh-sdk-jsonrpc-server`](../server/README.md) plugin; the clients are the TypeScript [`dsh-sdk-client`](../client/README.md) and the [Python SDK](../../../python/README.md), which mirrors these shapes without importing them. Use this package when you implement or debug a wire end: framing rules, method names, payload types, and error semantics all live here. It is a pure library — no plugin, no configuration, no registrations.
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
Use this package when you build or debug an SDK wire end — the serving plugin, a client library, or custom tooling that speaks the SDK protocol. It gives you one transport for JSON-RPC 2.0 over caller-owned byte streams and the typed shapes for every SDK method and notification.
`InitializeParams.approvalRequests: true` enables `approval/request` server requests, indexed by `HarnessSdkClientRequestMap`. `ApprovalRequestParams` contains `sessionId`, `toolName`, optional `callId`, and optional `reason`; `ApprovalRequestResult` is `{ outcome: "allowed-once" | "rejected" | "cancelled" }`. Omission or `false` leaves existing answerers in control. Only `allowed-once` grants one operation. Cancellation discards the pending request and late responses; shutdown and transport loss fail pending decisions closed. The client must keep reading and answering requests while waiting for an Agent to finish.
### Framing and transport
Wire one JSON-RPC 2.0 message per `\n`-terminated line over byte streams you own. A frame with both `id` and `method` is a request, `id` alone is a response, and `method` alone is a notification; malformed lines are ignored. Requests with no registered handler answer `-32601`, handler failures answer `-32603`, and error responses reject the pending request with `JsonRpcResponseError`, which preserves the wire `code` and optional `data`. `start()` attaches stream listeners and `close()` detaches them and rejects pending requests without destroying the streams.
### The SDK methods
Both wire ends share one method set: client-to-server requests, opt-in server-to-client requests, and server notifications.
| Direction | Method | Payload types |
|---|---|---|
| client→server | `initialize` | `InitializeParams` → `InitializeResult` |
| client→server | `session/prompt` | `SessionPromptParams` → `SessionPromptResult` (durable enqueue receipt) |
| client→server | `session/query` | `SessionQueryParams` → `SessionQueryResult` |
| client→server | `session/resume` | `SessionResumeParams` → `{}` |
| client→server | `session/wait` | `SessionWaitParams` → `{}` |
| client→server | `session/working-directory/get` | `SessionWorkingDirectoryParams` → `SessionWorkingDirectoryResult` |
| client→server | `session/working-directory/set` | `SessionWorkingDirectorySetParams` → `SessionWorkingDirectoryResult` |
| client→server | `shutdown` | no params → `{}` |
| server→client | `approval/request` | `ApprovalRequestParams` → `ApprovalRequestResult` |
| server→client | `user-question/ask` | `SdkUserQuestionRequest` → `SdkUserQuestionResult` |
| server→client | `user-question.cancelled` | `SdkUserQuestionCancelled` |
| server→client | `session.event` | `SessionEventNotification` (every session in the runtime, unfiltered) |
| server→client | `session.status` | `SessionStatusNotification` (whole-agent `running`/`idle` transition) |
| server→client | `session.query.completed` | `{ sessionId, commandId, finalResponse, finishReason }` |
| server→client | `subagent.started` | `SubagentStartedNotification` |
| server→client | `subagent.finished` | `SubagentFinishedNotification` (in-process runs only) |
`HarnessSdkRequestMap`, `HarnessSdkClientRequestMap`, `HarnessSdkHostRequestMap`, and `HarnessSdkNotificationMap` index these shapes by method name; the package root exports them together with the transport.
`session/wait` accepts `{ sessionId }` for an existing SDK-owned session and returns `{}` after the root stays idle and progressing managed descendant work finishes. Idle descendants with parked input remain resident without blocking this wait. Unknown ids and live Agent failures without a newer committed terminal are rejected. Notifications committed during the wait precede its response on the same transport; raw `session.status` transitions retain their whole-agent meaning.
`session/query` accepts `{ sessionId, contentBlocks }`, where `contentBlocks` uses `SdkPromptContentBlock[]`. Both SDKs normalize string input into a text block and send content-block arrays through this same entry. The runtime replies with `{ kind: "prompt", messageId }` for queued ordinary input, preserving its block order, or `{ kind: "command", execution }` after a supported command settles. A recognized command requires one complete text block with only encoded images beside it; other combinations reject before execution or enqueue. The images go through native attachment admission and handler validation. Empty arrays and arrays containing only blank text are rejected; image-only input is allowed. A prompt receipt is not a model result; command completion does not require an inbox receipt or idle notification. Recognition and the supported command set belong to the [SDK server](../server/README.md).
`session/prompt` queues content blocks without native slash-command routing; runtime skill recognition still applies.
Command lifecycle notifications precede the `session/query` handler response. A command query ends high-level observation with `session.query.completed` for the same `commandId`, after model activity it starts; goal creation and resume include goal termination and closing activity. State-only commands need no model turn, and their completion notification can precede the response. SDKs consume this receipt internally and return `RunResult`; forwarders must preserve order. Shutdown cancels unfinished observation.
`session/resume` accepts a non-empty persisted `{ sessionId }` and returns `{}` after restoring that exact session. It requires initialization and the original persistence store, sends no new input, and rejects missing, unreadable, incompatible, or occupied sessions without creating a replacement. Historical events remain in the stored log; notifications report newly committed events.
<a id="native-user-questions"></a>
### Native user questions
An answering host sets `initialize.capabilities.userQuestions: true`. The runtime sends `user-question/ask` with `{ requestId, sessionId, questions }`; questions retain their original ids, options, detail, and presentation intent. The host returns `{ answers: [{ id, selected, custom? }] }`, naming every question id exactly once, or `{ cancelled: true }` with no other fields for a user dismissal. Dismissal maps to native `ASK_CANCELLED`; it is not runtime cancellation, a provider failure, or approval. Invalid batches fail with `BAD_ANSWER`; host or transport failures fail with `PROVIDER_ERROR`. The SDK does not synthesize answers.
`user-question.cancelled` identifies the request that stopped waiting. The host cancels its own work and discards the question; the runtime abandons its pending RPC and ignores late responses. Without the advertised capability, existing in-process answerers remain responsible and an absent answerer reports `NO_PROVIDER`. This flow requires no GUI.
### Payload semantics
`SessionPromptResult.messageId` identifies the queued user message; it does not identify a later assistant message, turn ending, or prompt result. `SdkPromptContentBlock` accepts ordinary durable content plus `SdkEncodedImageBlock { type: "image", data, mimeType }`; the server converts encoded images to durable references before enqueue. `InitializeParams.reasoningEffort` is an optional non-empty adapter-owned identifier for the selected provider/model route; omission preserves that model's default. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission lets the selected adapter's exact-model default apply. The server resolves the exact route during initialization and rejects `session/prompt` until that handshake succeeds, so a missing adapter, unavailable model, or unsupported effort cannot fall back to constructor defaults. `SubagentFinishedNotification.lastAssistantMessage` carries the child's last non-empty assistant message, or its accumulated assistant text when no such message exists; the field is absent when the child produced neither. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`. Notification payloads depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`), so the session vocabulary is part of the wire contract.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
This section explains the design behind the wire library; the observable behavior is fully covered in [Use this package](#use-this-package).
### Design concept
The package is built on one separation: a single newline-delimited transport class shared by both wire ends, and named types that index the protocol methods. The package root is the only import surface — source modules are not exported as deep imports. It is a pure library with no plugin, config, or registration; the serving plugin and the clients own all behavior around it.
### Source map
| File | Role |
|---|---|
| [`src/transport.ts`](src/transport.ts) | `JsonRpcLineTransport`: line framing, request/response/notification dispatch, error mapping, pending-request bookkeeping |
| [`src/types.ts`](src/types.ts) | Named request/result and notification payload types, indexed by method |
| [`src/index.ts`](src/index.ts) | Consumer interface: the transport and the named wire types |
### Frame dispatch
Incoming lines are parsed one at a time: a frame with `id` and `method` is answered through the request handler (or `-32601`), a frame with `id` alone resolves the matching pending request (an error frame rejects it with `JsonRpcResponseError`), and a frame with `method` alone is handed to the notification handler. `start()` attaches the input listeners; `close()` detaches them and fails every pending request without destroying the streams.
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
Read these pages when the wire contract is not enough. They move from the serving plugin to the clients and the runnable application.
- [JSON-RPC serving plugin](../server/README.md) — the runtime plugin that serves this protocol over stdio.
- [TypeScript SDK client](../client/README.md) — the client that drives this protocol.
- [Python SDK](../../../python/README.md) — the Python counterpart that mirrors these shapes.
- [SDK application bundle](../../bundle/sdk-app/README.md) — the `dsh --profile sdk` application that boots the server.
-----
<a id="model-experience"></a>
## Model Experience
None, as this is a client-facing wire library; the runtime plugins behind the serving entry own all model-facing behavior.
#### KV Cache effect
None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
These limits define what the protocol does not cover or promise. They are current package constraints, not a comparison with other wire formats or a task backlog.
- **No protocol-version negotiation** — the handshake carries only `serverInfo.version` (`0.0.1`, unvalidated by clients); pre-release stance, no compatibility promise.
- **No cancel or session-close methods** — a client abandons a turn by closing the runtime process; see the [JSON-RPC serving plugin](../server/README.md).
- **Question waits are process-local** — question request ids are not durable handles for replying after runtime restart. Session history retains completed tool results through its ordinary persistence path.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
This Dev Note is working context for maintainers and is explicitly non-authoritative — shipped behavior and limits live in the sections above and in the code. This protocol's shapes are mirrored (not imported) by the Python SDK, so changing a method, payload, or the wire-stable `serverInfo.name` here requires updating the Python counterpart and the TypeScript client in the same change. No other unresolved design questions are recorded.
</details>