DeepSeek Harness 事件系统

2026-09-08 00:00    #AI   #工具   #DeepSeek  

插件之间松耦合通信靠事件(Cordis 的通信核心机制)。事件分监听触发两端:

1// 监听事件:注册一个回调
2ctx.on('event-name', (payload) => {
3  // 处理事件
4})
5
6// 触发事件:广播给所有监听器
7ctx.emit('event-name', payload)

通过 ctx.on 注册的监听器,会在插件卸载时自动移除。

五种分发模式

模式分发方法是否 await顺序是否有返回值典型场景
emit 广播ctx.emit按注册顺序通知类:所有监听者观察
bail 短路ctx.bail按注册顺序决策类:第一个有效返回值胜出
serial 顺序ctx.serial按注册顺序分阶段初始化:按序执行并等待
waterfall 流水线ctx.waterfall按注册顺序处理链:逐层包装下游返回值
parallel 并行ctx.parallel全部并行扇出:多个监听者并行处理

每个事件有且只有一种分发模式,且只能通过对应方法分发。

各模式示例

emit:广播 — 所有监听器同步执行,返回值被忽略。

1// 触发方
2ctx.emit('my-plugin/ready', { id: 'runoob-worker-1' })
3
4// 监听方
5ctx.on('my-plugin/ready', ({ id }) => {
6  console.log(`${id} is ready`)
7})

bail:短路 — 监听器按顺序运行,第一个不是 nullfalseundefined 的返回值会成为最终结果。

1// 分发方:做一次检查,取第一个「有意见」的结果
2const result = ctx.bail('some-check', input)
3
4// 监听方
5ctx.on('some-check', (input) => {
6  if (shouldBlock(input)) return 'blocked'
7  // 返回 null / false / undefined 表示「我没意见」,让后面的监听器继续
8})

serial:顺序执行 — 监听器按注册顺序依次执行并等待异步结果。第一个有效返回值会终止后续执行。

1await ctx.serial('setup-phase', context)

waterfall:流水线 — 每个监听器可以包装下游返回值,形成处理链。监听器接收 (...args, next),调用 next() 会执行下游监听器。

1// 分发方
2const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
3
4// 监听方:必须调用 next(),拿到下游结果后做一次加工再返回
5ctx.on('my-plugin/transform', async (_input, next) => {
6  const downstream = await next()
7  return downstream.trim()
8})
waterfall 监听器必须调用 next()。不调用 next 会短路整个流水线,这是故意为之的设计,用于实现拦截或网关逻辑。策略监听器在拥有决策权时,可以不调用 next() 直接返回;仅做标注或观察的监听器则必须委托给下游。

类型安全的事件

用 TypeScript 声明合并为事件提供类型安全:

 1// 文件路径:scratch-plugin/src/events.ts
 2import '@deepseek-ai/cordis'
 3
 4declare module '@deepseek-ai/cordis' {
 5  interface Events {
 6    'my-plugin/ready': (payload: { id: string }) => void
 7    'my-plugin/check': (input: string) => boolean | undefined
 8    'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
 9  }
10}
11
12// 此后 ctx.on / ctx.emit 的参数会被自动推断,拼错事件名或参数类型都会在编译期报错

Cordis 事件与会话记录

Harness 的 Cordis 事件遵循 namespace/action 命名,例如 agent/stepagent/requesttools/resultsession/event。注意区分两类事件:

事件是什么如何观察
agent/steptools/resultCordis 事件,实时分发直接 ctx.on('tools/result',...)
turn/*step/*tool/calltool/resultcompaction/*持久化的会话事件类型监听 session/event,检查 event.type

turn/*step/*tool/calltool/result持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 session/event 并检查 event.type

动手示例:日志插件

 1// 文件路径:scratch-plugin/src/tool-logger.ts
 2import type { Context } from '@deepseek-ai/cordis'
 3import '@deepseek-ai/dsh-tools'
 4
 5export const name = 'tool-logger'
 6
 7export function apply(ctx: Context) {
 8  // 监听 tools/result 事件:每次工具执行完成都会触发
 9  ctx.on('tools/result', (exec, result) => {
10    // 打印工具名与参数
11    console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
12    // 把结果内容里的文本块拼起来,只打印前 100 个字符
13    const text = result.content
14      .map(block => block.type === 'text' ? block.text : '')
15      .join('')
16    console.log(`[tool result] ${text.slice(0, 100)}`)
17  })
18}

要点