DSH:以Cordis构建插件化AI助手架构

本文基于智能代理编排框架DSH的源码实现,并结合一篇88页的《面向时空可组合性的编程范式》学术论文,深入拆解其架构设计与技术细节。该框架目前处于开发者预览阶段,其API与论文中的目标模型均不视为最终稳定版本。
在配置好Node.js开发环境的系统中,可以通过以下命令直接启动DSH的Web界面,无需全局安装:
npx dsh web
DSH常以"一切皆插件"来概括自身的架构设计,但这一表述并不足以完全解释其核心优势。该框架真正的独特之处,在于通过一套统一的机制组合了模型适配器、系统提示词、工具目录、会话管理、存储系统、沙箱环境、Agent循环、Web界面等所有核心能力,同时解决了五个通常彼此分离的架构问题:
- 插件在当前上下文下能够访问哪些能力
- 当依赖的必要能力未就绪时,插件是否应该启动
- 同名服务能否在不同会话或作用域中解析到不同实现
- 插件退出时,其注册的项、监听器、进程和句柄由谁负责清理
- 静态配置如何转换为一棵可更新、可检查的运行时插件树
更清晰的理解方式是将DSH看作两套同时运行的协同系统:
- Cordis维护一张运行时插件图,记录当前系统中存在哪些能力、它们的依赖关系、生效的作用域以及卸载时的清理逻辑
- Session维护一份追加式事件流,记录一次Agent工作的完整执行过程,并用于投影模型上下文、UI轨迹、会话恢复与分叉所需的数据
Agent循环位于这两套系统之间,它从插件图中获取模型、工具、提示词和会话服务,再将执行过程写入事件流。Cordis解决"系统当前由什么组成"的问题,Session解决"系统刚刚执行过什么"的问题,二者结合构成了DSH的核心架构。
这一架构设计也解释了为什么专业级的Agent编排工具不能只关注Agent循环本身。最基础的Agent循环仅需要拼接提示词、调用模型、执行工具并将结果返回给模型;而完整的产品级工具还需要处理模型与凭证管理、会话恢复与压缩、工具策略与审批、沙箱与远程执行、多宿主支持、子Agent管理、插件启停逻辑,以及UI如何观察同一份运行时事实。DSH将后一组复杂的组合逻辑交给Cordis处理,将历史连续性的维护交给Session。
配置驱动的插件树构建
DSH启动时并不会直接构建一个固定的Agent应用。它会先读取配置文件与插件包,再叠加用户自定义的补丁,最终由Cordis加载器将配置行逐条挂载为插件实例。
这里存在三层核心的配置概念:
- Bundle:用于分发一组Cordis配置和对应的插件代码
- Profile:定义一个进程需要堆叠哪些Bundle,内置模板包括web与无界面运行模式
- Patch:用于替换或插入配置行,支持用户自定义配置和命令行覆盖
加载器处理后的结果并非普通的配置对象,而是一棵正在运行的插件树。每一条配置行决定了插件的名称、父子位置、参数、启停条件和服务隔离域;当配置发生变化时,相应的插件实例也可以被重新配置、卸载或挂载。
Profile的各层配置并非简单的"合并"。源码会从空的入口列表开始,依次应用Profile声明的Bundle、Profile自身的配置补丁、本地配置目录下的补丁,再应用命令行传入的补丁;如果启动参数关闭了遥测功能,加载器还会在最后叠加对应的覆盖配置。后加载的配置会按照配置行ID替换整段配置或插入新的行。因此在调试启动问题时,源码的import图只能说明"可能加载什么",而通过命令行导出的最终配置树,才能反映当前机器实际会挂载的插件。
这一设计让DSH不仅支持开发阶段的扩展,也具备了部署阶段的装配能力。如果更换某项实现仍需要修改启动代码,那么控制权还在程序内部;但当部署方可以直接通过配置替换实现时,配置本身就成为了系统提供的能力。
DSH还区分了宿主组合与单个Agent的组合。宿主层持有进程共享的注册表、持久化存储、沙箱与审批、模型路由等能力。Agent预设则挂载在会话作用域中,主要贡献该会话使用的工具、人格、提示词片段和少量隔离服务。预设ID还会写入会话头部,恢复会话时必须使用相同的组合,否则历史中已经出现过的工具和提示词可能与当前Agent能力不一致。
Cordis运行时核心机制
仅凭YAML配置描述插件树,仍然无法回答运行时的核心问题:当依赖未就绪时该如何处理?同名能力如何隔离?当Provider被替换后哪些组件需要重启?插件产生的副作用由谁回收?
我们可以通过一个简化的文件系统组合示例来理解这些机制。为了便于阅读,我们将minimal预设中的文件系统配置进行了整理,并故意将消费者放在提供者前面:
# agent.cordis.yml
- id: filesystem
name: cordis:group
group: true
# 为这棵子树创建私有的fs服务域
isolate:
fs: true
config:
# 消费者:当tools或当前域的fs缺失时,Fiber保持等待状态
- id: editor
name: '@dsh/tool-str-replace-editor'
inject: [tools, fs]
config:
maxOutputChars: 16000
# 提供者:在私有域中发布ctx.fs
- id: fs-provider
name: '@dsh/fs-local'
config:
cwd: !!js process.env.DSH_CWD ?? process.cwd()
配置中的isolate.fs: true让这个分组拥有独立的服务域,另一个分组可以发布自己的fs服务而不会产生冲突。inject配置让editor插件在tools和fs服务都可解析之前保持等待状态,因此YAML的行顺序并不决定启动顺序,只有当提供者出现后,Cordis才会激活对应的消费者。如果宿主环境已经提供了沙箱策略,我们可以将fs-provider这一行替换为沙箱版本的实现,提供者Fiber的身份变化会刷新消费者的依赖纪元,使旧的editor Fiber先卸载,再使用新的fs服务重新加载。
YAML配置本身并不会描述"如何关闭"插件,这部分逻辑留在插件代码中。提供者和消费者通过ctx.effect()、ctx.provide()、ctx.on()等方法登记清理函数,Cordis会在Fiber卸载时自动执行这些清理逻辑。简言之,YAML声明的是期望的拓扑结构,而Context、Fiber与effect负责让这一拓扑在运行时成立并安全退出。
Cordis通过Context、服务、Fiber与effect共同维护这些依赖关系,它们并非四层互不相关的抽象,而是一套完整的插件节点运行规则。这也是Cordis与普通的"插件表+事件总线"架构的核心区别。
Context与服务
DSH插件通过ctx.tools、ctx.llm、ctx.sessions等服务名称进行协作。消费者依赖的是能力接口,而非直接绑定具体的实现。
ctx看起来像是装满单例的对象,但实际上它是经过代理包装的服务解析边界。Context不仅保存当前可用的服务,还携带父作用域、隔离域、依赖声明和当前Fiber信息。子Context默认会继承父级的服务,isolate()方法可以为某项服务创建独立的作用域。即使两个会话都访问ctx.tools,也可以获得各自独立的工具目录。
Cordis使用代理处理Context上的服务访问。通过ctx.foo直接读取未在inject中声明的服务会抛出错误;而显式调用ctx.get("foo")是更底层的读取入口,不受这项声明检查。当必需的依赖缺失时,插件不会进入活跃状态。
这只是一种依赖约束,而非权限控制。原生JavaScript插件仍然在宿主进程中执行,未注入fs服务并不代表它在操作系统层面失去了文件访问能力。
在DSH中,一个可替换的能力通常包含三个角色:
- 服务定义:定义接口和公共语义
- 服务提供者:提供本地、远程、沙箱或测试实现
- 服务消费者:通过当前Context使用该能力,最常见的是模型可见工具
isolate配置则允许这种替换只作用于某个Agent,而非整个进程。
Fiber:插件实例
Plugin是可复用的定义,而一次实际的挂载对应一个Fiber实例。同一个插件使用不同的配置、挂载在不同的Context下,会产生多个独立的Fiber。
Fiber是Cordis为本次插件运行创建的实例记录,它将依赖解析、生命周期和资源所有权统一到同一个对象中,主要保存以下信息:
- 父Context与当前Context
- 插件配置和注入依赖
- 当前解析到的服务实现
- 加载、活跃、卸载等生命周期状态
- 运行期间登记的清理函数
Fiber会根据依赖计算自身的激活状态。当必需的提供者尚未出现时,它会停留在等待状态;当提供者就绪后才会加载。当提供者消失或更换实现时,Cordis会通知相关的Fiber,重新计算依赖并触发卸载或重载。
因此,Cordis维护的并非启动时解析一次的依赖注入关系,而是一张会随提供者变化而动态刷新的运行图。Plugin是代码定义,而Fiber才是"这份定义在这个父Context下,使用这份配置和这组依赖"的活实例。所谓的依赖顺序也不再只是启动脚本中的先后关系,而是运行时持续维护的状态。
Effect:副作用归属
插件加载通常会产生注册项、事件监听器、定时器、文件句柄或后台进程。Cordis要求插件在创建这些资源时,同时登记对应的清理函数。与单独维护activate()/deactivate()方法相比,将资源获取和释放写在同一个effect中,不容易随着功能迭代而出现不匹配的情况。ctx.on()、ctx.provide()等框架辅助方法本身也接入了effect所有权机制。
我们可以通过DSH的JSON存储插件来理解这套机制:
export const inject = ["storage"]
export function apply(ctx, config) {
const backend = new JsonStorageBackend(config.root)
ctx.effect(() => {
const unregister = ctx.storage.backend.register("json", backend)
return async () => {
unregister()
await backend.close()
}
})
ctx.provide("storageBackend:json", backend)
}
inject配置让插件等待存储中心服务,Context会为它解析当前作用域中的storage服务。effect方法将注销和关闭backend的逻辑归属于当前Fiber,provide方法则将JSON后端暴露给其他消费者。
单次ctx.effect()内收集的清理函数会按照逆序串联,适合处理"后创建的资源依赖先创建的资源"这类关系。当Fiber整体卸载时,内置的Cordis实现会并发启动多个顶层effect的清理过程并等待它们全部结束,因此不能将所有顶层清理函数简单理解为严格串行的LIFO栈。
effect的能力仅限于声明边界之内。插件没有登记的进程和句柄不会自动消失,已经提交到外部系统的事务也不会因为Fiber卸载而撤销。它提供的是结构化的资源管理,而非事务级的回滚能力。
时空可组合性的理论基础
Cordis的学术论文为上述机制建立了一套形式化模型,解释了Cordis的组合语义、成立条件与适用边界。其结论并不覆盖DSH整体架构的正确性或性能。
本文涉及三份核心材料:
- 学术论文:描述时空可组合性的目标模型、性质与成立前提
- 独立Cordis仓库:用于观察论文与上游实现快照的对应关系
- DSH定制版Cordis:DSH实际运行的版本,包含了针对DSH的本地加固
按照固定的源码快照,DSH的定制版Cordis已经修补了effect设置期间重入卸载、异步清理失联、加载器更新失败回滚、配置监听串行化等问题,与独立仓库的快照存在实质差异。包版本号无法代替实际的实现核对,论文中的性质是否成立仍取决于具体的代码和证明前提。
论文将组件的能力需求、环境贡献和撤销逻辑整合为可由运行时持续调和的对象,并研究了当多个对象交错运行时,系统在什么条件下仍能恢复、正确排序并最终收敛。
Agent循环执行链
当插件树构建完成后,DSH的Agent循环才具备运行条件。默认的AgentLoop明确依赖五项核心服务:
[agents, sessions, llm, tools, systemPrompt]
这五项依赖勾勒出一次请求的完整流程:
sessions:保存规范的事件日志,并将事件投影为模型可识别的消息systemPrompt:汇总提示词片段、变量与工具schemallm:提供模型适配与流式输出能力tools:管理工具目录、策略和执行管线agents:管理Agent实例与运行中的协调逻辑
一次step通常对应一次成功的模型请求及其工具执行;当请求错误触发内部重试时,同一个step可能会发起多次提供者请求。一个turn可以包含零到多个step。被拒绝或被改写为空的第一次输入仍会留下turn边界,但不会消耗一次模型step。当工具结果要求模型继续工作,或有新的输入进入下一step时,turn会暂时结束。
主执行流程可以简化为:
输入进入收件箱
→ turn/start
→ 领取下一步输入与排队消息
→ 读取提示词片段与工具schema
→ agent/pre-step 接纳、拒绝或改写输入
→ 拒绝,或第一次输入被改写为空:turn/end,不产生step
→ 接纳:
step/start
→ user/message 写入会话日志
→ deriveMessages() 投影模型历史
→ agent/request → llm/stream
→ assistant/chunk* → assistant/message
→ tool/call* → tools/pre-execute
→ tools/execute
→ tools/post-execute
→ tool/result*
→ step/end
→ 仍有工具后续步骤或下一步输入:进入下一step
→ agent/turn-stopping
→ turn/end
这条路径中存在三类不同的事件,不能因为都称为event就混淆它们的作用:
- 会话事件:持久化的事实记录,包括turn/*、step/*、user/message、assistant/*和tool/*等类型,必须支持跨重启恢复
- Agent事件:携带活跃的Agent对象,用于收件箱、请求、验证、继续运行和状态协调
- 能力事件:附着在fs/*、tools/*、telemetry/*等能力边界上,用于策略控制与适配
Cordis服务负责直接提供能力,而Cordis事件负责观察和拦截运行过程。持久化事件会进入会话日志;而agent/*和tools/*这类中间件大多只存在于当前运行时中。这种分层设计既避免了将所有中间件的临时状态写入日志,也避免了只保存聊天文本而丢失恢复所需的因果边界。
追加式事件流设计
运行时插件图描述的是"当前系统有哪些能力",而会话事件流描述的是"这次工作已经发生了什么",二者的生命周期完全不同。
插件可以被卸载,提供者可以被替换,Context的服务图会动态变化;但已经写入Session的user/message、assistant/message、tool/call和tool/result等记录都是历史事实。会话的恢复、分叉、轨迹UI和统计分析都基于这份日志。
DSH将Session设计为追加式日志。turn/step边界、原始流式chunk、最终模型消息和工具往返都带有连续序号,系统不会为了压缩上下文而直接修改旧事件。
架构文档将核心约束总结为"Model-visible means logged":所有进入模型请求的输入,都必须能够从规范日志中重建。当需要新增一种模型可见的输入类型时,应该先扩展SessionEventMap,再由日志系统投影它。这里的重点是"可重建",而非要求所有内部调度状态都转化为模型消息,也不是机械地保存每次prompt的完整副本。
模型下一次看到的内容并非整份日志。deriveMessages()方法会从日志中维护的表面数据投影出模型消息:
assistant/chunk:保留流式回放的细节,但不会与最终的assistant/message重复出现在模型历史中- turn/step边界和统计类事件不会被转化为模型消息
- 压缩操作会追加替换节点,在当前表面中遮蔽一段旧消息,但原始事件仍会保留在日志中
因此,"完整记录"与"完整发送"是两个不同的概念。Token消耗取决于当前消息投影、系统提示词和工具schema的大小,而非磁盘日志的长度。
四种Agent模式解析
标准、PTC、极简和创造四种Agent模式共享同一个Agent循环与宿主服务,它们之间的差异由会话作用域中的配置行决定。
standard:提供完整的编码Agent工具组合code:保留标准的工具注册和执行管线,增加工具展示功能,将工具目录以代码模式暴露给模型。模型主要看到run_code与生成的TypeScript SDK,通过一段程序组合多个工具调用。PTC模式改变的是模型调用工具的协议层,而非建立第二套调度循环minimal:使用完整的人设,仅提供持久化Bash和字符串替换编辑器,且不加载压缩功能。这里的"仅保留两个工具"指的是模型可见的表面;宿主层共用的会话、持久化、审批与沙箱服务并未被删除,Web组件则取决于当前是否使用Web Profilecordis:在标准组合的基础上增加运行时检查与临时插件管理工具。动态包位于共享的DSH进程内存中,重启后会消失,不会自动保存为插件文件或持久化配置。执行模型生成的JavaScript接近Shell权限,不能作为安全隔离环境
这些模式示例说明了Cordis带来的产品价值:不同的模式差异可以表现为插件图的增量变化,而无需复制整个Agent循环。
与其他同类框架的对比
这些项目并非完全处于同一技术层级。Pi更接近DSH中的Agent内环;OpenClaw、Hermes-Agent、Codex与DSH更接近完整的Agent编排工具或产品宿主;而Cordis则处于更低的层级,它并不理解模型与工具的具体逻辑,只负责组件的组合、依赖管理和生命周期。
更合适的比较方式是围绕三个共同问题展开:一项能力如何接入系统,同一套核心如何服务多个入口,连续请求如何维持稳定的模型前缀。
各自的设计重心
- Pi:将Agent状态、模型调用和工具循环作为核心。它的扩展能力并不弱,工具、提供者、压缩功能和UI都可以被替换;其特色是内环设计简洁、接口直接,没有引入额外的领域无关组合运行时
- OpenClaw:其核心更接近网关和产品领域。工具、消息渠道、提供者、Hook与Agent编排工具都有明确的入口,扩展能力通常由宿主语义直接定义
- Hermes-Agent:围绕Agent对象组织产品能力,同时将"这项能力会占用多少长期模型上下文"作为设计约束。Skill、条件工具、Plugin、MCP与核心工具集并非同一种入口,而是一组成本不同的选择
- Codex:更强调Thread/Turn/Item这组执行语义,以及围绕它建立的类型化客户端协议。扩展边界较为清晰,也更容易从类型系统中判断某个客户端或插件被允许执行的操作
- DSH:同时将插件运行图与Session事件流作为核心。它的辨识度不在于某个特定工具或循环逻辑,而在于让大量不同的能力共享Context、服务、effect、Fiber和加载器这套组合语义
多端复用的设计策略
这些项目都在复用Agent核心,只是边界画在不同位置。
Pi直接提供交互、单次输出、JSON、RPC与SDK等入口;OpenClaw通过网关将内部Agent核心连接到消息渠道和原生macOS伴侣程序;Hermes的CLI、TUI、消息渠道与Electron桌面应用共享同一个Agent对象,桌面端通过JSON-RPC/WebSocket连接无头运行的服务端,并未将Python运行时嵌入Electron应用。
Codex将多端复用收敛到清晰的协议边界。Electron应用会以app-server模式启动可执行文件;TUI通过AppServerSession接入同一套接口,VS Code也被列为app-server客户端。各端共享的并非终端UI,而是codex-core的执行语义,以及由Thread、Turn、Item构成的类型化合同。
DSH的Web与无头运行模式同样复用Cordis、Session和Agent的基础合同,但二者加载的默认插件集合并不相同。Profile决定进程级的宿主组合,而预设则决定每个会话使用哪些工具、提示词与局部服务。因此,DSH的复用边界不仅限于客户端协议层:配置本身也参与决定运行时的组成。
架构的收益与成本
Cordis带来的收益并非笼统的"更灵活",而是将几种原本分散的组合问题整合到了同一套语义中。
组合方式统一:新增工具、替换提供者、注册UI、构建基准预设,无需各自发明一套注册、作用域和卸载协议。插件作者反复使用Context、服务、事件、effect与Fiber,发布者再使用同一种配置树装配它们。
生命周期成为一等概念:配置、依赖和清理函数都归属于一次Fiber挂载。开发阶段的热更新、测试隔离、临时插件和会话级能力因此拥有共同的卸载路径,而非依赖每个子系统约定自己的shutdown逻辑。
依赖可以延迟绑定并按位置替换:消费者依赖稳定的服务名称;当提供者延迟加载、消失、替换为远程实现或仅在某个作用域中替换时,Cordis提供对应的等待、通知和重载语义。一个Agent使用本地能力,另一个使用沙箱能力,无需复制消费者代码。
模式差异通过组合实现,而非主程序分叉:Web/无头模式与四种Agent预设位于不同的配置轴上,PTC模式主要替换工具展示逻辑,Minimal模式主要收窄模型可见表面。它们共享同一套会话、策略和循环逻辑,降低了多种体验各自漂移的风险。
运行时本身可观察和实验:配置入口、Fiber、服务与effect都是可观察的实体。创造模式允许查看当前组合并挂载内存插件,说明这里的"插件生态"不仅限于构建期的扩展点,也可以成为编排工具的实验台。
相应的成本同样来自这套统一机制:
静态代码不再等于实际系统:import图只能说明可能性;Profile、Bundle、Patch、预设、条件表达式和作用域共同决定了真实的拓扑结构。如果缺少最终配置树,问题报告通常无法完整描述复现对象。
动态依赖会放大因果链:提供者的一次变化可能导致一组消费者Fiber依次进入卸载和重载流程;异步设置、事件通知与清理函数又可能产生交错。排障时除了调用栈,还需要查看提供者身份、Fiber纪元、当前已提交的绑定和清理是否收敛。
可逆不等于事务:effect只能回收插件声明过的资源,无法自动补偿网络消息、共享文件写入或支付操作。顶层effect还会并发清理,当存在严格的资源顺序要求时,必须由插件显式建立边界。
插件化不等于安全:inject约束仅限制通过Context访问的能力,无法阻止同进程代码直接导入系统API;effect解决的是资源所有权问题,工作线程只能提供部分执行隔离,都不是不受信任代码的安全边界。创造模式尤其需要按照高权限运行时实验室来理解。
结语
DSH真正的价值在于抓住了Agent编排工具产品化后的另一类复杂度:组合关系可能比循环逻辑本身更难管理。Cordis将能力的可见范围、激活条件、依赖重载和资源清理转化为显式的运行时图;Session则将执行历史保存为可恢复、可分叉和可投影的事件流。运行图与事件流一横一纵,构成了DSH最具辨识度的架构。
这套设计是否值得,取决于项目面临的组合压力。对于单一宿主、固定循环和少量稳定工具的场景,显式插件图可能带来的概念复杂度会超过其收益;但当多宿主、多提供者、会话级隔离、运行时装卸和第三方生态同时出现时,组合关系本身就会成为主要问题,Cordis的投入才会开始显现价值。
我们可以通过三个方向来阅读DSH的源码:首先查看加载器输出的配置树,然后追踪服务的provide/inject、Context作用域与Fiber effect,最后沿Session事件到deriveMessages()检查模型实际看到的内容。这些运行时特性,比"一切皆插件"这句口号更能决定DSH能否成为可靠的Agent编排基础设施。

