DeepSeek Harness(dsh)是 DeepSeek 开源的 agent 底座,口号就一句:everything is a plugin。它是一个让模型适配器、工具注册表、会话日志、甚至 agent 循环本身都能被插件替换的运行时框架,跑在 DeepSeek 自己维护的 Cordis 插件框架上,MIT 协议,目前处于 developer preview。这篇拆它的插件化设计,以及能抄走什么。

Cordis 的五个理念

Cordis 是 dsh 的底层插件框架,理解 dsh 得先理解它的五个基本设定。第一,插件是实现了 Service 的对象:一个带 inject 和 apply(ctx) 字段的函数。第二,context 是服务的仓库,一个服务占一个稳定的 ctx 键,其它插件按 key 找服务,而不是 import 具体实现。第三,依赖用 inject 声明:声明它需要哪些服务,就等这些服务就位才挂载,加载顺序靠服务依赖表达,而不是手写 boot 顺序。第四,事件是类型化的,分发方式有 emit、waterfall、parallel、serial、bail 五种。第五,注册是可逆 effect,prompt 段、工具 schema、适配器、监听器都通过 ctx.effect() 或 ctx.on() 装上去,reload 和 teardown 时能按序回退。

这五条指向一个结论:dsh 没有特权核心,扩展它是往旁边挂插件,不是 patch 中心模块。

profile 与 bundle

一个跑起来的 dsh,是一个启动时从有序 layer 组合出来的插件树。组合的容器叫 profile,分发格式叫 bundle。

profile 是存 Harness home 里的一个命名组合:它列出要叠哪些 bundle、装了哪些 out-of-tree 插件、保留用户自己的 cordis.patch.yml。官方随包带 web、headless、sdk、sdk-minimal、acp 五个模板。bundle 是 Cordis 配置行和它们挂载代码的分发格式,保证它插入的东西还能被上面的 layer patch。两者都在自己的 package.json 里用一个 dsh 字段声明:

{
  "name": "my-agent-profile",
  "dsh": {
    "profile": ["dsh-base", "dsh-headless", "my-team-bundle"]
  }
}

dsh-base 是 web、headless、sdk、acp 四个 profile 共享的第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭证、遥测。dsh-web-app 往上加浏览器应用,dsh-headless 加一个无服务的单次 runner,dsh-sdk-app 加 SDK 的 JSON-RPC server。想看你机器实际 boot 出来的树,一条命令:

dsh --profile web --dump-config

打印出来的每一行,都能被你自己的 patch 替换掉。这就是"无特权核心"在组合层的体现——连 base 都能盖。

一个最小工具

最能说明插件化的是写一个工具。dsh 里一个模型可见的工具是一个 defineTool 调用,schema 会自动流进 system prompt 组装。下面是一个自写的取 issue 工具:

import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'my-issue-tool'
export const inject = ['tools']

export function apply(ctx) {
  ctx.tools.register(defineTool({
    name: 'fetch_issue',
    description: 'Fetch a GitHub issue by number.',
    parameters: {
      repo: { type: 'string', required: true, description: 'owner/repo' },
      number: { type: 'number', required: true },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      const res = await fetch(
        `https://api.github.com/repos/${args.repo}/issues/${args.number}`,
        { signal: exec.signal },
      )
      const body = await res.json()
      return body.title
    },
  }))
}

注意两点:inject: ['tools'] 声明依赖 ctx.tools,apply 里就能拿到这个服务;注册是 effect-based,卸载插件时自动注销工具。execute 收到的 args 已按 schema 校验,exec.signal 是操作级取消信号,执行身份在分发期间不可变。加一个能力和关掉一个能力是同一件事的两面,都不需要动循环本身。

核心包与 ctx 键

dsh 的能力被拆进几个核心包,每个包占一个 ctx 键:

包 拥有什么 ctx 键
core/session append-only 的 SessionEvent 日志与内存 store ctx.sessions
core/system-prompt prompt 段与工具 schema 的组装 ctx.systemPrompt
core/tools 带作用域的工具注册表与受保护的执行管线 ctx.tools
core/agent Agent 接口、活跃注册表、agent/* 事件 ctx.agents
core/agent-loop 默认的循环驱动 ctx.agentLoop
llm/llm 消息/流词汇表与适配器缝 ctx.llm

模型适配器也一样。加一个 provider 就是注册一个 adapter:

import { LlmAdapter } from '@deepseek-ai/dsh-llm'

export const name = 'llm-myprovider'
export const inject = ['llm']

export function apply(ctx, config) {
  ctx.llm.registerAdapter(['my-provider'], new MyAdapter(config))
}

一个 provider 路由对应一个 adapter,重复注册会抛错。provider/model 由 options 选择,所以一个动态 catalog adapter 可以服务新模型而不需要重新配置生命周期。

三类事件与会话日志

扩展点分三个域。session 事件是落进日志的持久事实,事实必须跨 reload 存活时用它;agent 事件(agent/)带着活着的 Agent 对象,用来观察或拦截在途工作;capability 事件把策略和适配器挂到某个缝上(fs/、tools/、telemetry/),不用 import 循环本身。

这里有个硬规则值得记:模型可见即已落盘。每个模型请求都必须能从日志重建,新的模型可见输入必须对应 session 事件。会话日志就是模型看到的上下文来源,deriveMessages() 从它投影出历史。这意味着回放、fork、遥测、持久化全部从同一份 append-only 日志派生,而不是从"最近一次请求的快照"。

能力缝

dsh 把可替换的能力叫 seam(缝),一个缝有三个角色:Service Definition 声明接口、Service Provider 实现、Consumer 使用(通常是模型可见工具),三者缺一不可。缝的价值是"换一个 provider 就换掉整个产品":文件系统和子进程 provider 共享同一个执行世界,把两者指向远程沙箱,Bash、PTY、LSP 会跟着一起挪过去,不需要 provider fork。

踩坑

上手有几个坑。一是 developer preview,官方明说还会有破坏性兼容变更,拿来做生产底座前要自己评估节奏。二是概念密度高,profile、bundle、layer、patch、seam 这套术语要先把 architecture 文档读一遍,不然连 --dump-config 的输出都看不懂。三是插件要自己处理 teardown——每个注册都要有 disposer,否则 reload 时状态泄漏。四是不确定 API 是否存在时别硬编,宁可少写一段。

值不值得用

值不值得抄,取决于你要什么。如果你的团队在做 agent 产品、又受够了"改循环就得 fork 框架",dsh 的"无特权核心 + 可逆注册 + 类型化事件"这三点单独拎出来都值得抄;如果只是想要一个开箱即用的编码助手,它现在还是 research 阶段的原型,不如直接等它稳定。方向上,把 agent 的每一块能力都做成可挂载、可替换、可回退的插件,是比"一个大循环硬编码所有工具"更可持续的路线。