乐于分享
好东西不私藏

插件层面的时空可组合性:Cordis 在 DeepSeek Harness 中如何落地

插件层面的时空可组合性:Cordis 在 DeepSeek Harness 中如何落地
上一篇文章,我们从理论的角度,阐述了论文《A Programming Paradigm for Spatiotemporal Composability》的心智:时间维度是可逆效应,空间维度是响应式协效应,二者经由统一的 Context 捏合成一个能跑的系统。
但是论离工程总隔着一层。这一篇,我们换个姿势:以 DeepSeek Harness(dsh)为实例,把 Cordis 的时空可组合性落到插件这一层,看看那些花哨的概念,到底是怎么变成一行行能编译、能启动的代码的。

一、插件层面的时空可组合性

论文里那两个正交维度,落到 dsh 的插件系统,被翻译成了四件非常工程化的事:
  • 空间维度(Spatial):回答"装什么、怎么装"。把一个功能模块分发出去(Bundle),再按场景组装起来(Profile)。谁提供能力、谁消费能力,靠声明式依赖解耦(Seam)。
  • 时间维度(Temporal):回答"运行时怎么变"。同一份代码,在不同时间切片(web 场景 / headless 场景)下表现出不同行为;发布后还能用 Patch 一层层覆盖,用 Overlay 临时顶替。
四层装载机制解决"装什么、怎么装、谁能覆盖";Seam 解决"装进去的能力本身如何被替换而不动消费方"。两者正交叠加,才是完整的时空可组合性。
如果你和笔者一样,看到这些新的概念一脸懵,不要着急,且往下看。

二、四个概念,一句话本质

很多同学第一次看 dsh 的插件体系会晕,其实拆开就四个词:
如果你熟悉Java体系,或者有前端开发经验,基本上已经懂了。
不懂也没关系,先从自顶向下,从关系的角度去继续了解,后续结合dsh内置的示例来加深理解。

三、四者关系:分层叠加模型

理解这四个概念,最直观的方式是看它们怎么"叠"在一起:
加载顺序(从底向上叠加)
  1. dsh-base 等 Bundle 按 Profile 列出的顺序逐个应用;
  2. Profile 自己的 cordis.patch.yml;
  3. Home 级 cordis.patch.yml;
  4. 任意 --patch 命令行 Overlay(最顶层覆盖)。
后加载的层可以覆盖先加载的层,越往上优先级越高。所有层共同作用于同一个"空条目列表",最终合成一棵插件树。
纵向的说完了,接着说横向的相互关系:
纵向的叠加顺序说到这里,顺着这条线,我们自然要问一个问题:这四层里,谁和谁是一伙的?其实把它们两两一对拆开看,逻辑就顺了——前两个(Bundle、Profile)管"装什么",后两个(Patch、Overlay)管"怎么改",而每一对内部又有"持久 vs 临时"的分工。
先看纵向串起来的"装什么"这一对。Bundle 是零件供应商,Profile 是装配线调度员——这是理解整套体系的第一把钥匙。Bundle 负责"卖什么":它是一个可分发的功能模块,把一组能力连同代码和默认配置一起打包出去,它插入到条目列表里的东西,随时可以被上层 Patch 改写。而 Profile 负责"买什么":它是一份纯配置、不含任何代码,只决定要启哪些 Bundle、按什么顺序组装。dsh发行版自带的 web 和 headless 两个模板 Profile,本质上就是两套不同的"采购清单"。一个只管供货,一个只管排产,二者解耦,这正是空间可组合性能成立的前提。
再看"怎么改"这一对。Patch 是落盘的补丁,Overlay 是临时的顶替,区别全在生命周期。Patch 会被持久化写入 cordis.patch.yml,下次启动依然生效,支持 overlay / insert / remove / disable 四种原子操作;而 Overlay 通过命令行 --patch xxx.yml 传入,进程一退出就消失,却拥有整张图里最高的优先级。说白了,Overlay 就是"用命令行传入的一份 Patch",只是不写进磁盘——它天生适合调试、A/B 测试、CI 临时覆盖这类"用完即走"的场景。
把这两对拼起来,四层机制的轮廓就完整了:Bundle 供货、Profile 排产,解决"装什么、怎么装";Patch 落盘改、Overlay 临时顶,解决"谁能覆盖、改完留不留"。
下一节,我们钻进一个 Bundle 内部,看看dsh内部究竟是怎样的一个魔法世界。

四、Bundle:空间组合的载体

