DeepSeek Harness 的 Cordis 插件架构

DeepSeek Harness 把模型、工具、会话、沙箱、文件系统、Agent 循环、调度和 UI 都交给插件。

系统没有一块承载业务能力的固定内核,Cordis 只维护上下文、服务注册、事件分发、依赖激活和副作用回收;Agent 能做什么,由启动时挂入的一棵插件树决定。

传统的插件有注册容易,撤销困难;初始化容易,失败回滚困难;全局单例容易,局部覆盖困难等问题,而 Cordis 把这些困难收进了运行时语义,Harness 再用 session、agent 和 preset 的业务约束补齐它。

今天聊了一下 DeepSeek Harness 的核心 Cordis 架构以及 Cordis 所谓「时空可组合」,究竟怎样落到代码里,又为何适合 Agent Harness 这类持续变化的运行时。

插件的历史

插件架构的历史并不短。Eclipse 在二十多年前已经把 IDE 拆成插件、扩展点和扩展,宿主声明可扩展位置,其他插件通过清单贡献菜单、编辑器和处理逻辑。

OSGi 更进一步,给 bundle 配置安装、启动、停止、更新和卸载生命周期,再通过共享服务注册表完成发布、查找和绑定。服务离开注册表时,依赖方必须跟着处理动态变化。

今天看 Cordis,能找到这些设计的清晰痕迹:动态服务、生命周期、注册表、声明式依赖、事件通知都已有成熟先例。

Cordis 没有发明插件系统。

它简化并重新组合了这些概念,让它们适合 TypeScript 应用内的细粒度组装。OSGi 的部署单元是 bundle,模块、生命周期、服务和安全各有一层;Cordis 的执行单元是 Fiber,一个函数或一个 Service 子类就能成为插件。Eclipse 的扩展点通常由宿主定义结构化清单,Cordis 让服务和类型化事件直接成为扩展面。Agent 运行时里的工具、提示词片段、模型适配器和审批策略变化频繁,如果每次扩展都要引入重量级模块边界,团队很快会绕过框架,重新写回几个全局数组。

Cordis 官方仓库把自己定位为「Meta-Framework of Spatiotemporal Composability」。

从源码看,「元框架」表示它不规定 Agent、Web 服务或机器人该有什么组件,只提供构造框架所需的基础语义。DeepSeek Harness 在其上定义 sessions、tools、llm、agents 等服务,又用这些服务组装产品。Cordis 与 Harness 的关系更接近运行时机制和领域框架,而非通用插件平台与插件集合。

DeepSeek Harness 还把 Cordis 源码直接放进 vendor/,固定在明确的上游提交,并将包名重映射到 @deepseek-ai 命名空间。

项目维护的本地修改包含 Fiber 重入卸载加固、配置更新事务、HMR 精确监听、延迟配置解析等。这增加了维护成本,也换来了框架层的可审计性。Agent 能执行 Shell、修改文件、访问网络,插件生命周期出错会留下进程、监听器、终端模式或权限状态。

此时依赖一个本地的代码白盒,会让人放心一些。

调用链路图

简单的调用图如下所示:

添加图片注释,不超过 140 字(可选)

最小内核

Cordis 的根对象是 Context。它同时承担依赖容器、插件挂载入口和事件入口。

服务通过稳定名称出现在 ctx 上,例如 ctx.tools、ctx.llm、ctx.sessions。插件声明 inject 后,Cordis 只有在所需服务可用时才激活它;服务消失,依赖插件也会进入卸载或等待状态。配置文件中条目的先后顺序因此不承担启动顺序,依赖关系才承担。

这种处理解决了传统插件系统的第一个顽疾:隐含初始化顺序。

常见实现会遍历插件数组,依次调用 init。当工具依赖文件系统、文件系统依赖沙箱、UI 又依赖会话时,数组顺序就变成一套没有类型、没有诊断的依赖图。后来插入一个插件,顺序约束可能跨越几十个文件。

Cordis 把需求写进 inject,Fiber 会为每项依赖保存当前实现;缺少依赖时保持 PENDING,依赖齐备后进入 LOADING 和 ACTIVE。启动失败则进入 FAILED。状态机至少让故障有了准确位置。

Context 还是一个代理对象。

