DeepSeek Harness 事件系统(Cordis事件总线)
事件系统是 Cordis 微内核实现插件松耦合扩展的核心基础设施。Harness 中智能体生命周期、工具执行钩子、安全拦截校验、提示词流水线、会话轨迹流转全部构建在事件总线之上。
不同于简单的发布‑订阅模型,Cordis 提供五种不同语义的事件分发模式,分别适配广播通知、短路拦截、顺序编排、洋葱中间件流水线、并发扇出等场景。同时支持 TypeScript 声明合并实现类型安全,并且所有通过 ctx.on 注册的监听器都会跟随插件生命周期自动销毁,避免内存泄漏。
一、基础用法:监听与触发事件
插件在 apply(ctx)内部使用 ctx.on() 监听事件,使用对应的分发函数触发事件。通过 ctx.on 注册的监听器属于插件副作用,插件卸载、热重载时会被内核自动移除,不需要手动写注销逻辑。
import type { Context } from '@deepseek‑ai/cordis';export const name = 'demo‑event';
export function apply(ctx: Context) {
// 监听会话启动事件
ctx.on('session/start', (session) => {
ctx.logger.info('新会话启动', session.id);
});
// 触发广播事件
ctx.emit('worker/ready', { workerId: 'worker‑01' });
}
事件命名遵循官方约定 namespace/action,例如 agent/step、tools/before‑run、security/validate‑command,便于区分子系统。
二、五大事件分发模式
Cordis 提供 5 套分发契约,不同模式返回值、异步行为、短路规则完全不同,需要根据业务场景选择正确模式。
| 模式 | API | 行为说明 | 典型业务场景 |
|---|---|---|---|
| 广播 | ctx.emit | 同步执行所有监听器,忽略返回值,不等待异步 Promise | 日志打印、状态通知、埋点上报 |
| 短路拦截 | ctx.bail | 顺序执行,第一个返回非 null/false/undefined 的值直接作为结果并终止后续 | 安全校验、一票否决权限拦截 |
| 顺序异步执行 | ctx.serial | 按注册顺序 await 每一个监听器;非空返回提前终止 | 系统分阶段初始化、迁移、前置检查 |
| 洋葱流水线 | ctx.waterfall | 中间件模式;必须调用 next(),不调用直接截断链路 | 提示词转换、请求包装、缓存网关 |
| 并发扇出 | ctx.parallel | 并发全部监听器,等待全部 Promise 完成,不短路 | 并行执行多个后置统计、无关的后置任务 |
1. emit 广播
只做通知,监听器返回值全部丢弃;不会等待监听器内部异步操作。适合只通知不处理返回结果的场景。
ctx.emit('worker/ready', { workerId: 'worker‑01' }); ctx.on('worker/ready', (payload) => { ctx.logger.info(`worker ${payload.workerId} 就绪`); });
2. bail 短路拦截(安全校验常用)
按注册顺序执行监听器;只要某个监听器返回非空真值,立刻停止后续监听器,返回该结果。返回null / undefined / false 代表放行,继续执行下一个监听器。
// 安全策略拦截示例 ctx.on('security/validate‑command', (command) => { if(command.includes('rm -rf /')) { return '检测到高危毁灭性指令,已被安全策略拦截'; } });const blockReason = ctx.bail('security/validate‑command', userInputCommand);
if(blockReason) throw new Error(blockReason);
3. serial 顺序异步执行
依次 await 每个异步监听器;得到非空返回值就提前终止链路。适合分阶段启动、多步前置检查流程。
await ctx.serial('system/bootstrap‑phase', runtimeCtx);
4. waterfall 洋葱流水线(中间件)
每一个监听器必须显式调用 await next() 将控制权交给下游;如果不调用 next,流水线直接被截断短路,这是故意设计,用于实现缓存命中、熔断拦截等逻辑。
// 修改提示词:追加时间戳 ctx.on('prompt/transform', async (prompt, next) => { const downstream = await next(); return `${downstream}\n[Timestamp:${Date.now()}]`; });// 触发流水线处理原始prompt
const finalPrompt = await ctx.waterfall('prompt/transform', rawPrompt, async () => rawPrompt);
⚠️ 重要警告:waterfall 监听器忘记调用 next(),后续全部中间件不会执行,直接截断处理链。
5. parallel 并发扇出
全部监听器并发执行,等待所有异步完成,不会短路。适合多个互不依赖的后置统计、日志上报任务。和 emit 的区别:parallel 会等待 Promise,emit 完全不等待异步逻辑。
三、类型安全事件(TypeScript声明合并)
Cordis 支持通过模块声明合并扩展全局 Events 接口,实现事件的参数、返回值类型推导,编写代码获得自动补全、类型校验。
import type { Context } from '@deepseek‑ai/cordis';// 扩展Cordis全局事件类型
declare module '@deepseek‑ai/cordis' {
interface Events {
// 普通广播事件
'trajectory/step'(step:{index:number;action:string}):void;
// bail短路事件,返回string代表拦截,void放行
'security/check'(input:string):string | void;
// waterfall流水线事件
'prompt/transform'(input:string, next:()=>Promise<string>):Promise<string>;
}
} export function apply(ctx: Context) {
// 这里 step 对象会获得完整类型提示
ctx.on('trajectory/step', (step) => {
ctx.logger.info(步骤#${step.index}:${step.action});
});
}
四、区分两类事件:Cordis运行时事件 vs 会话持久化事件
这里极易混淆,官方明确区分两套事件体系:
- Cordis运行时事件:例如
agent/step、tools/result。内存实时分发,直接使用ctx.on('xxx',handler)监听;不会自动写入会话日志。 - 会话持久化事件:
turn/*、step/*、tool/call、tool/result、compaction/*。追加写入会话只读日志,用于回放、轨迹展示;不能直接监听该类型字符串,需要监听session/event,再判断 payload 内部的 event.type 字段。
// 监听持久化会话事件的正确写法
ctx.on('session/event', (evt) => {
if(evt.type === 'tool/call') {
ctx.logger.info('工具被调用', evt.data);
}
});
会话本身就是一份只追加的事件日志,是回放与轨迹的唯一真相源。
五、监听器与副作用生命周期
- 在 apply 内部通过
ctx.on()注册的监听器,自动作为插件副作用;插件卸载 / 热重载,内核自动解绑全部监听器,不会内存泄漏。 - 不要在 apply 外部、全局作用域注册事件,脱离 ctx 托管将无法自动清理。
- 可以接收 ctx.on 返回的 disposer 函数,运行期手动解绑单个监听器。
- 监听器内部抛出异常会被内核隔离捕获,打印日志,不会打断事件总线其他监听器执行。
六、实战示例:简单工具日志插件
监听工具执行结果,打印调用参数与返回内容,完整可直接运行的插件片段:
import type { Context } from '@deepseek‑ai/cordis'; import '@deepseek‑ai/dsh‑tools';export const name = 'tool‑logger'; export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
ctx.logger.info([tool] ${exec.name}(${JSON.stringify(exec.arguments)}));
const textOut = result.content
.filter(b => b.type === 'text')
.map(b => b.text)
.join('');
ctx.logger.info([tool‑result] ${textOut.slice(0,120)});
});
}
七、常见问题FAQ
Q:emit 和 parallel 的区别?
A:emit 同步触发,完全忽略 Promise,不会等待异步回调;parallel 等待所有监听器的 Promise 全部完成。
Q:waterfall忘记调用 next()会发生什么?
A:流水线直接短路截断,下游所有中间件不会运行;这是用于缓存、熔断的故意设计,写中间件务必不要漏掉 next()调用。
Q:监听器抛异常会不会导致整个事件停止?
A:不会,每个监听器异常被隔离捕获并输出日志,其他监听器继续正常执行。
Q:如何手动解绑事件?
A:保存 ctx.on 返回的 disposer 函数,调用即可手动解绑;插件卸载时无需手动处理,内核自动清理。
Q:我想监听 tool/call,直接写 ctx.on(‘tool/call’) 为什么收不到?
A:tool/call 属于会话持久化事件,不是Cordis运行时事件;需要监听 session/event 再判断 evt.type 字段。
八、开发最佳实践
- 所有事件监听写在
apply(ctx)函数内部,交给插件副作用托管,禁止全局注册监听器。 - 根据业务语义选择分发模式:日志通知用 emit;安全拦截用 bail;分阶段启动用 serial;中间件改写用 waterfall;并行统计用 parallel。
- 区分Cordis运行时事件与session持久化事件,不要直接监听 turn/step/tool/call 这类持久化事件名字符串。
- 开发自定义事件,补充 TypeScript 声明合并,获得类型安全。
- waterfall 中间件必须保证调用 next(),除非业务目标就是主动截断链路。
本篇小结
事件系统是 Cordis 微内核的消息中枢。五种分发模式分别适配通知、拦截、顺序编排、中间件流水线、并发扇出;配合类型安全声明合并,以及监听器跟随插件生命周期自动销毁的副作用机制,实现插件之间完全解耦。
开发时必须分清两套事件:内存实时分发的 Cordis 运行时事件,和写入会话日志的持久化会话事件。理解事件系统,就掌握了Harness实现拦截、扩展Agent行为的最核心手段。
0 条笔记