第 6 章 动手实战(二):事件与 Waterfall
事件让插件无需知道谁在监听就能发出通知。harness 用事件处理工具结果、模型请求、审批决定等交互。本章对应官方教程第 4 章。
6.1 声明、发出与监听
创建 stats.ts——一项负责计数并在每次变化时发出通知的服务:
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
interface Events {
'stats/report'(name: string, count: number): void
}
}
export class StatsService extends Service {
private counts = new Map<string, number>()
constructor(ctx: Context) {
super(ctx, 'stats')
}
bump(name: string) {
const next = (this.counts.get(name) ?? 0) + 1
this.counts.set(name, next)
this.ctx.emit('stats/report', name, next)
}
}
export const name = 'stats'
export function apply(ctx: Context) {
ctx.plugin(StatsService)
}
要点:
创建 reporter.ts:
import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
export const inject = ['stats']
export function apply(ctx: Context) {
ctx.on('stats/report', (name, count) => {
console.log(`[stats] ${name} -> ${count}`)
})
ctx.stats.bump('tool_call')
ctx.stats.bump('tool_call')
ctx.stats.bump('prompt')
}
注意 import type {} from './stats.ts':它不会在运行时导入任何内容,作用是让 TypeScript 看到声明合并。运行输出:
[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1
因为 ctx.on() 属于 effect,监听器会随插件一同消失,永远不需要手动 removeListener。
6.2 五种分发模式回顾
ctx.emit(name, ...args)
await ctx.parallel(name, ...args)
await ctx.serial(name, ...args)
ctx.bail(name, ...args)
await ctx.waterfall(name, ...args, next)
每个 harness 事件的模式都记录在其所属子系统页面的自动生成参考中。
6.3 Waterfall:转换或短路
waterfall 是实现拦截的模式。每个监听器收到参数和一个 next() continuation:
创建 waterfall-demo.ts:
import type { Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
}
}
export const name = 'waterfall-demo'
export function apply(ctx: Context) {
ctx.on('demo/transform', async (input, next) => {
const downstream = await next()
return downstream.toUpperCase()
})
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **'
return next()
})
void (async () => {
console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello'))
console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words'))
})()
}
cordis.yml 只指向该文件,运行输出:
第二行的执行过程:监听器 1 先运行并调用 next() → 触发监听器 2 → 监听器 2 看到 blocked 后不调用 next() 直接返回 → 最内层默认逻辑(传给 ctx.waterfall 的函数)从未运行 → 返回途中监听器 1 把替换消息转为大写。
黄金纪律
只负责观察或标注的 waterfall 监听器必须调用 next()。不调用就直接返回代表有意短路。日志监听器若忘记调用 next(),会悄无声息地吞掉所有下游的默认行为。
harness 使用 waterfall 处理"协作插件可以包装或回答"的决策:
agent/request:替换模型调用配置;
approval/request:策略代替用户作答;
agent/pre-step、llm/stream、tools/* 三事件等(均为瀑布式);
对比:agent/turn-stopping 是 serial 事件,没有 next()。
依据官方教程第 4 章(04-events)整理。