线路协议是让一个进程能够驱动另一个进程中的 DeepSeek Harness 运行时的契约。本页专注协议本身——帧格式、具名消息类型,以及它们与会话和 Agent 主循环的关系。对话两端分别在 SDK 客户端 与 SDK 服务端 中讲解;本页引用 packages/sdk/protocol/src。
包
@deepseek-ai/dsh-sdk-protocol(packages/sdk/protocol/package.json)是一个纯库:它不注册插件、没有 Config、也没有任何注册动作,只提供传输层类与具名线路类型,外加导出的 JsonRpcResponseError。
| 字段 | 值 |
|---|---|
| name | @deepseek-ai/dsh-sdk-protocol |
| version | |
| 角色 | SDK 运行时的共享线路协议 |
| 产物 | lib/index.js、lib/types/**/*.d.ts |
| peer 依赖 | dsh-invariants、dsh-llm、dsh-session、dsh-subagent、cordis(仅类型) |
| 模块 | src/index.ts 从 transport.ts 与 types.ts 再导出 |
设计目标
协议回答一个特定的产品问题:当运行时没有终端 UI、也没有审批界面时,外部调用者(Python 或 TypeScript 进程)如何操作一个完整的 harness——而不只是调用单个模型?三个决定随之而来。
- 基于 stdio 的新行分隔 JSON-RPC 2.0。子运行时的
stdout被保留给帧;诊断信息属于stderr。通道就是进程自身的管道,因此没有端口、没有端口探测、也没有 HTTP。 - 持久事件流,而非提示词结果。服务端不会用一个 assistant 消息来回答提示词。相反,它把每一条会话日志事件与整个 agent 的状态转换作为 通知(notification) 推送出去,客户端从这条流中自行拼装一轮对话。
- 除了
initialize之外没有中场状态机。initialize是一次握手,在服务端整个生命周期内固定 cwd/provider/model;此后都是无状态的session/prompt请求。
传输层是 JsonRpcLineTransport(packages/sdk/protocol/src/transport.ts)。JsonRpcTransportPeer 是出站接口(只有 request/notify),服务端类与客户端都以它作为类型基准。
帧格式规则
每条 JSON-RPC 2.0 消息是一个紧凑 JSON 对象,用 JSON.stringify 序列化并以 \n 结尾。行通过 UTF-8 StringDecoder 读取、trim、逐行排空。非法 JSON 行会被静默忽略,不产生错误反压。
出站(服务端 -> 客户端):{"jsonrpc":"2.0","method":"session.event","params":{...}}\n
{"jsonrpc":"2.0","method":"session.status","params":{...}}\n
入站(客户端 -> 服务端):{"jsonrpc":"2.0","id":"req_…","method":"session/prompt","params":{...}}\n
{"jsonrpc":"2.0","id":"req_…","result":{"messageId":"…"}}\n帧分类(来自 handleLine):
| 帧形态 | 含义 |
|---|---|
同时带有 id 和 method | 请求——分发给请求处理器 |
只有 id | 响应——解析对应的 pending 请求 |
只有 method | 通知——分发给通知处理器 |
请求 id 形如 req_<uuid-hex>(randomUUID().replaceAll('-', ''))。request(method, params, signal) 支持可选的 AbortSignal:中止会移除 pending 条目(为永远不回应的响应保留陈旧状态),并以 signal 的 reason 拒绝。flush() 通过写入一个空屏障来等待先前的写回调。错误响应变成 JsonRpcResponseError,它保留线路上的 code 与可选的 data。
| 错误场景 | 线路码 | 说明 |
|---|---|---|
| 未安装请求处理器 | -32601(method not found) | |
| 处理器被拒绝 | -32603(internal error) | 携带 error.message |
有 pending 请求时 close() | 无 | pending 请求以 JSON-RPC transport closed 拒绝 |
| 非法 JSON 行 | 无 | 丢弃该行 |
// packages/sdk/protocol/src/transport.ts(节选)
export class JsonRpcLineTransport implements JsonRpcTransportPeer {
// ...
request(method: string, params: object, signal?: AbortSignal): Promise<unknown> {
const id = `req_${randomUUID().replaceAll('-', '')}`
const message = { jsonrpc: '2.0', id, method, params }
// 注册 pending 条目,写入帧,返回 promise
}
notify(method: string, params?: object): void {
this.write(params === undefined
? { jsonrpc: '2.0', method }
: { jsonrpc: '2.0', method, params })
}
}线路类型
packages/sdk/protocol/src/types.ts 命名了每种载荷。HarnessSdkRequestMap 索引客户端→服务端请求;HarnessSdkNotificationMap 索引服务端→客户端通知。
| 方向 | 方法 | 请求类型 → 结果类型 |
|---|---|---|
| 客户端→服务端 | initialize | InitializeParams → InitializeResult |
| 客户端→服务端 | session/prompt | SessionPromptParams → SessionPromptResult |
| 客户端→服务端 | shutdown | 无参数 → {} |
| 服务端→客户端 | session.event | SessionEventNotification |
| 服务端→客户端 | session.status | SessionStatusNotification |
| 服务端→客户端 | subagent.started | SubagentStartedNotification |
| 服务端→客户端 | subagent.finished | SubagentFinishedNotification |
initialize
进程级握手。cwd 会被记录到每个 SDK 创建的会话的 header 上;provider 与 model 成为每个 SDK 创建的 agent 所运行的路线。maxTokens 是可选的、必须为正的安全整数输出 token 上限,由 SDK 创建的 agent 及其进程内后代继承;非法值会拒绝握手,省略时则让适配器按精确模型或 provider 使用默认值。
// packages/sdk/protocol/src/types.ts(节选)
export interface InitializeParams {
cwd: string
provider: string
model: string
maxTokens?: number
}
export interface InitializeResult {
serverInfo: { name: string; version: string }
}serverInfo.name 是线上稳定的 deepseek-harness-sdk-runtime;version 是普通字符串。
session/prompt
某个 SDK 会话上的一轮用户对话。sessionId 是 SDK 侧 id;未知 id 会在服务端懒创建“agent+会话”对。contentBlocks 会被原样作为用户消息发送。结果是持久的入队回执,而非对话结果:
export interface SessionPromptParams {
sessionId: string
contentBlocks: ContentBlock[]
}
export interface SessionPromptResult {
messageId: string // 入队的 UserMessage 的身份
}messageId 只标识入队的 UserMessage,并不标识后面的 assistant 消息、一轮对话的结束或最终答案。因此客户端需要把开放的 session.event 流与整个 agent 的 session.status 结合起来,由自己拥有活动时间区间。
通知
四种服务端→客户端通知。载荷类型刻意依赖 SessionEvent(dsh-session)、ContentBlock(dsh-llm)与 SubagentStopReason(dsh-subagent)——协议流式传输的是完整会话日志外壳,因此会话词汇本身也是线上契约的一部分。
| 方法 | 载荷字段 | 语义 |
|---|---|---|
session.event | sessionId、event: SessionEvent | 运行时中每一个会话(不过滤)的一条持久会话日志事件 |
session.status | sessionId、status: 'idle' | 'running' | 整个 agent 的生命周期状态转换 |
subagent.started | parentSessionId、childSessionId | 运行时内创建了一个子会话 |
subagent.finished | provider、agentId、parentSessionId、childSessionId、status、stopReason、lastAssistantMessage? | 一次进程内子 agent 运行结束 |
// packages/sdk/protocol/src/types.ts(节选)
export interface SubagentFinishedNotification {
provider: string
agentId: string // 本地运行时等于 childSessionId
parentSessionId: string
childSessionId: string
status: SdkRunStatus // 'ok' | 'error'
stopReason: SubagentStopReason
lastAssistantMessage?: ContentBlock[] // 子 agent 未产生任何输出时缺省
}SdkRunStatus 是 'ok' | 'error'。lastAssistantMessage 是子 agent 最后一条非空的 assistant 消息,若不存在这样的消息则是其累积的 assistant 文本;只有子 agent 两者都没产生时该字段才缺省。subagent.finished 只上报进程内的子运行——远程运行不会出现在这条线上。
它如何映射到 Agent 主循环
协议是对 agent 主循环自身事件的一个前置包装。在服务端,HarnessSdkJsonRpcServer 订阅三个 Cordis 生命周期事件,并把每个重新投射为一条通知:
Cordis 事件(ctx.on) | 发出的通知 |
|---|---|
session/event | session.event |
agent/status | session.status |
session/created(带 header.parentSession) | subagent.started |
subagent/end(仅当 info.local) | subagent.finished |
没有“本轮对话以此文本结束”的推送通道。最接近的持久事实是 turn/end 会话事件,由客户端从 session.event 流中读出。这正是 SDK README 反复强调“提示词级别结果被刻意省略”的原因:活动归属属于观察者,而非线路本身。
版本管理
不存在协议版本协商。握手只携带 serverInfo.version,客户端并不校验它。这是明确的预发布立场,跨版本不提供兼容性承诺。后果是:SDK 客户端与运行时必须来自同一发布通道;把来自某个 revision 的客户端与不同的服务端 revision 混用,超出契约范围。
已知协议缺口
| 缺口 | 影响 | 变通 |
|---|---|---|
| 没有取消方法 | 客户端无法单独放弃一轮对话 | 关闭运行时进程 |
| 没有会话关闭方法 | SDK 创建的 agent 会一直存活到进程关闭 | — |
| 没有针对提示词的结果 | messageId 只是收件箱准入凭证 | 客户端自己负责“回执→idle”的收集 |
| 服务端→客户端请求 | 传输层支持,但服务端从不发送 | Python 的响应面为将来审批流程预留 |
| 没有协议版本协商 | 握手版本不被校验 | 让客户端与运行时保持同步 |
可运行示例
这套协议由 examples/jsonrpc-agent/cordis.yml 端到端实践:它在一个无人值守的编码 agent 组合(bash、文件工具、子 agent、todo、JSONL 持久化、compaction)内部挂载 sdk-jsonrpc-server 插件;dsh-jsonrpc-agent bin 的生命周期则在 packages/examples/jsonrpc-demo/src/runner.ts。examples/jsonrpc-agent/tests/snapshots/*/notifications.expected.jsonl 下的快照展示了线路实际承载的 session.event/session.status/subagent.* 形状。
延伸阅读
- SDK 客户端——讲这套协议的
DeepSeekHarness/HarnessSession/HarnessClient各层。 - SDK 服务端——
@deepseek-ai/dsh-sdk-jsonrpc-server如何无头地托管 agent。 - Typert:类型生成器——同一工程章节中、与本协议无关的类型/schema 生成器。
- 发行组合:
examples/jsonrpc-agent/cordis.yml(服务端行后面到底有哪些插件)。 - 会话事件与 agent 事件的区分:会话管理。
- 仓库内相对路径:
packages/sdk/protocol/src/transport.ts、packages/sdk/protocol/src/types.ts。