插件直接读取 ctx.tools 时,反射层会检查它是否声明过依赖,并沿 Fiber 父链解析实现。未声明就读取会抛错;声明了但当前上下文不可用,也会抛出另一类错误。这种约束防止依赖藏在任意函数深处。源码里依然提供 ctx.get(name) 读取可选服务,区别在于调用方显式接受服务可能不存在。

这里的「服务定义、服务提供方、消费方」三种角色被完整建模。比如文件系统能力不能只写一个接口,也不能只挂一个本地实现。定义方稳定调用协议,提供方接入本地目录或远程沙箱,消费方把能力变成模型可见工具。Harness 把这组关系称为 capability seam。替换沙箱时,Shell、PTY、LSP 只要依赖同一能力面,就能整体迁移到新的执行环境,消费方无需知道提供方运行在本机还是远端。

「一切皆插件」有一个前提是:一切产品能力皆由插件贡献,Cordis 自身仍保留插件得以存在的机制。上下文代理、Fiber 状态机、服务存储、事件总线和 Loader 属于元层。

这并不是系统里没有内核,更准确的说,应该是,内核不拥有模型、工具和循环等产品特权。

空间组合

同一个进程里,服务名称必须稳定,实例又不能全局唯一。两个 Agent 可能选择不同模型、不同工具集、不同 persona 和不同沙箱。如果 ctx.llm 永远指向一个全局对象,插件替换只发生在进程级,无法满足多会话并存。

Cordis 的 Context.isolate 为指定服务名创建一个 realm 标签。服务注册和查找都以该标签定位,同名服务可以在不同子上下文里各自存在。两个 isolate 调用传入同一标签时会加入同一 realm;使用新标签时彼此隔离。子上下文通过原型继承父上下文,隔离映射按层遮蔽,因此局部配置不需要复制整棵容器。

这解释了「空间可组合」的第一层:组合具有位置。插件挂在哪个上下文,决定它提供的服务、注册的监听器和持有的副作用在哪个范围可见。传统依赖注入容器也有 singleton、request、session 等 scope,Cordis 的差异在于作用域与插件树、事件过滤、资源所有权使用同一个 Context 表达。开发者无需在四套 API 之间同步身份。

DeepSeek Harness 又增加了 dsh-scope。它用不透明对象作为 scope key,维护父子关系,并创建带路由身份的事件 receiver。注册视图沿父链向下继承:Agent 能看到所属 preset 的提示词和工具。事件则沿链向上接纳:preset 级监听器能收到其下 Agent 的事件,兄弟 Agent 互不串线。这是第二层空间结构,解决的是领域身份,而非单个服务名的 realm。

为何需要两层?isolate 处理「哪个 tools 服务实例」,dsh-scope 处理「同一工具注册表里,哪些注册项对当前 Agent 可见」。前者适合替换提供方,后者适合对注册内容分层。把所有差异都做成独立服务实例,会放大内存和初始化成本;把所有差异都塞进一个全局注册表,又会让过滤规则散落在调用点。这两层模型把实例隔离和内容路由分开了。

Agent preset 是空间组合的完整应用。一个 preset 的 Cordis 配置会挂在一个长期存在的 scope 下,Agent 创建时把自己的 scope key 绑定到该 preset。相同 preset 的并发首次使用通过 single-flight 共享一次挂载,后续 Agent 复用这份组合。文件变化后,新会话加入新一代组合,已有会话保留原来的代际。这个策略避免会话运行中途突然更换工具或提示词,代价是旧代际要保留到整棵运行时退出,配置频繁变化时内存会按代际增长。

源码还专门审计 preset 子树是否把服务发布到了 root realm。发生这种泄漏时,第二个会话挂载同一 preset 会与第一个冲突,所谓会话级组合也会退化成进程全局状态。DeepSeek Harness 在发布 Agent 前拒绝这种配置。空间隔离若只靠约定,迟早会被一个没有 isolate 的 provider 穿透;运行时审计比文档警告可靠。

这套空间模型存在认知成本。插件作者要同时理解 Context 父链、服务 realm、业务 scope 父链和事件过滤方向(当然,在当前 Vibe Coding 盛行的时代,也可以作者不理解,直接让 AI 来搞)。

