上一篇文章,我们从理论的角度,阐述了论文《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内置的示例来加深理解。三、四者关系:分层叠加模型
理解这四个概念,最直观的方式是看它们怎么"叠"在一起:- dsh-base 等 Bundle 按 Profile 列出的顺序逐个应用;
- Profile 自己的 cordis.patch.yml;
- 任意 --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: — 插入:在指定位置新增插件节点;
- - 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(this, this)) // 原型链! 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(远程沙箱)时——这三个 Consumer 会一并被"搬"到远端,无需为它们各自写一份远端专用 fork。这正是 DIP 的红利:抽象稳定,实现可热插拔。- 四个层级(横向):回答"如何把代码和配置组装进系统",决定哪些插件/配置存在于运行时;
- 能力 Seam(纵向):回答"运行时某个能力如何被替换",决定某能力用哪个实现。
一句话总结:四个层级解决"装什么、怎么装、谁能覆盖";Seam 解决"装进去的能力本身如何被替换而不动消费方"。两者正交叠加,才是完整的时空可组合性。十一、写在最后
从一篇论文到一行能启动的代码,中间隔着的不是工程难度,而是一个心智模型落地的过程。这篇想聊的,不是 Cordis 怎么用,而是它这趟从范式到工程、再到"一切皆插件"的演进里,有哪些趋势值得我们停下来想想。我们自己创设的企业级智能体,是基于当前极具影响力的 Pi Agent(pi-mono)范式,走的是"最小核心 + 按需扩展",但 Pi 的扩展锚点仍主要落在"工具与提示"这一层,循环、会话、运行模式本身是被内核固定下来的。DeepSeek Harness 则走得更彻底:不仅工具可扩展,循环、会话、沙箱、UI、调度本身都是插件。它提供 Web UI 与多种运行模式(标准、极简、创造、PTC),dsh 的插件化更彻底、适用面更广。值得反思的是:虽然我们创设的智能体已经拥有了一条能跑的通路,但 dsh 提示了一个更高的目标——把运行时本身也变成可拼装的生态,智能体才真正获得跨场景繁衍的生命力,而不只是一个被精心调校的单一工具。 从"按需扩展"到"扩展一切",这中间的跨度,或许正是下一代 Agent 基础设施该认真掂量的方向。所以与其说这是一篇"解读",不如说是一点不成熟的学习笔记。范式终会过时,但"把变化当成一等公民来设计"这件事,值得我们一直想下去。见贤思齐,与诸君共勉。