DeepSeek Harness 深度解析:一切皆插件的 Agent 运行时与自定义开发指南
从 Agent 主循环到能力缝架构,从前端面板定制到接入私有模型,一篇文章讲清 DeepSeek Harness 的设计与扩展方式
一、这是什么:一切皆插件的 Agent 运行时
DeepSeek Harness 是一个基于 Cordis 插件框架构建的 Agent 运行时(monorepo,npm scope 为 @deepseek-ai/dsh-*)。它最核心的理念是:没有特权核心。模型适配器、工具注册表、会话日志、甚至 Agent 主循环本身,全部都是插件,挂载到共享的 Context 上——任何部分都可以从配置层替换,而不需要改主循环代码。
这种设计带来一个直接的好处:作为使用者,你几乎所有的定制需求——换模型、加工具、改 UI、定义子智能体、定制每会话的 Agent 组合——都不需要 fork 代码,只需要往对应的扩展点注册实现。
架构分层一览:
二、两个基石设计模式
2.1 能力缝(Capability Seam)
每类可替换能力都是一个「三角色组合」,缺一不可:
- Service Definition:拥有
ctx.<key>的抽象Service类(如LlmRuntime、ShellExecutor),定义接口词汇; - Service Provider:一个或多个实现该接口的插件;
- Consumer:注入服务并使用它的插件(通常是面向模型的工具)。
典范是 shell 家族:dsh-shell(Definition,ctx.shell)→ dsh-bash-local / dsh-pwsh-local(Provider)→ dsh-tool-bash(Consumer)。扩展插件只依赖 Definition,绝不依赖具体 Provider——这正是可替换性的来源。
2.2 Cordis 事件四模式
插件之间通过类型化事件协作,所有事件用 TypeScript declaration merging 声明:
| 模式 | 语义 | 典型事件 |
|---|---|---|
emit | 通知,不等待 | agent/created、session/event |
waterfall | 中间件链,可改写结果;listener 必须调 next() 否则短路 | agent/request、tools/pre-execute |
parallel | 并行等待所有 listener | session/flush |
serial | 按序等待 | agent/turn-stopping |
所有注册(工具、监听器、适配器)都通过 ctx.effect() / ctx.on() 安装,插件卸载时自动回收——注册是可逆副作用。
三、Agent 是怎么写的
Agent 分两层:packages/core/agent 提供抽象(Agent 接口、AgentRegistry),packages/core/agent-loop 提供实现(ReactLoopAgent 主循环驱动器)。
3.1 主循环:事件驱动的命令式状态机
入口是 Inbox:followup() / steer() / inject() 把消息排入队列,每次变更都持久化为 agent/inbox/spliced 事件。wakeDriver() 唤醒处于 idle 相位的驱动器,进入 turn():
- 组装提示:每个 step 先调
ctx.systemPrompt.assemble()(sections/contexts/variables 分层合并,带system-prompt/assemblewaterfall),从 event-sourced 的 session surface 派生历史消息; - pre-step 裁决:
agent/pre-stepwaterfall——插件可拒绝或替换进入 step 的消息; - 请求配置:
agent/requestwaterfall——插件可改写本次请求的 LLM 配置; - 流式请求:每个 chunk 落
assistant/chunk日志,支持 token 级重放; - 错误接管:失败走
agent/request-errorwaterfall,listener 返回{kind:'retry'}即接管重试; - 工具调度:
executeToolCalls()把exclusive工具构成 barrier,parallel工具走有界并发池;派发可重叠但结果严格按模型顺序提交;每个调用经过tools/pre-execute(审批门)→tools/execute(超时/重试包装)→tools/post-execute(结果改写)三段 waterfall; - turn 收尾:
agent/turn-stoppingserial(listener 可steer()追加工作),然后turn/end。
3.2 持久化:事件溯源
Session 是 append-only 事件日志(turn/*、step/*、user/message、assistant/*、tool/* 等),核心不变量是**「模型可见 ⟺ 可从日志重建」**。新增模型可见输入必须通过 declaration merging 扩展 SessionEventMap。真正的落盘由订阅 session/event / session/flush 的持久化插件完成(JSONL/SQLite provider),主循环本身不关心存储。
3.3 组合:preset 机制
一个 preset = 一个含 agent.cordis.yml 的目录(目录名即 id)。每个 preset 在进程内只挂载一次(standing mount),Agent 创建时通过 scope 父子绑定加入:工具、prompt section 向下继承,事件向上传播。子 Agent 通过 composeFrom() 继承父 Agent 的同一代组合。apps/cli/config/agent-presets/ 下的 standard / code / minimal 就是现成例子——minimal 甚至演示了用 isolate: { fs: true } 里的本地 fs provider 遮蔽宿主沙箱 provider 的写法。
四、自定义前端面板:client 插件 + slot 系统
内置 Web GUI(apps/web + packages/client/*,React 18)本身就是插件化组合的,扩展点是 slot 系统(ctx.slots.register)。想在某处加一个 tab,标准做法如下:
- 新建 client 插件包:
package.json声明dsh.client(platform: 'web'),经exports["./client"]导出构建好的 bundle,在部署的cordis.yml中加一行组合它; - 向 slot 注册组件:在
apply()里调用ctx.slots.register({ name, ... }, Component)。
常用 slot 入口:
| Slot | 效果 |
|---|---|
'conversation.view' | 会话主区的视图标签页环——Chat、Trajectory 都是其中一页,注册 { id, order, label } 即投影为新 tab |
'settings.section' / 'settings.plugins.tab' | 设置页加分区 / 加标签页 |
'conversation.session.header.actions' | 会话标题栏按钮 |
'conversation.input.dock' | 输入区 dock(TodoDock 在此) |
'conversation.composer' | chain 式接管输入区(审批面板的实现方式) |
'tool.call.toolview' | 按工具名定制工具卡片 |
约束:一个 UI 功能一个包;跨包不直接 import 符号,只走 slot 或 ctx 服务。工具侧无需碰前端代码——presentCall / presentResult 返回纯函数的 render intent(generic / terminal / diff / search / web + locations 文件跳转),各 UI 自行映射渲染。
如果不想进入这套 React/slot 体系,也可以走 packages/sdk(stdio JSON-RPC,有 TS 和 Python client)自写前端,但拿不到工具卡片、富审批 UI 等能力;ACP 则明确只面向自动化场景。
五、接入自己的模型提供商
LLM 是标准能力缝:LlmRuntime(ctx.llm)是 Definition,LlmAdapter 是要实现的抽象类——唯一必需的方法是 stream(options)(产出 StreamChunk,尊重 signal),可选覆写 resolveModel()、listModels()、providerRetryPolicy()。
路线 A:零代码(内部网关是 OpenAI 兼容协议时)
直接用现有 adapter:
- 给
llm-deepseek配baseURL指向内部网关; - 或在
llm-pi-ai的 settings 里手写声明路由:baseURL+api: openai-completions+apiKeyEnv+ 模型清单。
API key 通过 credentials 能力管理:配置里只放环境变量名引用,值由 ctx.credentials 每次请求时解析,改 key 免重启。
路线 B:自写 provider 插件
仿照 packages/llm/llm-deepseek,两个核心文件:
src/adapter.ts:class MyAdapter extends LlmAdapter,实现stream(),请求必须带attributionHeaders();src/index.ts:cordis 插件,inject = ['llm'],apply()里:ctx.llm.registerAdapter(['my-route'], adapter)ctx.llm.registerConfigurableProviders([...])(让设置页能发现)- 可选
installSettingsSection()支持热更新;
- 宿主
cordis.yml加一行{ id: llm-mine, name: '<你的包>', config: {...} }即完成装配。
另有 llm/stream waterfall 可在不碰 adapter 的情况下做全局重试、路由、replay。
六、子智能体:原生支持且可插件化扩展
packages/subagent 是完整能力缝:
- Definition:
SubagentRuntime(ctx.subagents),命名 provider 注册表 +start(name, request)一次性委派 + continuable 系列(followup/interrupt/reportFrom); - Provider:内置
spawn(全新子 Agent)、fork(以父会话前缀为 seed),以及进程外的acp/codex/claude-code/dsh-sdk后端。自定义后端 = 实现SubagentProvider接口 +ctx.subagents.registerProvider(),一行配置即可被所有 preset 按名选用; - Consumer:
dsh-tool-subagent注册subagent工具,config 指定provider、toolName、backgroundMode、agentOptions(子 Agent 的模型路由)、persona(scoped 覆盖)、toolFilter、maxDepth(默认 3)。
preset 里可挂多个实例各指不同 provider——standard preset 就同时挂了 spawn / fork / codex / claude-code 四个(后两个默认 disabled)。子 Agent 自动继承父 Agent 的 preset 组合。
七、开发速查表
| 需求 | 做法 |
|---|---|
| 加模型 provider | 实现 LlmAdapter 注册到 ctx.llm(或零代码配 llm-pi-ai 路由) |
| 加模型工具 | ctx.tools.register(defineTool({...})),声明 render intent |
| 加 UI 面板 / 标签页 | client 插件 + ctx.slots.register('conversation.view' 等) |
| 加持久状态 | declaration merging 扩展 SessionEventMap |
| 拦截请求 / 工具 / 轮次 | 监听 agent/request、tools/pre-execute、agent/turn-stopping 等 waterfall |
| 加子智能体后端 | 实现 SubagentProvider 注册到 ctx.subagents |
| 定制每会话 Agent | 在 $DSH_HOME/.agent-presets/<id>/agent.cordis.yml 写 preset |
| 限定单 Agent 的注册 | 用 agent.ctx(scope 机制,同名注册遮蔽全局) |
配套文档:docs/architecture.md(架构总纲)、docs/cordis-primer.md(框架机制)、docs/cookbook/(加包 / 工具 / LLM adapter / Chat 节点的分步指南)、packages/client/AGENTS.md(UI 插件约束)。
结语
这个 harness 的一切扩展——换模型、加工具、改 UI、定义子智能体、定制每会话组合——都不需要改主循环。要做的只是:往对应的能力缝注册 Provider、往事件 waterfall 挂监听器、往 slot 注册组件、或往 preset 写一份 agent.cordis.yml。开发时先确定你的需求属于哪条缝,再按三角色(Definition / Provider / Consumer)的既有范例动手即可。
Last updated on