在认知不清楚的时候,常见的错误往往表现为某项能力「看不见」或意外泄漏,类型系统无法证明运行时挂载位置。DeepSeek Harness 用 preset 挂载审计、包级 invariant 和真实组合测试降低风险,却没有消除模型复杂度。团队若只需要单进程单 Agent,直接引入整套空间语义会显得过重。

时间组合

插件能挂载只是静态组合。运行期间服务上线、配置改变、插件失败或上下文销毁,系统还要回到一致状态。Cordis 用 Fiber 和 effect 管理这条时间轴。

每次插件应用都会产生一个 Fiber。Fiber 记录父上下文、原始配置、解析后配置、依赖实现快照、生命周期状态和 disposables。插件调用 ctx.effect() 注册副作用,effect 的执行结果返回 disposer。事件监听、服务提供、子插件和访问器最终都进入这套所有权体系。Fiber 卸载时按注册的逆序执行清理,并等待异步清理达到静止状态。

逆序本身不是一个特别要讲的事情,因为这是必须的。

若插件先启动子进程,再注册输出监听,最后暴露服务,销毁时应先撤销服务,停止新请求,再移除监听,最后结束进程。资源创建顺序的反向通常就是依赖安全的拆卸顺序。

传统插件常给出一个 deactivate() 钩子,把所有清理责任推给作者;漏掉一个定时器或事件监听,热加载几次便出现重复执行。Cordis 让每次注册同时产生撤销动作,框架持有所有权。

DeepSeek Harness 的 vendor 版本进一步处理了重入场景:effect 在执行 setup 前先登记所有者包装;插件发布事件时,观察者可能同步卸载它;异步 cleanup 已经开始后,其他调用者仍能等待同一次清理;Fiber 处于 UNLOADING 时拒绝创建新 effect。这些代码看起来全是繁琐的边界处理,但为了保证生命周期并发的安全,一个都不能少。否则插件的热卸载就只是个玩具,没法真正落地。

依赖变化也属于时间组合。某个服务被提供后,反射层通知所有声明该依赖的 Fiber 重新检查;条件满足便激活。服务撤销后,依赖方会卸载并回到等待。OSGi 早已采用动态服务注册表,Cordis 的新意主要在于把动态依赖、作用域上下文和 effect 所有权压进一个很小的进程内模型。代价同样继承自 OSGi:任何持有服务引用越过生命周期的代码,都可能在提供方撤销后继续调用过期对象。Cordis 保存加载时的实现快照,却无法替业务代码管理逃逸引用。

Loader 把时间组合延伸到配置。DeepSeek Harness 的 profile 由多层 bundle patch 叠加:基础 bundle、模式 bundle、profile patch、home patch、命令行 overlay。patch 按 id 定位条目,配置采用整块替换。整块替换要求用户重述保留字段,使用上稍显笨重,却避免深度合并规则在数组、表达式和删除语义上制造歧义。

配置热更新时,Loader 先导入候选插件,再卸载旧实例并应用候选;候选失败会恢复旧插件或旧配置。Group 对一批子条目并发启动,收集全部结果,只要一项失败就删除新增项并重建旧配置。Include 读取候选文件、在副本上应用 patch、完成树协调后才提交缓存。用户 patch 语法错误或新插件启动失败时,最后一棵可用树继续运行。

这已经接近配置事务,却不能等同于数据库事务。

插件 effect 可能调用外部 API、创建远端资源、发送消息;disposer 只能做补偿,无法保证外部世界回滚。Loader 能保证自己管理的树和服务注册恢复,无法撤回所有不可逆副作用。插件开发规范仍要限制初始化期行为,把外部写入延迟到真正的业务请求,或者设计幂等键和补偿路径。

时间组合还带来可观的测试面积。挂载成功只是第一条路径,还要覆盖依赖晚到、依赖撤销、初始化失败、卸载重入、异步清理、热更新失败、回滚再次失败。DeepSeek Harness 要求注册项证明 disposal,产品可见插件还要通过真实 Loader 组合测试。这个成本无法靠框架消失,只能被框架集中暴露。相比线上出现幽灵监听器和半更新状态,还是愿意支付这部分测试费用。

事件契约

插件之间只靠服务调用,会把所有扩展都变成接口方法。宿主每增加一项策略,就要修改服务定义。Cordis 同时提供类型化事件,并区分 emit、parallel、serial、bail 和 waterfall。

