DeepSeek Harness 的「一切皆插件」:Cordis 到底承担了什么

2026-08-16

先把限定说在前面:deepseek-ai/deepseek-harness 这个仓库建立于 2026-08-13,我们采集快照是 2026-08-16,前后只差三天。根 package.jsonversion0.1.0-rc.5,GitHub 上一个 Release 都没有,README.md:9-11 的 Developer preview 小节用全大写写着 THERE WILL BE COMPATIBILITY-BREAKING CHANGES.。下面提到的每一个 API 名、字段名、默认值,都随时可能变。

这句话不是宣传语,是仓库里反复写了四遍的约束

Everything is a Plugin. 出现在仓库的 GitHub description 原文里。同一句话在仓库内部至少还有三处:

  • README.md:7 写 dsh 使用 everything is a plugin 的架构,由 Cordis 驱动,并给出 Cordis 仓库与一篇设计论文的链接(论文标题 A Programming Paradigm for Spatiotemporal Composability)。这两个链接我们没有打开过。
  • AGENTS.md:3 写 DeepSeek Harness 是建立在 vendored Cordis 之上的插件式 agent harness。
  • docs/architecture.md:11 说得最具体:产品的每一部分都是插件,包括模型适配器、工具注册表、会话日志、以及 agent loop 本身,所以每一部分都可以从配置里替换。紧接着 docs/architecture.md:13 补了一句更硬的话——「没有一个特权 core 可以打补丁」(原文 There is no privileged core to patch)。

「agent loop 本身也是插件」和「没有特权 core」这两句,才是这个架构真正的主张。它意味着仓库里不存在一个「主程序」文件,你打开它就能顺着 main 往下读。

最能说明问题的一组数字

packages/ 目录底下有 49 个分组目录——注意不是 49 个包。真实的包是两层 glob,packages/<group>/<pkg>/package.json 一共 219 个(截至 2026-08-16 快照 47f9438,用 Python 遍历数出)。写成「49 个包」就错了。

真正有意思的是依赖统计:遍历这 219 个包的 package.json,合并 dependencies / peerDependencies / devDependencies 之后,声明了 @deepseek-ai/cordis 的是 219 个,也就是全部。另有 108 个声明了 @deepseek-ai/schemasteryAGENTS.md:100 把这条写成了约定:@deepseek-ai/cordis 是每个 harness 包的 peerDependency(外加 dev)。翻开 packages/core/tools/package.json 就能看到,@deepseek-ai/cordis 同时出现在 peerDependenciesdevDependencies 里,取值都是 workspace:^

「一切皆插件」在这里不是形容词,是一条 219/219 的实读事实。

Cordis 被整个搬进了仓库里

docs/cordis-primer.md:5 把 Cordis 定义为「DeepSeek Harness 底下 vendored 的插件框架」。vendor/README.md:3 讲了为什么要 vendored:这里存的是 Cordis 框架及其基础库的源码级拷贝,不走 npm 依赖,原文的理由是让 harness 完整拥有自己的框架层(可审计、可打补丁、可锁定)。所有 vendored 包都改名进了 @deepseek-ai scope,cordis 变成 @deepseek-ai/cordis@cordisjs/plugin-<x> 变成 @deepseek-ai/cordis-plugin-<x>

框架核心其实很小。vendor/cordis/src/ 下九个文件的行数是:context.ts 146、events.ts 352、fiber.ts 754、index.ts 14、logger.ts 270、reflect.ts 418、registry.ts 337、service.ts 115、utils.ts 287。整个 vendor/cordis/ 是 9 个文件、2,693 行。相比之下 packages/ 下有 1,942 个 TS 文件、429,397 行。两千七百行的内核托着四十多万行的产品代码,这就是「没有特权 core」的字面形状。

docs/cordis-primer.md:7-13 把 Cordis 归纳成五条:插件是一个实现 Service 的对象;context 是服务的仓库;用 inject 声明服务依赖;用 Typed Events 通信;注册是可逆的 effect。下面用一个真实的服务把这五条串一遍。

顺着 ToolRuntime 走一遍

packages/core/tools/src/index.ts:787export class ToolRuntime extends Service。这一个类基本上覆盖了上面五条里的四条:

第 788 行 static inject = ['systemPrompt']——这是依赖声明。docs/cordis-tutorial/03-services.md:59 讲了它的语义:在 inject 列出的服务全部存在之前,Cordis 让这个插件停在 PENDING 状态。07-into-the-harness.md:83 把这条落到了实际后果上:因为工具要向系统提示词贡献 schema,所以组合里必须一并列出 @deepseek-ai/dsh-system-prompt,否则 tools 插件就停在 PENDING。

第 790-793 行是配置 schema,mode 默认 'native'(可选 native / code / both),maxParallelSubCalls 默认 10。这里的 z 是第 8 行导入的 @deepseek-ai/schemastery,不是 zod。要强调一句:这两个是配置里的默认值,不是任何关于实际表现的承诺,不能拿来推算并发或吞吐。

第 827 行构造函数第一行 super(ctx, 'tools')——服务名就此认领,别的插件从此可以用 ctx.tools 拿到它,而不需要 import 具体实现。vendor/cordis/src/service.ts:42 的构造函数 JSDoc 写明它调用 ctx.reflect.provide(name, this, ...),所以服务会在所属 fiber 卸载时自动注销。第 832 行紧接着 ctx.systemPrompt.tools(...),向系统提示词接线。

