Skip to content

DeepSeek Harness 事件系统:emit / bail / serial / waterfall

插件之间怎么松耦合通信?事件就是 Cordis 的通信核心机制。

Harness 大量使用事件来实现可扩展的扩展点,这一篇把五种分发模式讲清楚。


基本用法

事件分监听与触发两端。

实例

python

// 监听事件:注册一个回调

ctx.on('event-name', (payload) => {

  // 处理事件

})

// 触发事件:广播给所有监听器

ctx.emit('event-name', payload)

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


五种分发模式

Cordis 提供多种分发模式,适用于不同的交互契约。

16-dispatch-modes.svg

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

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

官方入门文档把 waterfall 标为「不 await」;但由于监听器通常是异步函数,教程示例里普遍写 await ctx.waterfall(...) 来等待整条处理链的最终结果。记住这个差别即可,两种写法都不算错。


emit:广播

所有监听器同步执行,返回值会被忽略。

实例

javascript

// 触发方:广播一条「就绪」消息

ctx.emit('my-plugin/ready', { id: 'runoob-worker-1' })

// 监听方:收到就绪消息,打印日志

ctx.on('my-plugin/ready', ({ id }) => {

  console.log(`${id} is ready`)

})

bail:短路

监听器按顺序运行,第一个不是 null、false 或 undefined 的返回值会成为最终结果。

实例

javascript

// 分发方:做一次检查,取第一个「有意见」的结果

const result = ctx.bail('some-check', input)

// 监听方:命中拦截条件就返回 'blocked',否则继续

ctx.on('some-check', (input) => {

  if (shouldBlock(input)) return 'blocked'

  // 返回 null / false / undefined 表示「我没意见」,让后面的监听器继续

})

serial:顺序执行

监听器按注册顺序依次执行,并等待异步结果。

第一个不是 null、false 或 undefined 的返回值会终止后续执行。

实例

javascript

// 顺序执行一段分阶段初始化,等每一阶段完成

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

waterfall:流水线

每个监听器可以包装下游返回值,形成处理链。

监听器接收 (...args, next),调用 next() 会执行下游监听器,下游返回值通过 next() 返回给当前包装层。

实例

javascript

// 分发方:初始值就是 input,传给第一个监听器

const output = await ctx.waterfall('my-plugin/transform', input, async () => input)

// 监听方:必须调用 next(),拿到下游结果后做一次加工再返回

ctx.on('my-plugin/transform', async (_input, next) => {

  const downstream = await next()

  return downstream.trim()

})

waterfall 监听器必须调用 next()。不调用 next 会短路整个流水线,这是故意为之的设计,用于实现拦截或网关逻辑。

策略监听器在拥有决策权时,可以不调用 next() 直接返回,从而拦住整条链。

仅做标注或观察的监听器则必须委托给下游。


类型安全的事件

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

实例

python

// 文件路径:scratch-plugin/src/events.ts

import '@deepseek-ai/cordis'

// 声明合并:注册事件名与签名

declare module '@deepseek-ai/cordis' {

  interface Events {

    'my-plugin/ready': (payload: { id: string }) => void

    'my-plugin/check': (input: string) => boolean | undefined

    'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>

  }

}

// 此后 ctx.on('my-plugin/ready', ...) 与 ctx.emit('my-plugin/ready', ...)

// 的参数会被自动推断,拼错事件名或参数类型都会在编译期报错

Cordis 事件与会话记录

Harness 的 Cordis 事件遵循 namespace/action 命名,例如 agent/step、agent/request、agent/request-error、tools/result 和 session/event。

注意区分两类事件:

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

turn/*、step/*、tool/call、tool/result 和 compaction/* 是持久化的会话事件类型,不是同名 Cordis 事件。

需要观察它们时,监听 session/event 并检查 event.type。


动手示例:日志插件

官方文档用下面这个插件记录工具调用与工具结果,监听的是 Cordis 事件 tools/result。

实例

bash

// 文件路径:scratch-plugin/src/tool-logger.ts

import type { Context } from '@deepseek-ai/cordis'

import '@deepseek-ai/dsh-tools'

export const name = 'tool-logger'

export function apply(ctx: Context) {

  // 监听 tools/result 事件:每次工具执行完成都会触发

  ctx.on('tools/result', (exec, result) => {

    // 打印工具名与参数

    console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)

    // 把结果内容里的文本块拼起来,只打印前 100 个字符

    const text = result.content

      .map(block => block.type === 'text' ? block.text : '')

      .join('')

    console.log(`[tool result] ${text.slice(0, 100)}`)

  })

}

这个插件在 Agent 每次调用工具后,把工具名、参数和结果摘要打到终端。

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


小结自测

五种分发模式覆盖「广播、决策、按序、流水线、并行」五类契约;waterfall 必须调用 next();declare module 合并让事件名与参数有类型保障。

自测一下:

  1. 五种分发模式分别用哪个方法触发?哪些有返回值?
  2. waterfall 监听器不调用 next() 会怎样?这是 bug 还是设计?
  3. turn/start 这类持久化会话事件,应该监听哪个事件、检查哪个字段?

AI 思考中...

DeepSeek Harness 服务隔离与作用域

DeepSeek Harness 能力三角色:Definition / Provider / Consumer

基于 VitePress 构建,部署于 GitHub Pages