DeepSeek Harness 最依赖的是 waterfall。它的监听器拿到 next,调用后把控制权交给下一层,不调用就截断链条。模型请求、工具执行和轮次控制都能被插件包裹。审批策略可以在工具执行前拒绝,重试插件可以包围模型流,日志插件可以观察前后状态。它与 Koa 一类中间件链相似,但事件名和 Context 过滤让同一个分发器覆盖多个能力域。

waterfall 也最容易出错。一个只想记录日志的监听器忘记调用 next(),整条能力链便被短路。DeepSeek Harness 把「必须调用 next()」写进项目级规则,并在事件文档里记录 dispatch mode。类型能保证参数,却很难保证 continuation 一定执行。代码评审和组合测试仍是主要防线。

事件的持久性也被刻意分层。agent/* 和 tools/* 事件用于活跃运行时的拦截;会话事件追加到日志,承担恢复、fork、UI 回放和模型历史投影。DeepSeek Harness 规定「模型可见即已记录」:进入模型请求的输入必须能从 session log 重建。插件若偷偷修改 prompt 却不产生会话事实,重放结果会漂移,问题也无法审计。

这条约束说明,一切皆插件并不意味着一切皆事件。直接能力调用放进 Service,策略和拦截放进实时事件,需要持久化的事实进入 session log。三类通信各自承担调用、扩展和历史。将它们混成一个全局 event bus,短期代码更少,长期会失去时序语义和数据权威。

Agent 适配

Agent Harness 比普通 Web 服务更需要动态组合。模型供应商会变,工具权限随工作区变化,子 Agent 的执行环境可能与父 Agent 不同,UI 还要消费同一份流式过程。如果主循环直接 import 每个能力,任何替换都会改循环;循环逐渐成为依赖最多、风险最高的文件。

DeepSeek Harness 的默认 agent loop 仍然存在,但它也是一个服务提供方。循环读取 session log 生成历史,组装插件贡献的 prompt 和 tool schema,通过 agent/request 进入模型适配器,再经过工具流水线记录结果。扩展点围绕步骤、请求、工具和停止阶段布置。新功能通常挂到这些事件或注册表,项目规则甚至要求修改 agent loop 时同步更新架构文档。

我赞成这种限制。循环承担控制流,频繁承载产品策略后会迅速腐化。压缩上下文、重试、审批、工具超时、计划模式、子 Agent 调度都可以有自己的生命周期与配置。它们进入循环的接口必须少且稳定。插件化不会自动获得解耦,真正起作用的是 DeepSeek Harness 为能力选择了明确的 seam,并拒绝消费方特有逻辑污染定义层。

UI 作为插件也有实际意义。Web 和 headless profile 在共享 base bundle 上增加不同条目,后者可以完全不启动服务器。UI 驱动 ctx.agents,订阅 session/event 渲染状态,没必要成为循环的一部分。服务端、CLI、ACP 和浏览器界面可以共享会话及 Agent 语义,同时保留自己的传输和展示逻辑。

插件树还提供自省基础。Fiber 保留名称、状态、依赖和 effect 元数据,DeepSeek Harness 能实现查看、挂载、卸载自身插件的能力。对 Agent 而言,这比普通应用更敏感:模型可以通过工具改变运行时,错误配置可能直接扩大权限。源码中的 sandbox 和 approval 仍是独立能力,插件架构没有天然安全性。自修改必须受工具权限、配置校验和作用域审计约束。

架构代价

第一项代价是启动与运行时开销。每个插件产生 Fiber,服务访问经过 Proxy 和反射解析,事件分发要执行作用域过滤,注册项还要保存 disposer 与诊断元数据。对于 LLM Agent,单次模型请求通常以百毫秒到秒计,这些 JavaScript 调度开销很难成为主要瓶颈;高频 token chunk、文件扫描或终端字节流若全部穿过通用事件总线,成本会被放大。DeepSeek Harness 把流式模型输出定义为能力语义,但大量数据处理仍应留在具体 provider 内,事件只承载必要扩展点。

第二项代价是故障面扩大。插件初始化可以同步抛错、异步拒绝、等待缺失服务,也可能在 disposer 中失败。Group 并发启动缩短时间,却会带来多个兄弟同时失败的 AggregateError。DeepSeek Harness 的 boot 会等待 Loader 结算,审计所有启用条目是否加载与激活,失败时先 dispose 部分构造的上下文,再退出。少做任何一步,都可能让终端停留在 raw mode,或者让后台 watcher 继续运行。

第三项代价是配置成为编程接口。cordis.yml 支持 !!js 表达式,条目可以按环境禁用,配置会在依赖激活后求值。灵活性很高,静态分析能力随之下降。表达式读取服务时,求值时机与上下文位置都会影响结果。DeepSeek Harness 只允许 config 和 disabled 使用插值,其他元数据保持字面量,并让错误尽早暴露。这仍要求运维人员理解插件依赖和 patch 覆盖语义,配置文件已经超出普通 YAML 参数表的复杂度。

第四项代价是生态兼容。Cordis 的服务名和事件类型提供了源码级协议,插件版本升级仍可能修改配置、事件 payload 或生命周期假设。DeepSeek Harness 目前处于预发布阶段,仓库规则允许直接拒绝旧磁盘格式,也没有承诺 session 格式兼容。现在的可组合性主要服务于同一发行版内的替换和扩展,尚不能推导出跨版本插件 ABI 稳定。

第五项代价是组织治理。一切都能成为插件后,团队容易把每段十几行逻辑都拆成包,形成依赖图膨胀、文档分散和测试启动缓慢。DeepSeek Harness 用 package 分组、Service Definition/Provider/Consumer 角色、每包 README、运行时 invariant 和真实组合测试控制边界。这些规范本身就是成本。小团队若没有维护扩展生态或多 profile 的需求,模块化函数加显式依赖可能更合适。

历史对照

从 OSGi 看 Cordis,动态服务和生命周期联动已有先例。服务注册、发现、撤销后触发依赖变化,这条主线几乎一致。Cordis 值得学习的新点,是把资源清理统一成 effect,并让子插件、服务、监听器都归属于同一个 Fiber。OSGi 的 bundle 生命周期更完整,也更重;Cordis 选择应用内细粒度对象,失去了类加载隔离、标准版本解析和安全层,获得了低门槛组合。

从 Eclipse 看 DeepSeek Harness,profile 与 bundle patch 很像部署时组装,Service 和事件类似扩展点。差异在于 Eclipse 扩展通常围绕稳定宿主能力,DeepSeek Harness 连 agent loop 和 UI 都可替换,核心插件与第三方插件共享同一挂载机制。这个平权减少了特权内核,风险也更集中到协议治理:循环能被替换,不代表任何替代循环都遵守 session log、权限和工具时序约束。

从依赖注入容器看,Cordis 的服务解析并不陌生。新的组合来自 DI 与生命周期的绑定。普通容器负责构造对象,定时器、监听器和子进程仍由业务代码清理;Cordis 把注册动作收敛成 effect。空间 scope 与时间 ownership 同时存在,插件才能在某个 Agent 范围内挂载,并随该 Agent 完整撤销。

从微内核看,DeepSeek Harness 的内核确实很轻,但其 Loader 和配置协调已经承担不少平台职责。它要解析模块、等待依赖、处理 HMR、执行事务式更新、保存最后可用树、输出诊断。这提醒我,微内核减少的是业务特权,不会减少生命周期复杂度。能力越动态,内核对失败语义的要求越高。

Cordis 的贡献可以概括为一种紧凑的组合坐标:Context 决定插件位于哪里,Fiber 决定插件在何时有效,Service 负责直接能力,Event 负责横切协作,effect 负责撤销。DeepSeek Harness 在这个坐标系上加入 Agent scope、持久会话事件和配置事务,使其能承载多会话、多 profile 与热更新。

工程取舍

当我想要用类似架构时,会先确认三个条件,基于这三个条件判断来是否采用。

其一,能力确实需要独立替换,至少存在两个生产提供方或明确的外部扩展需求。

其二,同一进程需要多套组合并存,或者运行期间需要可靠更新。

其三,团队愿意为卸载、回滚和真实组合测试持续付费。

缺少这些条件,插件框架容易沦为复杂的工厂模式。

能力切分要从消费方开始。先列出谁调用、调用时需要哪些稳定语义,再设计 Service Definition;随后实现 provider,最后把面向模型的 schema、展示和错误文本留给 consumer。接口只有一个内部调用者时,保留私有闭包,不急着升格为公共 service。可替换性必须由真实替代者证明。

所有注册都要有明确所有者。注册工具、提示词片段、适配器、监听器时同步返回 disposer;卸载测试要观察注册项确实消失。涉及异步资源时,dispose 完成的定义要包含子进程退出、队列排空和 watcher 关闭,不能只发出取消信号。DeepSeek Harness 对 Fiber quiescence 的处理值得照搬。

配置更新要区分内存一致性和外部副作用。Loader 可以恢复旧树,插件初始化若已经创建云资源,回滚需要业务补偿。规则可更保守一些:挂载阶段只做校验和本地注册,外部写操作放到带幂等标识的运行阶段。确实要在初始化创建资源时,必须让 disposer 能识别部分完成状态。

作用域要控制在两种以内。实例隔离与注册可见性已经足够表达多数 Agent 场景,再增加租户、请求、工作区、角色四套独立 scope,调试成本会失控,需要根据实际业务需求和必要性来判断。当需要新增维度时,先判断它属于服务实例选择、注册过滤、持久数据权限还是请求参数。很多所谓新 scope,其实只是一次显式参数传递。

事件也要克制。需要唯一返回值的调用用 Service,需要持久恢复的事实写 session log,需要多个插件观察或包裹的阶段才用事件。waterfall 必须把短路当成公共协议,监听器顺序也要有测试。事件名虽然能降低导入耦合,却会增加时序耦合;后者通常更难排查。

小结

DeepSeek Harness 选择 Cordis,解决的主要矛盾是 Agent 能力变化速度与运行时一致性之间的冲突。模型、工具、沙箱和 UI 都可以替换并不稀奇;这些能力可以在局部作用域内共存,能随依赖变化启停,能在配置失败后保留旧树,卸载时还能回收监听器、服务和子插件,才构成可用的插件架构。

它也没有抹平工程风险。Proxy 隐藏了查找路径,双层 scope 增加理解成本,热更新无法回滚任意外部副作用,预发布阶段也缺少跨版本兼容承诺。团队采用这套思路时,应复制它的生命周期纪律和能力边界,别只复制「一切皆插件」的目录结构。

源码给我们一些启发,把插件设计的评审顺序倒过来:先问如何撤销,再问如何注册;先证明局部挂载不会泄漏,再讨论全局复用;先定义失败后保留哪一棵树,再讨论热更新速度。做到这些,时空可组合才是一组可以验证的运行时语义。

这套架构个人理解是想往 Agent 的实时「自生长」,通过 AI 的能力在使用过程中让自己更强大。

逼逼这么多,主要还是在这个过程中学习一下,说实话,有了 DeepSeek Harness,想自己开发一个专属的 Agent ,快捷方便了很多,eg且这是 MIT 协议的。

以上。

参考资料

相关推荐
IT规划师3 小时前
工业 AI 走向产线规模 - 20+ 伙伴的演示,不如一条 +20% 吞吐
ai编程
lucas_AI5 小时前
Termination Lag:你的 Coding Agent 早就修好了 Bug,只是它停不下来
agent·ai编程
小虎AI生活14 小时前
反推任意图片的神技能,WorkBuddy和Loomy装上就能用
aigc·ai编程
金銀銅鐵15 小时前
带您看懂字节码指令(二):空循环里有什么
java·后端·ai编程
JAVA前线15 小时前
用《实践论》分析前沿部署工程师:AI时代听见炮声的人
架构·ai编程
飞哥数智坊16 小时前
打造自己的 Personal Agent,从抄大肥鱼作业开始
人工智能·deepseek
IT规划师19 小时前
Agent 总返工? Google 工程师把工程实践写成了技能库
ai编程·代码规范
OpsEye19 小时前
同一套用例跑两个大模型接口,差异到底出在哪
javascript·ai编程
程序员老刘20 小时前
Flutter 3.50更新,开发者应该关注什么?
flutter·ai编程·客户端
孟健21 小时前
我用 REA 逆向自己的 Electron 桌面端,扒出 178 个 IPC 通道后我反而放心了
架构·开源·ai编程