Bundle 是一个可分发、可嵌套继承的应用单元:一个 npm 包 + 一份 cordis.patch.yml 声明式描述。
子 Bundle(如 dsh-headless)通过继承复用父 Bundle(dsh-base)的全部基础插件,只写增量 Patch。这恰是空间维度的"组合"——不是复制,而是引用与叠加。时间可组合性里那句"修改可逆、可叠加",在工程上靠的就是这种层层叠加、每层都可被上层再覆盖的结构。
dsh内置的 dsh-base 这样的 Bundle,内部大致切成六层:

五、Profile:空间切换的入口

Profile 是"使用场景"的集合,保存在 ~/.dsh/profiles//。它是同一份代码在不同空间切片下表现不同行为的开关。
dsh --profile web 启动 Web UI 场景(含 Host + HTTP + 浏览器插件)dsh --profile headless 启动无头任务场景(一次性 Agent 驱动)
官方提供 Web(含 Host + HTTP + 浏览器插件)与 Headless(一次性 Agent 驱动)两种场景。这正是论文"时间可组合性"的具象:运行时切换而非重启重建。而用户 Profile 层的 cordis.patch.yml(优先级高于 Bundle),承载更强的机器适配偏好——场景级定制可以覆盖"出厂默认"。

六、Patch:声明式增量修改的核心原语

Patch 是 Cordis 的核心扩展机制,支持四种操作——这恰是"时间可组合性"里"修改可逆、可叠加"的工程表达:
  • - id: xxx — 覆盖:修改已有条目的配置或状态;
  • - insert: — 插入:在指定位置新增插件节点;
  • - remove: — 移除:删除不需要的插件;
  • - disabled: true — 禁用:保留定义但不激活。
# 示例:headless bundle 覆盖 base bundle 的行为id: system-prompt          # 覆盖:修改 system prompt 配置  config:    persona: "You are a coding agent powered by {{model}}..."id: hmr                    # 覆盖:禁用 HMR 热更新(无头模式不需要)  disabled: true- insert:                    # 插入:新增 headless 专用插件    - id: code-runtime      name: '@deepseek-ai/dsh-code-runtime-worker-thread'    - id: headless-runner      name: '@deepseek-ai/dsh-headless'      inject: [headlessStartup]      config:        task: !!js ctx.headlessStartup.task
覆盖、插入、移除、禁用——四种操作都是显式的、可被上层再覆盖的。删掉一份 Patch,系统回到上一层状态。这呼应了上篇讲的"操作账本":每个改动的逆操作都存在。

七、代码级解析:一个插件的全貌

把一个 Bundle 拆开看,一个插件就是 name + inject + Config + apply(ctx, config) 四件套。Cordis 约定的入口签名,把"依赖声明"与"运行时消费"彻底解耦。
// packages/bundle/headless/src/index.tsexport const name = 'headless-runner'                 // 全局唯一标识export const inject = ['agentDefaultModel''agents''sessions']  // 声明式依赖export interface Config { task: string }export const Config = z.object({ task: z.string().required() })    // Zod 校验export function apply(ctx: Context, config: Config): void {  const exit = ctx.get('appExit')                    // 从 Context 读退出回调  void ctx.get('loader')?.await().then(() => {       // 等兄弟插件就绪(时间同步点)    const agents = ctx.get('agents')                 // 跨插件通信:不硬编码引用    const sessions = ctx.get('sessions')    run(ctx, config.task, io)                        // 驱动任务到静止态  })}
三个值得品味的细节:
  • inject 数组:声明我需要什么,不关心谁提供。Cordis 挂载前自动解析拓扑序——这就是响应式协效应。
  • ctx.get():从全局服务商店读取——跨插件通信无需 import,与 Spring 的 getBean() 异曲同工。
  • loader.await():时间同步点,等所有插件就绪再跑业务——这是时间维度的"会合"。

八、Context 核心:原型链继承 + 服务隔离

四层装载机制之所以能"叠加而不冲突",根子在 Context 的实现:每个插件挂载时拿到一个子 Context,通过原型链继承父级、又能独立隔离。
// vendor/cordis/src/context.tsexport class Context {  /** 创建子上下文(原型链继承,不修改父级) */  extend(meta = {}): this {    const self = Object.create(getTraceable(thisthis))  // 原型链!    for (const prop of Reflect.ownKeys(meta))      Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)    return self  }  /** 为指定服务创建隔离作用域 */  isolate(name: string, label?: symbol): this {    const shadow = Object.create(this[symbols.isolate])    shadow[name] = label ?? Symbol(name)    return this.extend({ [symbols.isolate]: shadow })  }  /** 为服务添加拦截配置(合并到子插件的 config) */  intercept<K extends InjectKey>(name: K, config): this {    const intercept = Object.create(this[symbols.intercept])    intercept[name] = config    return this.extend({ [symbols.intercept]: intercept })  }}
  • 空间语义:extend() 通过原型链实现作用域继承;isolate() 允许同一服务的不同实现共存——多租户沙箱的物理基础。
  • 时间语义:每次 extend/isolate/intercept 都创建新的上下文快照,支持回溯与并发——这正是"时间可组合"的底层支撑。

