4. 事件

English | 中文 
 服务支持直接调用； 事件 让插件无需知道有哪些插件正在监听，就能发出通知。harness 使用事件处理工具结果、模型请求和审批决定等交互。 
 声明、发出与监听 
 创建 stats.ts ，将它放在 tmp/cordis-tutorial 中。它是一项负责计数并在每次变化时发出通知的服务： 
 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)
}
 
 interface Events 合并与第 3 章的 interface Context 合并在事件系统中相互对应：它声明事件名称及其监听器签名，因此 ctx.emit 和 ctx.on 都具有完整类型。 namespace/action 命名约定让扁平的事件命名空间保持易读。 
 创建 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 看到声明合并。组合并运行： 
 - name: './stats.ts'
- name: './reporter.ts'
 
 [stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1
 
 因为 ctx.on() 属于 effect，监听器会随插件一同消失，绝不需要手动维护 removeListener 。 
 分发模式 
 emit 是 5 种分发模式之一。事件采用哪种模式是其约定的一部分，决定了监听器能否返回值、能否并发运行，以及能否彼此短路： 
 
 
 
 模式 
 调用 
 语义 
 
 
 
 
 emit 
 ctx.emit(name, ...args) 
 同步广播；不会等待或收集返回的 promise 与值。 
 
 
 parallel 
 await ctx.parallel(name, ...args) 
 所有监听器并发运行，并一同等待。 
 
 
 serial 
 await ctx.serial(name, ...args) 
 监听器按顺序运行并等待；第一个非 null / false / undefined 返回值胜出，并停止后续监听器。 
 
 
 bail 
 ctx.bail(name, ...args) 
 serial 的同步版本。 
 
 
 waterfall（瀑布式事件） 
 ctx.waterfall(name, ...args, next) 
 环绕中间件，见下文。 
 
 
 
 每个 harness 事件都会在其所属 子系统页面 自动生成的参考文档中记录其模式。 
 waterfall：转换或短路 
 waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 next() continuation；它可以转换 next() 的返回值，也可以不调用 next() 就直接返回，从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 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) {
 // Listener 1: wrap the downstream result.
 ctx.on('demo/transform', async (input, next) => {
 const downstream = await next()
 return downstream.toUpperCase()
 })

 // Listener 2: short-circuit when it owns the decision.
 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 只指向该文件并运行： 
 HELLO
** BLOCKED **
 
 按顺序看第二行如何产生：监听器 1 先运行并调用 next() ，从而调用监听器 2；监听器 2 看到 blocked 后直接返回而不调用 next() ，因此最内层默认逻辑（传给 ctx.waterfall 的函数）从未运行；返回途中，监听器 1 再把替换消息转换为大写。 
 由此得到一项纪律： 只负责观察或标注的 waterfall 监听器必须调用 next() ；不调用就直接返回代表有意短路。如果日志监听器忘记调用 next() ，会悄无声息地吞掉所有下游的默认行为。这是本仓库的常设规则（ waterfall 语义 ）。 
 harness 使用 waterfall 处理协作插件可以包装或回答的决策： agent/request 允许插件替换模型调用配置， approval/request 允许策略代替用户作答。 
 下一章： 配置 ：来自 cordis.yml 的插件选项。