第 1037 行 register(definition),其 JSDoc(index.ts:1031-1036)写的是「returns the exact disposer that unregisters the tool」——注册返回卸载函数,这就是第五条「注册是可逆的 effect」。docs/cordis-tutorial/02-lifecycle-and-effects.md:86-90 说得更直接:ctx.on(event, listener)ctx.plugin(child)、service 注册本来就都是 effect,harness 的 ctx.tools.register(...) 也会把返回的 disposer 挂到调用它的插件上。

这一路上还有两处硬边界值得记:index.ts:1054-1056 把名字 run_code 无条件保留,注册或遮蔽它会抛错;index.ts:1071restrict(filter) 要求传入一个 scoped context,否则抛错,源码给的理由是「一个 context-global 的限制会遮蔽每一个 agent」。

我们用正则从源码里数出,packages/ 下(排除测试目录)有 66 处 class X extends Service、67 个不重复的服务名,llmsessionscompactionsandboxsubagentstokenMeter 都在其中。这 67 个是我们自己数的,不是任何文档的官方口径——仓库里 docs/subsystems/*.md 有生成的 cordis-surface 区块,我们没有去查它给的数字。

生命周期:可逆是有代价的

FiberState 定义在 vendor/cordis/src/fiber.ts:147-154,源码里的书写顺序是 PENDING, LOADING, ACTIVE, FAILED, DISPOSED, UNLOADING;tutorial 第 2 章 02-lifecycle-and-effects.md:73 画的状态机是 PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED,外加一条通向 FAILED 的分支。

拆卸有两个容易踩的语义。一是 02-lifecycle-and-effects.md:94 写明:disposer 按注册的逆序开始,但多个异步 disposer 是并发跑的;要求有序就得放进同一个 disposer 里 await。二是 ctx.effect() 有状态门槛,fiber.ts:419-422assertActive(),再判断若当前是 UNLOADING 就抛 CordisError('INACTIVE_EFFECT')。顺带一提,CordisError.Codefiber.ts:171-173 实读只有这一个码。

这套范式最该知道的,是三种「静默」

「没有特权 core」的另一面是:很多故障不会崩,只会安静地什么都不发生。仓库文档自己把这三处都写出来了:

  1. 缺 provider 不崩。 03-services.md:72 写:删掉 provider 之后,consumer 一直 PENDING、什么都不打印,不崩溃;而且一个 PENDING 的 fiber 不会让 Node 的事件循环保持存活,所以没有别的东西在跑时,整个组合会静默地 exit 0。
  2. 少挂一个基础插件,功能就永远等着。 06-composition-and-hmr.md:42 写:HMR 注入了 timer 服务用于防抖,没有 @deepseek-ai/cordis-plugin-timer 它会永远静默地停在 PENDING。
  3. waterfall 监听器忘了 next(),会吞掉下游所有人的默认行为。 04-events.md:138 把这条定成了本仓库的常驻规矩:只观察或只加注解的 waterfall 监听器必须调 next()AGENTS.md:106 又写了一遍。04-events.md:136 的示例演示得很清楚:监听器 2 不调 next(),传给 ctx.waterfall 的最内层默认实现就永远不会跑。

还有一条属于配置层的坑:06-composition-and-hmr.md:59 写,loader 按 id 对条目做 diff,而没有 id 的条目每次读取都会拿到新生成的 id,于是任何一次配置文件编辑都被算作「删除 + 新增」而重新挂载。

顺手记一处口径不一致

docs/cordis-primer.md:17 原文写「Every event can have one of the following dispatch mode」,其后 :19-24 的表只列了 emit / waterfall / parallel / serial 四种,没有 bail。而 docs/cordis-tutorial/04-events.md:82 明写这是五种派发模式之一,:84-90 的表列了含 bail 的五行;vendor/cordis/src/events.ts:32DispatchMode 类型实读也是五个成员:'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'。两处不一致,以我们实读的源码状态为准。至于原因,我们不作推断。

落到怎么读这个仓库

docs/architecture.md:17-27 描述了 boot 期的分层组合:profile 是存在 Harness home 里的具名组合,列出它叠的 bundle;bundle 是 Cordis 配置行及其挂载代码的分发格式;二者在自己 package.jsondsh 字段里自我声明。叠加顺序是先按 profile 列出的顺序叠每个 bundle,然后是 profile 的 cordis.patch.yml,再是 home 级的那份,最后是 --patch overlay,一个补丁按 id 命中一行并替换其整个 config,或插入新行。

所以想知道 dsh 装配好之后到底跑了哪些插件,入口不是某个 main.ts,而是那些 YAML。packages/bundle/base/cordis.patch.yml 有 451 行、78 个 - id: 条目,它开头的注释(:1-13)自己写明:补丁替换目标行的整个 config 而不是合并,而且「行的顺序不携带加载语义」,激活由服务可用性驱动,分组只是给读者看的。想看更小的样本,examples/headless-agent/cordis.yml 165 行、25 条插件条目。

docs/architecture.md:32 还给了一条查看本机实际组合树的命令 dsh --profile web --dump-config——我们没有运行过它,这里只记录文档写了这么一行。

延伸阅读


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。 dsh 会在本机执行工具、运行 shell 与子进程,安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。