文章摘要
文章基于DSH源码与相关学术论文,深入拆解其插件化AI助手架构。DSH以Cordis和Session为核心,前者维护运行时插件图,后者维护追加式事件流,二者结合解决多个架构问题。还介绍了配置驱动的插件树构建、Cordis运行机制、Agent循环执行链等,对比同类框架,分析架构收益与成本,指出其价值取决于组合压力。

本文基于智能代理编排框架DSH的源码实现,并结合一篇88页的《面向时空可组合性的编程范式》学术论文,深入拆解其架构设计与技术细节。该框架目前处于开发者预览阶段,其API与论文中的目标模型均不视为最终稳定版本。

在配置好Node.js开发环境的系统中,可以通过以下命令直接启动DSH的Web界面,无需全局安装:

npx dsh web

DSH常以"一切皆插件"来概括自身的架构设计,但这一表述并不足以完全解释其核心优势。该框架真正的独特之处,在于通过一套统一的机制组合了模型适配器、系统提示词、工具目录、会话管理、存储系统、沙箱环境、Agent循环、Web界面等所有核心能力,同时解决了五个通常彼此分离的架构问题:

  1. 插件在当前上下文下能够访问哪些能力
  2. 当依赖的必要能力未就绪时,插件是否应该启动
  3. 同名服务能否在不同会话或作用域中解析到不同实现
  4. 插件退出时,其注册的项、监听器、进程和句柄由谁负责清理
  5. 静态配置如何转换为一棵可更新、可检查的运行时插件树

更清晰的理解方式是将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.toolsctx.llmctx.sessions等服务名称进行协作。消费者依赖的是能力接口,而非直接绑定具体的实现。

ctx看起来像是装满单例的对象,但实际上它是经过代理包装的服务解析边界。Context不仅保存当前可用的服务,还携带父作用域、隔离域、依赖声明和当前Fiber信息。子Context默认会继承父级的服务,isolate()方法可以为某项服务创建独立的作用域。即使两个会话都访问ctx.tools,也可以获得各自独立的工具目录。

Cordis使用代理处理Context上的服务访问。通过ctx.foo直接读取未在inject中声明的服务会抛出错误;而显式调用ctx.get("foo")是更底层的读取入口,不受这项声明检查。当必需的依赖缺失时,插件不会进入活跃状态。

这只是一种依赖约束,而非权限控制。原生JavaScript插件仍然在宿主进程中执行,未注入fs服务并不代表它在操作系统层面失去了文件访问能力。

在DSH中,一个可替换的能力通常包含三个角色:

  • 服务定义:定义接口和公共语义
  • 服务提供者:提供本地、远程、沙箱或测试实现
  • 服务消费者:通过当前Context使用该能力,最常见的是模型可见工具
三者结合构成了能力边界。以文件系统和子进程为例,它们共享一套执行环境;将提供者指向远程沙箱,可以连带迁移Bash、PTY和LSP服务,而无需分别为每个工具创建进程。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:汇总提示词片段、变量与工具schema
  • llm:提供模型适配与流式输出能力
  • 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 Profile
  • cordis:在标准组合的基础上增加运行时检查与临时插件管理工具。动态包位于共享的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编排基础设施。

以上内容不代表本平台立场,仅供读者参考