九、加载流程:时空交织的一次启动

用户敲下 dsh --profile headless "写个排序算法",空间(Bundle 组装)与时间(Profile 切换 + Patch 叠加)两条线索在这一口气里完成:
用户 CLI ── dsh --profile headless "写个排序算法"  │  ├─① app-boot 读取 ~/.dsh/profiles/headless/  ├─② plugin-loader 层叠加载 cordis.patch.yml  ├─③ 先挂载 dsh-base(父 Bundle,~60+ 插件)  │       └─ ctx.plugin() 注册到 Cordis Context  ├─④ 再叠加 headless patch(overlay / insert / disable  ├─⑤ Cordis 拓扑排序 → 并行挂载  ├─⑥ loader.await() 全量就绪  ├─⑦ headless-runner.apply(ctx, config)  └─⑧ agents.create() → agent.followup(task) → 输出 + exit(code)
时空交织:空间维度决定"哪些插件存在于运行时"(Bundle/Profile),时间维度决定"它们如何随场景与补丁演化"(Patch/Overlay)。两者在同一棵 Context 树上汇合。

十、Seam:插件内部的"可替换能力单元"

四层机制解决"装什么";但装进去的某个能力如何被替换而不动消费方,靠 Seam(能力接缝)这个三元组:
Seam 就是依赖倒置原则(DIP)+ SPI 扩展点的工程化命名。消费方只依赖「接口定义」这一抽象,从不依赖具体实现;具体实现由「提供方」在装配期注入。在 Spring 语境里,这等价于:
换一个 Provider 实现 = 换一个 @Bean 实现类,整条调用链路自动跟随,消费方零改动。
官方经典例子:一个 Seam 同时驱动三个 Consumer
文件系统(ctx.fs)与进程(ctx.subprocess)共享同一个"执行世界"。当把提供方从 fs-local(本地)换成 fs-remote-sandbox(远程沙箱)时——
tool-bash(Bash 命令)
tool-pty(PTY 终端)
tool-lsp(语言服务)
这三个 Consumer 会一并被"搬"到远端,无需为它们各自写一份远端专用 fork。这正是 DIP 的红利:抽象稳定,实现可热插拔。
Seam 与四个层级,是正交的两个维度:
  • 四个层级(横向):回答"如何把代码和配置组装进系统",决定哪些插件/配置存在于运行时;
  • 能力 Seam(纵向):回答"运行时某个能力如何被替换",决定某能力用哪个实现。
一句话总结:四个层级解决"装什么、怎么装、谁能覆盖";Seam 解决"装进去的能力本身如何被替换而不动消费方"。两者正交叠加,才是完整的时空可组合性。

十一、写在最后

从一篇论文到一行能启动的代码,中间隔着的不是工程难度,而是一个心智模型落地的过程。这篇想聊的,不是 Cordis 怎么用,而是它这趟从范式到工程、再到"一切皆插件"的演进里,有哪些趋势值得我们停下来想想。
我们自己创设的企业级智能体,是基于当前极具影响力的 Pi Agent(pi-mono)范式,走的是"最小核心 + 按需扩展",但 Pi 的扩展锚点仍主要落在"工具与提示"这一层,循环、会话、运行模式本身是被内核固定下来的。
DeepSeek Harness 则走得更彻底:不仅工具可扩展,循环、会话、沙箱、UI、调度本身都是插件。它提供 Web UI 与多种运行模式(标准、极简、创造、PTC),dsh 的插件化更彻底、适用面更广。
值得反思的是:虽然我们创设的智能体已经拥有了一条能跑的通路,但 dsh 提示了一个更高的目标——把运行时本身也变成可拼装的生态,智能体才真正获得跨场景繁衍的生命力,而不只是一个被精心调校的单一工具。 从"按需扩展"到"扩展一切",这中间的跨度,或许正是下一代 Agent 基础设施该认真掂量的方向。
所以与其说这是一篇"解读",不如说是一点不成熟的学习笔记。范式终会过时,但"把变化当成一等公民来设计"这件事,值得我们一直想下去。见贤思齐,与诸君共勉。