05 核心架构:一切皆插件#
「一切皆插件」是 DeepSeek Harness 最核心的设计哲学。与传统框架将核心逻辑硬编码在主干中不同,dsh 从底层元框架到上层 Agent 循环、工具、UI,所有组件都是地位平等的插件,没有任何特权内核存在。本章深入解读这一架构设计。
设计哲学深度解读#
大多数 Agent 框架的架构是「内核 + 扩展点」模式:
存在一个不可修改的特权内核,包含核心循环、调度逻辑
预留若干 hook/扩展点,允许开发者在特定节点插入自定义逻辑
要修改核心行为必须 fork 源码、改动主干
dsh 完全抛弃了这种模式,选择了彻底的插件化:
不存在特权内核,没有任何一部分逻辑是「内置不可修改」的
模型适配器、工具注册表、会话日志、Agent 循环本身,乃至整个 UI 层,都是普通插件
插件之间通过统一的上下文(Context)和事件系统通信,地位完全平等
你可以替换任何一个组件,包括别人以为是「核心」的那部分
这一设计带来的直接结果是:扩展 dsh 不需要「打补丁」或修改源码——你只需要把自己的插件挂载到插件树上,不需要的插件直接卸载即可。
Cordis 元框架:一切皆插件的地基#
dsh 的底层由一个名为 Cordis 的元框架驱动。
论文来源#
Cordis 的设计思路源自学术论文《A Programming Paradigm for Spatiotemporal Composability》(时空可组合性的编程范式):
论文地址:cordiverse/paper
代码仓库:cordiverse/cordis(已作为
external/libs/cordis引入)
Cordis 不是一个 Agent 框架,它是一个更底层的插件化元框架——定义了插件如何注册、如何通信、如何叠加、如何撤销的一套通用机制,dsh 是在这套元框架上构建出来的 Agent 运行时。
Cordis 五种核心概念#
Cordis 的世界观由五个核心概念构成,缺一不可:
1. 插件:实现 Service 的对象#
插件有三种合法形态,本质都是向 Context 贡献服务:
// 形态 1:函数插件(带可选 inject 和 apply)
const myPlugin = (ctx: Context) => {
// 插件逻辑
};
myPlugin.inject = ['tools', 'llm']; // 声明依赖的服务
myPlugin.apply = (ctx: Context) => { /* 自定义应用逻辑 */ };
// 形态 2:Service 子类
class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService'); // 注册到 ctx.myService
}
}
// 形态 3:普通对象
const plainPlugin = {
inject: ['config'],
apply(ctx: Context) {
// 对象形式插件逻辑
}
};
2. 上下文:服务容器#
Context 是插件运行的容器,每个服务占据稳定的 ctx.<key> 命名空间:
例如
ctx.tools、ctx.llm、ctx.sessions都是稳定的服务键插件通过 key 查找服务,而不是通过模块导入
这意味着服务可以被替换,只要保持相同的 key 和接口契约
3. inject:声明式服务依赖#
插件通过 inject 字段声明自己依赖哪些服务:
Cordis 保证所声明的依赖服务全部就绪后才启动插件
加载顺序由依赖关系自动推导,不需要手动编排
循环依赖会在启动时被检测并报错
4. 类型化事件:五种分发模式#
Cordis 提供五种事件分发模式,覆盖不同场景:
模式 |
签名 |
行为 |
典型场景 |
|---|---|---|---|
|
|
同步执行,不 await,监听器按注册顺序观察,无返回值 |
纯通知、日志广播、状态更新 |
|
|
同步执行,按注册顺序调用监听器,第一个返回非 |
注册表查找、第一个匹配处理器获胜 |
|
|
await 所有监听器并行执行,无返回值 |
扇出通知、多监听器独立并行处理、遥测上报 |
|
|
await 监听器按注册顺序串行执行,有返回值 |
责任链、顺序校验、结果投票(如 |
|
|
环绕中间件模式,监听器接收 |
请求拦截改写、权限校验、中间件管道(如 |
重要修正:早期文档只提到 waterfall,实际 Cordis 源码(
events.ts中DispatchMode类型定义)提供以上五种分发模式,dsh 中五类事件都有使用。事件的分发模式通过 TypeScript JSDoc 的@mode标签标注。
5. 注册是可逆副作用#
所有通过 Context 进行的注册(事件监听、服务注册、配置修改)都是可逆的:
通过
ctx.effect()或ctx.on()安装的副作用,会在 reload/teardown 时自动撤销不需要手动管理清理逻辑,插件卸载即恢复原状
无特权内核设计原则#
无特权内核是 Cordis 最重要的设计原则:
不存在一个「特殊」的内核插件拥有更高权限
框架启动时,最先加载的也只是一个普通插件
你可以卸载任何插件,包括别人认为是「系统核心」的组件
所有插件遵循完全相同的接口规范和生命周期
这意味着:官方发布的默认插件集合只是一种「出厂配置」,而不是「唯一正确的形态」。
可逆效应:插件卸载自动清理#
可逆效应(Reversible Effects)是 Cordis 保证插件干净卸载的核心机制。
问题的提出#
传统插件系统经常遇到「卸载不干净」的问题:
插件注册了事件监听,卸载后没有移除,导致内存泄漏
插件修改了全局配置,卸载后没有还原,影响后续运行
插件在文件系统写入了文件,卸载后留下垃圾文件
插件注册了服务,卸载后服务仍在注册表中导致冲突
可逆效应的解决方式#
Cordis 要求插件在产生任何副作用时,都必须同时注册对应的「撤销函数(disposer)」:
监听事件时,
ctx.on()自动返回一个取消监听的 disposer注册服务时,返回一个注销服务的 disposer
修改配置时,记录修改前的值,返回还原 disposer
创建资源时,返回释放资源的 disposer
ctx.effect() 是通用副作用注册 API,支持四种 disposer 形式:
disposer 类型 |
说明 |
|---|---|
函数 |
同步清理函数,最常用 |
Generator |
可暂停的清理流程 |
Promise |
异步清理,await 完成 |
AsyncGenerator |
异步可暂停的清理流程 |
执行顺序:所有 disposer 按注册逆序执行(栈顺序,LIFO),保证依赖的资源先释放——例如如果插件 B 依赖插件 A,那么 B 先卸载,A 后卸载,避免依赖悬空。
当插件被卸载时,Cordis 会自动执行该插件注册过的所有撤销函数,将系统恢复到插件加载前的状态——就像这个插件从来没有存在过一样。
这就是「可逆效应」的含义:插件产生的所有效应都是可逆的,卸载即清理,不留痕迹。
实际价值#
可逆效应机制让 dsh 的插件组合变得极其安全:
你可以大胆试验新插件,不满意直接卸载,不会污染系统
可以动态切换实现(换模型适配器、换沙箱、换循环),不需要重启
不存在「卸载了插件但还有残留」的灵异问题
插件组合是可预测的,不会因为加载顺序产生难以调试的副作用
Bundle:能力捆绑包分发格式#
Bundle 是 dsh 的插件分发格式,相当于「插件包」。
Bundle 是什么#
一个 Bundle 是一个可分发的单元,通常包含:
一组 Cordis 配置行(声明要注册哪些服务、监听哪些事件、挂载哪些插件)
配置行要挂载的实际代码(JavaScript/TypeScript 模块)
元数据(名称、版本、描述、依赖等)
你可以把 Bundle 理解为「一份可安装的能力清单 + 实现代码」。
Bundle 的关键特性#
Bundle 最重要的特性是:Bundle 插入的任何内容,仍然可以被上层继续打补丁(Patch)。
也就是说,Bundle 不是「最终配置」——它只是「基础配置」,上层可以随时覆盖或修改 Bundle 提供的任何配置行。这是分层叠加机制的基础。
常见内置 Bundle#
dsh 默认提供多个内置 Bundle,例如:
core/model-adapters:模型适配器 Bundle,包含 DeepSeek、OpenAI、Anthropic 等适配器core/tools:核心工具 Bundle,包含文件操作、Shell 执行、编辑等基础工具core/session-log:会话日志 Bundle,实现 append-only 事件流core/agent-loop:默认 Agent 循环实现web/ui:Web UI 层 Bundle
Profile:命名组合清单#
Profile 是存放在本地的一份「命名组合清单」,定义了「我要加载哪些 Bundle、应用哪些补丁」。
Profile 是什么#
如果说 Bundle 是「零件」,那么 Profile 就是「组装说明书」:
Profile 是一个有名字的配置(如
web、headless、minimal、creator)它按顺序罗列自己要叠加哪些 Bundle
它可以安装 Bundle 之外的树外插件
它保存该 Profile 专属的补丁配置
Profile 文件存放在
~/.dsh/profiles/目录下
内置 Profile 模板#
dsh 出厂时自带两个预置 Profile 模板:
Profile |
用途 |
对应模式 |
|---|---|---|
|
带 Web UI 的完整模式,默认配置 |
Standard 模式 |
|
无头模式,无 UI,适合命令行一次性执行任务 |
无头运行 |
四种运行模式本质上是基于这两个基础 Profile 叠加补丁而来的。
用户自定义 Profile#
你可以创建自己的 Profile:
在
~/.dsh/profiles/下新建一个目录,目录名就是 Profile 名称在目录中编写
profile.yml,声明要加载的 Bundle 列表添加你自己的补丁文件
启动时用
--profile 你的profile名即可使用
通过自定义 Profile,你可以组合出完全属于自己的 Agent 形态。
分层叠加机制与补丁系统#
dsh 启动时,并不是简单地加载一个固定配置,而是按固定顺序分层叠加,最终构建出运行时的插件树。
五层叠加精确顺序#
运行时配置从空条目列表开始,从下到上依次叠加,上层覆盖下层:
第 1 层:dsh-base 基础层
↓(每个 profile 的第一层:模型适配器、工具、持久化、沙箱与审批、设置、凭据、遥测)
第 2 层:Profile Bundle 层
↓(按 profile 列出的顺序应用每个组合包,如 dsh-web-app 增加浏览器应用;dsh-headless 增加一次性运行器且无服务器)
第 3 层:Profile 补丁层
↓(应用 profile 自己的 cordis.patch.yml)
第 4 层:Home 级补丁层
↓(应用 ~/.dsh/ 目录下的 cordis.patch.yml 全局补丁)
第 5 层:命令行 Overlay 层
(应用任意 --patch 命令行参数传入的临时补丁)
每一层都可以修改、覆盖、插入下层的任意配置行,最终叠加结果就是实际运行的配置。
补丁(Patch)机制详解#
补丁是分层叠加的核心单元。补丁的工作原理是:
按 id 定位配置条目:每一条 Cordis 配置条目都有唯一的 id
支持两种操作:
替换(Replace):用新的配置整体替换原有条目(不是局部修改,是整个 config 替换)
插入(Insert):插入新条目
补丁只声明差异:你不需要写完整配置,只需要写你要改的那部分
举个例子,如果你想把默认的 DeepSeek 模型适配器换成你自己实现的版本,只需要写一个补丁:
patch:
- replace:
id: core/model-adapters/deepseek
with:
# 你自己的适配器配置
import: ./my-custom-deepseek-adapter.ts
config:
apiEndpoint: "https://your-custom-endpoint.example.com"
不需要改源码,不需要重新编译,只需要把这个补丁放在对应层,重启即可生效。
--dump-config:查看实际启动的配置树#
你可以用 --dump-config 命令查看当前配置叠加后的最终结果:
npx @deepseek-ai/dsh --profile web --dump-config
这条命令会打印出:
最终加载的所有插件列表
每一条配置条目的最终值
每个配置条目的来源(来自哪个 Bundle、被哪个补丁修改过)
完整的服务注册表和事件监听器
打印出来的任意一行,你都可以写补丁替换掉。这是调试配置、理解系统结构的最重要工具。
事件三大域#
dsh 在 Cordis 事件系统基础上,将事件划分为三大域,承担不同职责:
1. 会话事件(Session Events)#
追加到会话日志并通过
session/event广播的持久事实重启后仍然存在,因为它们被写入 append-only 日志
例如:用户消息、助手回复、工具调用、工具结果、Agent 状态变更
用于审计、回放、UI 渲染
2. Agent 事件(agent/*)#
命名空间:
agent/inbox、agent/step、agent/status、agent/request、agent/validate、agent/continue携带活跃 Agent 引用,用于观察/拦截进行中的工作
是拦截和修改 Agent 行为的主要扩展点
例如:在
agent/request上挂载 waterfall 中间件可以在模型调用前修改请求
3. 能力事件(Capability Seam Events)#
命名空间:
fs/*、tools/*、telemetry/*等无需导入循环即可向 capability seam 附加策略和适配器
例如:监听
fs/read可以审计所有文件读取;监听tools/before-execute可以做工具调用审批这是实现横切关注点(安全审计、限流、日志)的推荐方式
核心服务分类概览#
dsh 的所有核心能力都通过 ctx 上的稳定服务键暴露,按角色可分为三类:
ctx 键 |
角色 |
职责 |
|---|---|---|
|
core |
仅追加 SessionEvent 日志和内存存储 |
|
core |
提示词片段与工具 schema 组装 |
|
core |
作用域化工具注册表和带把关的执行流水线 |
|
core |
Agent 接口、活跃 agent 注册表和 agent/* 事件 |
|
bundle |
默认循环驱动器(唯一具体循环插件) |
|
seam |
消息与流式词汇表、适配器注册 |
|
seam |
文件系统提供方 |
|
seam |
Bash 执行器 |
|
seam |
子进程 spawn |
|
seam |
持久化 PTY 会话 |
|
seam |
进程沙箱 |
|
seam |
权限审批 |
|
seam |
凭据管理 |
|
seam |
子 Agent 提供方 |
|
seam |
网页搜索和抓取 |
|
seam |
后台任务注册表 |
|
core |
同会话目标管理 |
|
seam |
Skill 提供方注册 |
|
seam |
语言服务器导航 |
|
- |
MCP 客户端桥接 |
|
seam |
工作流脚本引擎 |
角色说明:
core:框架核心服务,定义了 dsh 的基本骨架,通常不建议替换
bundle:具体实现 Bundle,可以替换
seam:能力接缝(Capability Seam),是扩展和定制的主要位置
扩展指南:新行为归属位置决策表#
当你想为 dsh 添加新能力时,根据目标选择正确的注册位置:
目标 |
机制 |
|---|---|
添加模型提供方 |
在 |
添加面向模型的能力 |
在 |
添加 shell 执行 |
注册 |
添加持久化终端 |
注册 |
添加用户命令 |
在 |
添加后台工作 |
在 |
添加文件系统访问/策略 |
注册 |
限制启动进程 |
使用 |
拦截请求/工具/轮次 |
使用 |
添加模型可见上下文 |
调用 |
添加 UI/编辑器集成 |
驱动 |
管理同会话目标 |
使用 |
fork 活跃会话 |
|
将注册限定到单个 agent |
使用该 agent 的 |
架构价值:从改代码到改配置#
「一切皆插件」架构带来的最大价值是:换模型、换工具、换沙箱、换循环——这些原本需要改代码的操作,现在都变成了改配置。
传统框架 vs dsh#
操作 |
传统框架 |
dsh |
|---|---|---|
换一个模型提供商 |
修改模型适配层代码,重新编译,可能破坏其他功能 |
写一个补丁替换模型适配器 Bundle,一行配置搞定 |
添加一个自定义工具 |
实现工具接口,注册到工具注册表,修改框架初始化代码 |
写一个插件,在 |
替换沙箱实现 |
重构文件系统和子进程调用,侵入式修改 |
替换 Capability Seam 的 Provider,一次替换处处生效 |
完全改写 Agent 循环逻辑 |
fork 整个项目,大改核心循环,难以合并上游更新 |
卸载默认 |
定制 UI 界面 |
修改前端源码,重新构建 |
替换 |
架构带来的生态可能性#
这种架构为生态繁荣留出了巨大空间:
官方只负责定义接口和提供默认实现
社区可以开发更好的模型适配器、更智能的上下文压缩插件、更强大的记忆系统
企业可以开发内部专属的工具 Bundle 和安全审计插件
研究者可以快速替换循环逻辑,试验新的 Agent 范式
不同团队可以根据自己的需求组合出完全不同的 Agent 形态,却运行在同一个运行时上
官方明确表示:主仓库里的包并不比社区的包更重要——这不是一句客套话,而是架构本身的性质决定的:既然所有插件地位平等,官方插件没有任何特权,自然也就不存在「官方实现必须被使用」的情况。
理解了「一切皆插件」的架构,你就理解了 dsh 与其他 Agent 框架最本质的区别。下一章我们将深入这一架构中最核心的部分:Agent 循环与事件模型。
← 04 四种模式 | → 06 Agent 循环与事件