上一课,你学的是"任务按照什么顺序执行"。这一课,我们换一个观察角度:
执行循环、模型适配器、文件工具、权限策略这些代码,怎样接入 Harness,又怎样相互协作?
我们继续围绕"读取文件、计算平均值"讲。先理解现有架构,不要求你编写插件。
一、先分清:Cordis 和 Agent Loop 各自负责什么
你已经知道,Agent Loop 会推进模型请求、工具调用以及后续请求。
Cordis 负责的是这些组件的加载、依赖、通信和清理。
| 对象 | 在这个例子中负责什么 |
|---|---|
| 模型 | 根据本次输入,输出文字或工具调用请求 |
| Agent Loop | 请求模型、分派工具调用、记录结果、决定是否继续执行 |
| Cordis | 让执行循环、模型适配器、工具等插件按依赖关系运行,并管理它们的生命周期 |
所以,Cordis 本身不是"另一个负责推理的 Agent"。
在 DSH 中,连 Agent Loop 本身也是通过插件提供的。运行中的产品由插件组合而成,不是只有你后来添加的业务功能才叫插件。官方架构说明
这也解释了一个重要区别:
插件之间的依赖关系,不等于业务任务的执行顺序。
"文件工具依赖文件系统服务",描述的是运行条件;"先读取文件,再计算均值",描述的是任务过程。
二、先拆开"读取文件"这个功能
上一课为了理解执行顺序,我们把它统称为"读取工具"。
现在往内部看,就能发现不同职责。
以下以文件工具这条路径为例;通过 Shell 命令读取文件,属于另一条调用路径。
| 要解决的问题 | 官方代码中的对应角色 |
|---|---|
| 向模型提供怎样的文件操作入口? | dsh-tool-fs:提供文件读取、写入、编辑工具 |
| 文件访问要遵循什么程序接口? | dsh-fs:定义 ctx.fs 服务及相关类型、约定 |
| 怎样实际访问本地磁盘? | dsh-fs-local:文件系统服务的一种具体实现 |
注意:这是代码包的职责划分 ,不是说表中每个包都必须作为一个独立插件条目启动。实际部署还可能包含沙箱、文件操作策略等组件。官方文件系统说明
把它对应到一次读取:
- 模型提出文件读取的工具调用。
- Harness 的工具运行机制找到对应工具实现。
- 文件工具通过
ctx.fs使用文件系统能力。 - 当前文件系统提供方完成实际读取。
- 文件工具整理结果,交回执行循环。
这里最关键的是第三步:
文件工具使用的是 ctx.fs 这份接口约定,而不是必须直接依赖某个本地磁盘实现。
因此,如果有另一个实现遵守相同约定,就可以通过组装配置更换提供方,让工具继续使用同一个接口。实际替换仍需验证路径、权限、错误处理等行为是否符合约定。
官方把这种职责划分称为:
- Definition:定义接口。
- Provider:实现接口。
- Consumer:使用接口。
文件工具就是 Consumer,本地文件系统后端就是 Provider。
但这不是要求你以后每个功能都拆成三个 npm 包。官方明确说明:只有需要独立演进或替换时,才值得分包;简单工具可以在一个包内实现。官方能力角色设计
三、Plugin、Tool、Service,究竟有什么区别?
这是这一课最需要建立的认识。
1. Plugin:由 Cordis 加载和管理的扩展单元
一个插件被加载后,可以:
- 提供一个服务;
- 注册一个或多个工具;
- 注册事件监听器;
- 提供提示词内容;
- 组合其他插件。
它不一定包含面向模型的工具。
例如,一个只负责记录运行情况的插件,可以监听事件,却不向模型增加任何可调用能力。
在代码层面,Cordis 支持函数、类,以及带有 apply 方法的对象等插件入口形式。官方插件注册接口
因此:
"安装了一个插件"不等于"模型多了一个同名工具"。
还要看这个插件实际注册了什么,以及这些注册对当前 Agent 是否可见。
2. Tool:提供给模型使用的具名调用入口
以文件读取为例,模型需要知道:
- 工具叫什么;
- 能做什么;
- 接受哪些参数。
Harness 还需要持有这个工具的实际执行代码。
这两部分不是一回事:模型获得的是工具定义,不是自动获得工具实现的源代码。 工具运行时负责注册、查找和执行;向模型提供 schema 时,会排除执行回调等内部内容。官方工具运行时参考
因此,日常使用时更准确的说法是:
模型请求调用插件注册的 Tool,由 Harness 分派执行。
而不是"模型把整个插件运行了一遍"。
3. Service:插件之间可以使用的程序能力
文件工具需要读文件,就使用文件系统服务。
执行循环需要请求模型,就使用模型服务。
这里的 Service 通常是运行时中的程序接口,不意味着你必须另外部署一个 HTTP 服务或微服务。它的具体实现可以访问本地资源,也可以连接远程系统。
对应关系可以这样记:
Plugin 是扩展单元;Tool 是面向模型的调用入口;Service 是插件之间使用的程序接口。
一个 Plugin 可以提供 Service,也可以使用已有 Service 来注册 Tool。
四、ctx 是什么?为什么源码里到处都有它?
你以后读 DSH 源码,会不断看到:
TypeScript
ctx.fs
ctx.tools
ctx.llm
ctx.sessions
先把这些表达式翻译成具体意思:
| 表达式 | 在程序中取得什么 |
|---|---|
ctx.fs |
当前上下文中的文件系统服务 |
ctx.tools |
当前上下文中的工具注册与执行服务 |
ctx.llm |
当前上下文中的模型调用服务 |
ctx.sessions |
当前上下文中的会话管理服务 |
ctx 是 Cordis 的 Context 对象。插件通过它访问服务、事件和生命周期管理能力。官方 Context 参考
这里要特别区分两个"上下文":
模型上下文,是一次模型请求携带的提示词、历史消息、工具结果等信息。
Cordis 上下文 ctx,是程序运行时用于访问服务、注册能力和管理资源的对象。
它们不能混用:
- 把服务放到
ctx上,不代表模型自动看见它。 - 把一段文字放进模型上下文,也不代表程序里自动出现对应服务。
举个具体例子:
即使程序能够通过 ctx.fs 读取文件,如果没有适当的工具入口、没有对当前 Agent 暴露相应工具,模型也不能直接用 ctx.fs 发起文件操作。
另外,ctx 不是"所有插件永远共享同一份实现"的全局字典。Cordis 支持子上下文和服务隔离;经过明确配置,不同上下文中的同名服务可以解析到不同实现。
不过,服务作用域隔离不等于操作系统级安全沙箱。这两种隔离解决的问题不同,我们后面会专门展开。
五、inject:插件怎么知道依赖已经准备好了?
现在考虑一个实际问题:
文件工具开始初始化时,工具注册表或文件系统服务还没准备好,怎么办?
Cordis 通过声明服务依赖来处理。
你以后会看到这种代码:
TypeScript
export const inject = ['tools']
它表达的是:
这个插件需要
tools服务;服务未就绪时,不执行这个插件的初始化入口。
这里我们只读这一行,不需要创建文件运行。
如果一个插件需要多个服务,就声明多个依赖。文件工具这一类功能,至少需要考虑工具注册能力与文件访问能力;具体完整依赖以对应源码为准。
Cordis 会依据依赖是否就绪决定插件能否激活,而不是要求你靠手动排列一长串启动调用来保证顺序。官方服务与依赖说明
这里有两个容易混淆的"依赖"
npm 包依赖:代码能否被找到、导入。
Cordis 服务依赖:运行时需要的能力是否已经就绪。
例如:
- 相关 npm 包已经安装;
- 但没有加载对应服务提供方;
- 消费该服务的插件仍然可能无法激活。
所以,inject 不负责安装 npm 包,也不会替你填写 API Key。
还有一个边界:
服务已就绪,不代表后续每次业务调用都一定成功。
模型服务可以已经可用,但某次远程请求仍可能超时。
必需服务运行中消失了怎么办?
如果某个必需服务的提供方被卸载,依赖它的插件会被卸载、清理;服务重新可用时,相关插件可以重新加载。
这是依赖管理的一部分,不是只在启动时检查一次。
因此,"更换底层服务"可能影响依赖它的组件,不能把热替换理解成任何任务都保证无中断。
六、Event:怎样参与执行过程,而不用修改每个工具?
现在增加一个需求:
每次工具执行完成后,都要记录一次运行情况。
如果在每个工具实现里分别添加记录代码,文件工具、Shell 工具、网络工具都要修改。
DSH 已有相应扩展点,其他插件可以监听它们,从而参与运行过程。
但"事件"不能一律理解成"发一个通知就结束了"。这里先认识两种机制。
1. 通知型:告诉监听者发生了什么
例如,工具运行时的 tools/result 可以供监听者观察最终工具结果。
使用 emit 模式分发的事件,会同步调用监听器,并忽略监听器返回值。
这类机制适合观察、记录等用途。不能随便在监听器里返回一个 false,就认为能够取消已经完成的工具操作。
2. 环绕型:让监听者参与处理过程
工具执行前后,还有使用 waterfall 模式的扩展点。
这类监听器可以包装后续处理。你以后会看到一个重要参数:
next
调用 next(),表示把控制权交给后续处理;不调用,则会短路这条处理链。是否允许拒绝、怎样返回结果,要遵守该事件的具体约定。官方事件 API
注意这个名字:
这里的 next() 不是"开始下一个 Step"。
它只是继续当前事件的后续处理。一个 Step 内部,就可能经过多个这样的扩展点。
再区分:运行时事件与会话日志事件
下面两个名字很相似,但不是同一个东西:
tools/result:Cordis 工具运行时的结果观察事件。tool/result:Session 日志中的工具结果事件类型。
如果你要观察日志中的 tool/result,应监听 session/event,再检查收到事件的 type。
不能因为日志里有某个事件类型,就假设存在同名的 Cordis 事件。官方事件系统:会话记录的区别
到这里,服务和事件的使用区别就清楚了:
要直接使用一项能力,调用服务方法;要观察或参与已有处理过程,寻找对应的事件或策略扩展点。
七、生命周期:插件退出后,为什么不能留下旧注册?
假设某个插件加载时,注册了一个工具和一个事件监听器。
你修改代码后,再加载新版本。
如果旧注册没有清理,可能出现:
- 同一个事件被重复处理;
- 新旧逻辑同时运行;
- 旧实例仍持有连接或其他资源。
因此,插件不仅需要"怎样开始",还需要"怎样退出"。
Cordis 为插件的每次运行实例管理生命周期。这个实例在文档里叫 Fiber ;现在先把它理解成"某次插件加载对应的运行实例",不要与 Turn、Step 混淆。官方 Fiber 参考
生命周期中发生什么?
对于同一插件实例的一次激活,通常是:
- 检查必需服务是否就绪。
- 执行初始化入口。
- 注册工具、服务、事件监听等能力。
- 在运行期间处理相应调用或事件。
- 卸载时撤销注册、释放资源。
每次调用一个 Tool,不等于重新初始化整个插件。
如果发生重载或依赖变化,则可能开始新的一次激活过程。
哪些东西会自动清理?
通过 Cordis 管理的工具注册、事件监听等,会随所属插件卸载而撤销。
自定义资源则需要提供清理逻辑。例如,插件打开了一个连接,就应把关闭连接的操作交给生命周期管理机制。ctx.effect() 是处理这类资源的方式之一。官方插件与生命周期说明
这里必须避免一个误解:
自动清理,不是自动理解任意代码产生的全部影响。
更不是业务回滚:
- 撤销工具注册,不会自动删除该工具之前生成的报告。
- 移除监听器,不会自动撤回它之前发送的请求。
- 关闭数据库连接,不等于撤销之前已经提交的数据。
这与上一课"停止执行不等于撤销结果"是一致的。
八、本课实操:查看你的 Harness 是怎样组装的
这次实操只查看配置,不卸载插件、不改权限。
如果你之前使用 npx.cmd 启动,可以查看配置:
TypeScript
npx.cmd @deepseek-ai/dsh --profile web --dump-config
如果你使用的是全局安装、已经可以直接执行 dsh:
TypeScript
dsh --profile web --dump-config
两种方式选与你现有安装一致的那一种。如果之前固定了包版本,这里继续使用同一版本,不需要为了本课升级。
--dump-config 用于预览组装后的配置;其中还可能保留运行时才解析的表达式。它不是向模型发送任务,也不是所有插件已经成功激活的证明。官方配置预览说明
你这次只看三件事
第一,找插件入口。
关注条目中的 name,看它指向哪个包或模块。不要把它与模型看到的工具名称混为一谈。
第二,找配置归属。
关注 id 和 config:哪个配置属于哪个插件条目。
这里的插件条目 id,也不是你之前配置模型连接时填写的 Provider ID。
第三,尝试定位与 tools、fs、llm 有关的条目。
然后用本课的概念判断:
- 哪个负责工具注册或分派?
- 哪个提供底层能力?
- 哪个使用这些能力?
不要求你现在解释整个配置树。某些代码包只定义接口,某些插件可能在创建 Agent 时才被进一步挂载;找不到一个名称,不能直接判定安装有问题。
如果分享输出,只贴相关条目,并去掉密钥、令牌、私有地址等敏感内容。