第 3 章 Cordis 核心概念
在阅读 dsh 的子系统文档之前,必须先掌握 Cordis 的核心概念。本章对应官方 Cordis 入门(cordis-primer)。
3.1 五个核心概念
① 插件是实现 Service 的对象
插件有三种形态(均可被 Cordis 挂载):
函数/对象插件中,
name导出是可选的显示元数据,用于诊断信息。在你需要公开服务之前,请一直使用函数形态。
② 上下文是服务的容器
一个服务占据一个稳定的 ctx.<key>(如 ctx.tools、ctx.llm、ctx.sessions)。其他插件通过 key 查找服务,而非导入具体实现——因此配置可以自由替换提供方,消费方代码不用改。
③ 通过 inject 声明服务依赖
插件声明所需服务后会等待这些服务就绪才启动(在此之前处于 PENDING 状态)。加载顺序由服务依赖表达,而非手动编排启动序列,也不是配置文件里的书写顺序。
④ 类型化事件用于通信
服务通过 TypeScript 声明合并注册事件名,然后以 5 种方式分发(见 3.2)。事件让插件在互不知晓对方存在的前提下协作。
⑤ 注册是可逆的副作用
提示词片段、工具 schema、适配器、提供方、监听器……都通过 ctx.effect() 或 ctx.on() 安装,reload 和 teardown 时会自动撤销。每个注册都应有对应的 disposer:要么从 ctx.effect() 返回一个,要么使用内置 API 自带的自动管理。
3.2 五种事件分发模式
每种事件有且只有一种分发模式,这是事件公开约定的一部分:
| 模式 | 是否 await | 分发顺序 | 是否有返回值 |
|---|---|---|---|
emit | 否 | 按注册顺序观察 | 否 |
waterfall | 否 | 按注册顺序包装 | 是 |
parallel | 是 | 所有监听器并行 | 否 |
serial | 是 | 按顺序执行,首个非空返回值胜出并停止 | 是 |
bail | 否 | serial 的同步版本,停在首个 bail 值 | 是 |
调用方式:
新的 harness 事件通过 @mode 标签记录模式,生成的文档目录可将声明与分发调用点交叉校验。每个 harness 事件的模式都记录在其所属子系统页面的自动生成参考中。
3.3 Waterfall 语义:环绕中间件
ctx.waterfall 是环绕中间件,是实现拦截的核心模式:
监听器签名:
(...args, next)。调用
next()会执行下游监听器;下游返回值通过next()返回到当前包装层,可被该层包装后继续向外返回。不调用
next()直接返回 = 短路(否决),下游监听器和默认逻辑都不会运行。协作式监听器通常修改一个共享的请求/决策对象后调用
next()委托;监听器也可以完全替换结果。仅当监听器必须在普通注册之前运行时才使用
prepend: true。
纪律:只负责观察或标注的 waterfall 监听器必须调用 next()。一个日志监听器若忘记委托,会悄无声息地吞掉所有下游默认行为——这是本仓库的常设规则。
对于"单决策"事件,短路是设计意图:策略监听器在拥有决策权时可以不调用 next() 直接返回。
harness 中的典型 waterfall:
agent/request:允许插件替换模型调用配置。approval/request:允许审批策略代替用户作答。agent/pre-step、llm/stream、tools/pre-execute、tools/execute、tools/post-execute。
另外,agent/turn-stopping 是 serial 事件(没有 next())。
3.4 Loader 配置:!!js 表达式
@deepseek-ai/cordis-plugin-include 将 !!js 标签解析为表达式节点:
Loader 在插件声明的注入激活后,基于该插件上下文(
ctx.serviceName)插值条目的config。条目的
disabled字段在每次挂载决策时基于 loader 上下文求值(可按平台/环境门控一行配置)。!!js仅在config与disabled字段内有效;其余元数据(name、id、inject等)保持字面值。
3.5 实践规则(官方建议)
把行为封装为插件:工具流水线事件属于
ctx.tools,模型流式输出属于ctx.llm,实时 agent 协调属于ctx.agents。拦截和策略优先使用事件;直接能力调用优先使用服务方法。
每个注册都应有对应的 disposer。如果 teardown 顺序有要求,把相关工作放在同一个 effect 中——因为 disposer 按注册逆序启动,但多个异步 disposer 是并发运行的,只有放在同一个 disposer 里依次 await 才能保证顺序。
依据官方 Cordis 入门 整理。