ARTICLE · 1346618
dsh 插件开发 03|底座:Cordis 不知道 Agent,却管住所有插件
摘要:第 02 章打印出的配置树,谁把它跑起来?这一篇切开 Cordis 与 dsh 的分工,讲清五个核心概念与时空组合性。
第 02 章末尾留了四个问题:配置行是谁变成运行中的插件的,换个顺序为什么照样跑,某个服务没了依赖它的插件会怎样,卸载之后注册过的东西谁收回去。
四个问题的答案都不在 dsh。
docs/architecture.zh.md 把运行中的 dsh 定义成一棵插件树,由启动时按序叠加的各层组合而成;README.zh.md 说它构建于「一切皆插件」之上,由 Cordis 驱动。第 01 章那张四层图最底下的一层,就是这一篇的主角。
从这一篇起进入「底座」四章。本篇切开机制层与能力层,第 04 章看 ctx 上的服务从哪来,第 05 章拆 Registry 与 Fiber 状态机,第 06 章讲 effect 和可逆注册。
两层
边界比想象中干净:
[能力层] dsh 的插件们 ctx.tools / ctx.llm / ctx.sessions / ctx.agents ctx.agentLoop / ctx.systemPrompt / ctx.shell / ctx.fs agent loop、模型适配器、工具注册表、会话日志、Web UI[机制层] Cordis Context(服务容器)/ Registry(插件注册表) Fiber(插件状态机)/ 事件 / effect(可逆注册) inject(服务依赖)/ Loader(配置行 → 插件树)关系:能力层全部挂在机制层的插件树上; Cordis 只管「谁能加载、何时激活、卸载留下什么」。这张图是本系列按官方架构文档与 docs/cordis-api/ 下的 API 页做的教学拆分,官方没有这样画。能力层的名字来自架构文档的「核心包」表与子系统页,机制层的名字来自 Cordis 的 API 页。
架构文档的 Cordis 一节只有两句话,信息密度却很高。一句是「插件向共享上下文贡献服务、类型化事件和可逆的副作用」,另一句是「产品的每一部分都是插件,包括模型适配器、工具注册表、会话日志,以及 agent loop 本身,因此每个都可以从配置替换」。
第二句是第 01 章讲过的「没有特权内核」。第一句才是这一篇要盯住的:贡献服务、类型化事件、可逆副作用,三样都是机制词汇,不指向任何具体业务。
Cordis 提供的不是功能,是功能进出的规则。
分工落到文档里就是:Cordis 给机制,harness 包给能力。架构文档的「核心包」表把 core/session 对到 ctx.sessions、core/tools 对到 ctx.tools、core/agent-loop 对到 ctx.agentLoop、llm/llm 对到 ctx.llm,这些全是插件,和你要写的那一行配置在加载机制眼里是同一类东西。所以第 01 章引过的那句官方原文,到这里才有了成立的理由:
不存在需要打补丁的特权内核:扩展 dsh 的方式是把插件挂载到其他插件旁边,而各项注册都是副作用,会在其插件卸载时撤销。
撤销是可逆效果,挂载是插件注册表,两样都归 Cordis 管,dsh 只负责把各层叠成一棵树。
元框架
Cordis 自己的仓库第一屏写着 A Meta-Framework of Spatiotemporal Composability.,直译是「时空组合性的元框架」。dsh 的 README 给了论文链接:《A Programming Paradigm for Spatiotemporal Composability》(arXiv:2608.25512)。docs/cordis-primer.zh.md 第一句更朴素:Cordis 是 DeepSeek Harness 底层以 vendor 方式引入的插件框架。vendor 在这里指把依赖源码直接放进仓库,不从 npm 装。
元框架这三个字,落到 API 上很具体。docs/cordis-api/context.zh.md 列出的内置服务位只有 ctx.root、ctx.baseUrl、ctx.events、ctx.logger、ctx.reflect、ctx.registry;服务读写是 ctx.get、ctx.provide、ctx.set、ctx.accessor、ctx.mixin。根上下文、基础 URL、事件总线、日志、反射层、注册表,全是通用机制,没有一个是模型、工具、会话或智能体。同一页第一句还写明:所有服务、事件和生命周期 API 都通过 ctx 访问。
所以标题里「Cordis 不知道 Agent」是本系列按官方 API 归纳的说法,不是官方原话,第 01 章那句「完全不知道 Agent 是什么」的依据在这里补齐。可核对的部分有两处:内置服务位里没有 agent、llm、tool、session 这类名字;ctx.tools、ctx.llm 这些键由 dsh 的插件注册。论文摘要把「自演化的 agent harness」列为这类框架的应用领域之一,也没有说 Cordis 自己提供 Agent 能力。它不管你在上面挂的是 Agent、聊天机器人,还是一套构建流水线。
这里顺带校正一个容易串味的读法:Cordis 不是「dsh 里的一个模块」,它是被 vendor 进仓库的独立框架,dsh 是它的一个应用。这也解释了为什么后面四章能脱离 Web UI、脱离 API Key,单独把 Cordis 跑起来。
五个概念
docs/cordis-primer.zh.md 用一页列完整个底座,标题就叫「五个核心概念」。原文照抄比转述可靠:
插件是实现 Service的对象。可以是带可选inject和apply(ctx)字段的函数,也可以是Service子类,生命周期由 Cordis 挂载到当前上下文。人话:写插件就是写一个有开始也有结束的对象,不是往某个注册函数里塞回调。上下文是服务的容器。一个服务占据一个稳定的 ctx.<key>,如ctx.tools、ctx.llm、ctx.sessions;其他插件通过 key 查找服务,而非导入具体实现。人话:ctx.tools是插槽,谁来实现可以换。通过 inject声明服务依赖。插件声明所需服务后,会等这些服务就绪才启动;加载顺序通过服务依赖表达,而非手动编排启动序列。类型化事件用于通信。服务通过 TypeScript 声明合并注册事件名,再以 emit、waterfall、parallel、serial、bail分发,分别对应监听者观察、包装、并行扇出、按序执行、停在首个 bail 值。注册是可逆的副作用。提示词片段、工具 schema、适配器、提供方和监听器通过 ctx.effect()或ctx.on()安装,reload 和 teardown 时会按预期撤销。
五条不是五个并列的特性,前四条都在给第五条铺路:服务、依赖、事件最终都要落到「卸载时收得回去」上。primer 的「实践规则」还定了服务与事件的分工:工具流水线事件属于 ctx.tools,模型流式输出属于 ctx.llm,实时 agent 协调属于 ctx.agents;拦截和策略优先用事件,直接能力调用优先用服务方法。
ctx.tools | ||
inject | ||
这张表也是「底座四章」的索引:第 04 章拆第二条,第 05 章拆第三条,第 06 章一次拆掉第四、第五条。
时空组合性
README 里那篇论文不是装饰。摘要给了两个正交维度的定义,正好对应读者最常踩的两个坑。
原文写 temporal composability 是「the ability to completely revert a component's side effects upon removal」,spatial composability 是「the ability to declare and reactively manage inter-component dependencies」。落到 Cordis 的 API 上:
时间这一维对应 ctx.effect()返回的 disposer(资源释放函数)。论文叫 revertible effects(可逆效果),每一次上下文变换都带着一个由运行时持有的逆操作;插件卸载时把注册逐一撤销,热替换才不会留下旧监听和旧工具。空间这一维对应 inject。论文叫 reactive coeffects(响应式协效果),每一次上下文变化都会拿组件的依赖声明比对,决定激活还是停用;服务出现就加载、消失就卸载,配置行写在第几行因此不影响加载。
卸载即回卷,依赖即激活。
官方 Cordis 教程第 3 章把「空间」这一维做成了能直接看的实验:greeter.ts 提供服务,consumer.ts 里写 inject = ['greeter']。交换 cordis.yml 两行顺序,输出不变;彻底删掉 greeter.ts,消费方保持 PENDING、不输出任何内容,既不崩溃,也不会只运行一部分。原文写着「inject 并非一次性的启动检查」。运行期间服务消失,依赖插件随之卸载,服务恢复后再次加载。
cordis.yml 删掉提供方 → ctx.greeter 不再有实现 → inject = ['greeter'] 的消费方保持 PENDING,静默无输出 → 把提供方加回来 → 消费方重新加载,不必重启进程同一章给了更贴产品的例子:卸载 dsh-bash-local、换一个 shell 提供方,所有注入 'shell' 的插件都会重新启动并使用新实现。官方还提醒,PENDING 的 fiber 不会让 Node 事件循环保活,组合里没有其他运行项时进程会静默以状态码 0 退出。第 05 章排错会用到这条。
第 02 章说配置行的顺序不带加载语义,答案在空间这一维;第 06 章要讲的热替换为什么安全,答案在时间这一维。这不是把一个漂亮词拆成两半讲故事。论文摘要里 Cordis 的实现就写成两部分:核心库负责 effect tracking 与 coeffect resolution,声明式组件加载器负责配置协调与 HMR(热模块替换)。论文还把 effect context 与 coeffect context 统一成单一 context 类型,称为 context paradigm,落到代码上就是刚才那句「所有服务、事件和生命周期 API 都通过 ctx 访问」。
官方 primer 没有展开这两个词,它只给五个概念和一套实践规则。本篇用论文摘要补上定义,属于本系列的取舍:先让读者知道「时空组合性」指的是两件能验证的事,别把它当成宣传语。
场地
后四章不动 dsh Web UI,也不填 API Key。官方 docs/cordis-tutorial/index.zh.md 给了一个单文件启动器,七章示例都跑在它上面:
git clone https://github.com/deepseek-ai/deepseek-harness.gitcd deepseek-harnesspnpm installmkdir -p tmp/cordis-tutorialcd tmp/cordis-tutorialnode --import tsx ../../vendor/cordis/bin.js启动器会创建根 Context、挂上 Loader,让它从当前目录加载 ./cordis.yml;有哪些插件、怎么配置,全部来自自己写的那份 YAML。--import tsx 让 Node 无需构建步骤就能运行配置指向的 TypeScript 文件,tmp/ 已被 git 忽略,写进去的东西不会进版本控制。官方明说本教程不需要 API 密钥,所有示例均可在无密钥环境中运行。
七章顺序是:你的第一个插件 → 生命周期与 effect → 服务 → 事件 → 配置 → 组合与 HMR → 进入 harness。本系列第 04 到 06 章会借其中「服务」「组合与 HMR」两章做实验,第 15 章用到最后一章。教程首页还提醒:如果目标是给 harness 本身写插件(由 cordis.yml 加载、在 Web UI 中驱动),应该从《第一个 Harness 插件》那篇开始,而不是这个启动器。两者不是一条路,第 17 章会回到前者。
这一章不写 cordis.yml,也不写 apply(ctx)。
现在可以做的动作有两个:把 docs/cordis-primer.zh.md 的五个概念读一遍,对着第 02 章存下的 --dump-config 输出认一认哪些是 ctx.<key> 服务;按上面五条命令把临时目录和单文件启动器跑起来,先不写插件,确认它能从一个空 cordis.yml 启动。做完这两件,第 04 章讲 ctx.get 与 ctx.provide 时,你手上已经有一个能改的场地。
下一篇是 dsh 插件开发 04|底座:ctx 上凭空多出的服务从哪来。
本篇一手出处:github.com/deepseek-ai/deepseek-harness 的 README.md / README.zh.md、docs/architecture.zh.md、docs/cordis-primer.zh.md / cordis-primer.md、docs/cordis-api/context.zh.md、docs/cordis-tutorial/index.zh.md,以及 github.com/cordiverse/cordis 的 packages/core/README.md 与论文 arXiv:2608.25512 摘要页(核实日期 2026-11-12,分支 master)。逐条硬事实、教学归纳与存疑项见同目录 2026-11-12-dsh-03-cordis-facts.md。















































































