服务端是 SDK 的运行时侧半边。它是一个 Cordis 插件,名叫 sdk-jsonrpc-server,由 @deepseek-ai/dsh-sdk-jsonrpc-server 实现。它通过 stdio 服务线路协议,让进程外的 SDK 客户端(TypeScript 与 Python)无头地驱动 harness agent——没有终端 UI、没有审批界面、不监听端口。本页引用 packages/sdk/server/src 及其周边组合。
包
| 字段 | 值 |
|---|---|
| name | @deepseek-ai/dsh-sdk-jsonrpc-server |
| version | |
| 角色 | 面向进程外 SDK 客户端的 stdio JSON-RPC 服务端插件 |
| 协议对端 | @deepseek-ai/dsh-sdk-protocol |
| 依赖 | @deepseek-ai/schemastery(Config 类型) |
| peer 依赖 | dsh-agent、dsh-llm、dsh-llm-deepseek、dsh-scope、dsh-session、dsh-subagent、dsh-sdk-protocol、dsh-invariants,外加 cordis |
服务端从 ./server.ts 再导出 HarnessSdkJsonRpcServer,而 src/index.ts 声明插件本身(name、inject、Config、apply)。只有具名导出、没有 default 导出——这样 Loader 的 unwrapExports 才能保留插件形态。 |
插件契约
// packages/sdk/server/src/index.ts(节选)
export const name = 'sdk-jsonrpc-server'
export const inject = ['agents'] // 只需要 Agent 工厂
export interface JsonRpcConfig {
maxTokensAsSuccess?: boolean // 把达到 max-tokens 的结束上报为 'ok'
input?: Readable // 仅运行时的钩子;生产用 process.stdin
output?: Writable // 仅运行时的钩子;生产用 process.stdout
exit?: (code: number) => void // 仅运行时的钩子;生产用 process.exit
}
export const Config: Schema<JsonRpcConfig> = Schema.object({
maxTokensAsSuccess: Schema.boolean().default(false),
})input/output/exit 是仅供测试的运行时传输钩子;生产使用进程 stdio 与 process.exit。只有 maxTokensAsSuccess 可从 cordis.yml 设置。
是否加载该插件完全由周边的 cordis.yml 决定。examples/jsonrpc-agent/cordis.yml 是参考部署:它在 DeepSeek 适配器、bash 执行器、agent spine、JSONL 持久化、compaction、进程内子 agent spawn provider 旁边叠放服务端插件。
stdout 即协议
stdout 只承载 JSON-RPC 帧。部署方不得再组合一个 stdout logger;诊断信息属于 stderr。这一点靠组合而非检查来保证——插件不会否决兄弟 logger,因此一个加载了 stdout logger 的周边配置会破坏通道。
服务端生命周期
apply 把一条 JsonRpcLineTransport 接到所选流上,并构造服务端:
// packages/sdk/server/src/index.ts(节选)
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请求的处理器先运行,然后setImmediate编排disposeAndExit:它会flush 响应、处置根上下文(ctx.root.fiber),让 SDK 拥有的 agent、订阅与持久化达到静默状态,然后以代码0退出。因此协议级 shutdown 拥有整个运行时进程的所有权。- EOF 与信号退出属于应用 bin——loader 运行器(
packages/examples/jsonrpc-demo/src/runner.ts里的dsh-jsonrpc-agent)处理stdin 'end'、SIGTERM、SIGINT时处置根 fiber 并退出。 - 只卸载这个插件(例如经 HMR)会停止对外服务但不会退出进程——
ctx.effect的处置路径会先运行server.shutdown()再transport.close()。
共享的 exitTask 确保并发的 shutdown 请求不会多次处置根或退出进程。
HarnessSdkJsonRpcServer
该类(src/server.ts)拥有协议方法、会话注册表与事件再投射。构造时会订阅会话/agent/子 agent 生命周期事件,直到 shutdown;不支持重新初始化。
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> // 分发 switch
// 私有:getOrCreateSession / createSession / hasAdapterFor
}handleRequest 是 JSON-RPC 分发 switch:initialize、session/prompt、shutdown,否则抛 unknown DeepSeek Harness SDK runtime method。
模型与 agent 主循环接线
每个 SDK 会话映射到注册在 ctx.agents 上的一个存活 agent。getOrCreateSession 是一个记忆化工厂,会对进行中的创建去重:
// packages/sdk/server/src/server.ts(节选)
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 }),
},
})
// 记录 handle 到 this.sessions
}initialize 选定 provider/model 路线并处理适配器 seam:
- 若已有已注册适配器服务所请求的 provider,则复用它。
- 未拥有的
deepseek-official路线会以llmFiber方式挂载dsh-llm-deepseek。 - 任何其他未拥有的 provider 都会在初始化时失败。
Agent 工厂是唯一的硬性 inject;可选的 LLM seam 通过 ctx.get('llm') 读取(hasAdapterFor 会列出 providers)。组合把模型相关的行放在 host 平面,因此 SDK 创建的 agent 从全局层读取它们,而不是从一个预设名单读取(按 agent-presets README 中关于组合子 agent 的说明)。
prompt 在投递前会验证存活注册表(仅重载 agent-loop 可能会处置掉一个 agent,而此记录仍存活,届时该 SDK agent 不再等于 ctx.agents.get(...)),然后入队:
const message = createUserMessage({ content: params.contentBlocks, source: { kind: 'user' } })
rec.handle.agent.followup(message)
return { messageId: message.id }这就是“入队一条具名的用户消息,立即返回”的回执。followup 唤醒 agent 主循环;该响应没有任何针对提示词的结果。
事件再投射
构造函数注册四个 ctx.on 订阅,fan-out 成通知:
| Cordis 事件 | 通知 |
|---|---|
session/event | session.event(运行时内每条会话,不过滤) |
agent/status | session.status(整个 agent 的 running/idle) |
带 parentSession 的 session/created | subagent.started |
info.local 为真的 subagent/end | subagent.finished |
subagent.finished 的本地性规则很微妙:服务会对运行的 local 标志做快照(通过 subagentParentOf 用 carrierKeyOf 从作用域载体 Scoped<SubagentRuntime> 中恢复委托父 agent),并且只有进程内的子会话才发出该通知。provider 名、子 id 与持久血缘本身都不足以确立本地性。
配置键
键(cordis.yml 的 config:) | 默认 | 含义 |
|---|---|---|
maxTokensAsSuccess | false | 在 subagent.finished 上把 max-tokens 结束上报为 status: 'ok';根会话的提示词没有提示词级状态 |
initialize.maxTokens(线路参数,而非配置键)是可选的、必须为正的输出 token 上限,由 SDK 创建的 agent 及其进程内后代继承;非法值拒绝握手,省略时则不发送 SDK 上限,从而让适配器或 provider 路线的默认值生效。
单发与持久:与 dsh-headless 交叉引用
SDK 服务端是仓库中两种“无头服务端”模式之一。对比:
| 方面 | dsh-sdk-jsonrpc-server(持久) | dsh-headless(@deepseek-ai/dsh-headless) |
|---|---|---|
| 传输 | 基于 stdio 的新行分隔 JSON-RPC | 无——直接在进程内驱动 Agent |
| 交互模型 | 多条 session/prompt 轮次、会话、订阅 | 恰好一个任务、一个新 Agent、退出 |
| 结果 | 没有针对提示词的结果;事件流向客户端 | 把最后一条 assistant 文本写到 stdout,以 0/1 退出 |
| 组合 | 由外部 cordis.yml 挂载 | cordis.patch.yml 叠加在 dsh-base 上,无 Host/HTTP |
| 退出 | shutdown 请求、EOF 或信号(bin 持有) | 由启动器提供的 ctx.appExit 钩子 |
dsh-headless(packages/bundle/headless/src)通过核心注册表创建一个 Agent,把任务驱动到静默状态,flush 它的 Session,打印最终 assistant 文本,然后经由启动器提供的 appExit host 钩子退出——绝不打开监听端口。它是“单发”兄弟;SDK 服务端则是“持久、协议驱动”的兄弟。apps/cli(@deepseek-ai/dsh)依赖 dsh-headless 提供 dsh --profile headless "<task>" 单发模式。
已知局限
| 局限 | 后果 |
|---|---|
| 线上没有按会话关闭或提示词取消防法 | SDK 创建的 agent 保持存活直到进程关闭 |
| 没有针对提示词的结果 | messageId 只标识收件箱准入;拥有自动化时间区间的客户端必须自行定义并观察该区间 |
| stdout 纯净性靠部署保证 | 周边配置仍可能加载 stdout logger 并破坏通道 |
| 自动适配器挂载是 DeepSeek 特定的 | initialize 复用任何已注册适配器,但它唯一的回退只挂载 dsh-llm-deepseek |
| 仅进程内子 agent | subagent.finished 上报本地子运行;远程运行不会上报 |
包
| 包 | name | version |
|---|---|---|
| SDK 服务端 | @deepseek-ai/dsh-sdk-jsonrpc-server | |
| SDK 线路协议(对端) | @deepseek-ai/dsh-sdk-protocol | |
| 示例 bin | @deepseek-ai/dsh-sdk-jsonrpc-demo(dsh-jsonrpc-agent) | |
| 单发 bundle | @deepseek-ai/dsh-headless | |
| CLI | @deepseek-ai/dsh | |
参考组合 examples/jsonrpc-agent/package.json 是一个私有示例包(jsonrpc-agent-example)——不是已发布的 SDK 工件。 |
延伸阅读
- SDK 协议——本插件服务的帧格式、方法与类型。
- SDK 客户端——线的另一端。
- shutdown 语义见 SDK:线路协议与
packages/examples/jsonrpc-demo/src/runner.ts中的 bin 生命周期。 - 单发对比:
@deepseek-ai/dsh-headless见packages/bundle/headless/src/startup.ts。 - 运行时、agent 与会话概念:运行时与 Agent 生命周期与会话管理。
- 仓库内相对路径:
packages/sdk/server/src/server.ts、packages/sdk/server/src/index.ts、examples/jsonrpc-agent/cordis.yml。