ByteNoteByteNote
DeepSeek Harness 插件事件系统精读
字

字节笔记本

2026年10月6日 · 约 8 分钟读完

DeepSeek Harness 插件事件系统精读

API中转
¥120

DeepSeek Harness(简称 dsh)是 DeepSeek AI 开源的 agent harness,采用 MIT 协议,目前处于开发者预览阶段。它的架构原则只有一句话:一切皆插件。整个产品构建在 Cordis 插件框架之上,插件之间互相不知道对方的存在,协作靠两条通道完成:服务解决「我知道该找谁」的直接调用,事件解决「我不知道谁在听」的广播通知。Harness 大量使用事件来实现松耦合的扩展点,插件开发指南里专门有一篇讲事件系统,本文把它的要点整理出来。

监听与触发

事件的用法只有两个动作。监听用 ctx.on,触发用 ctx.emit:

ts
ctx.on('event-name', (payload) => {
  // 处理事件
})

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

同一个事件名可以有任意多个监听器,emit 一次,所有监听器都会被调用。

四种派发模式

Cordis 的事件不只是「发出去就算了」,一个事件采用哪种派发模式,决定了监听器返回值的命运。指南列了四种模式,各自对应一种交互契约。

Cordis 事件四种派发模式与 waterfall 的 next() 纪律

emit 是广播。所有监听器同步执行,返回值直接被忽略,适合「某件事发生了」这类纯通知:

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

ctx.on('my-plugin/ready', ({ id }) => {
  console.log(`${id} is ready`)
})

bail 是短路。监听器按注册顺序运行,第一个返回值不是 null、false 或 undefined 的监听器会成为最终结果,后面的监听器不再执行。它适合检查、表决类场景:一旦有人给出明确结论,就没有必要再问其他人:

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

ctx.on('some-check', (input) => {
  if (shouldBlock(input)) return 'blocked'
  // 返回 null、false 或 undefined,表示交由下一个监听器处理
})

serial 是顺序执行的异步版本。监听器按注册顺序依次执行,异步结果会被等待,第一个不是 null、false 或 undefined 的返回值会终止后续执行,适合初始化这类必须一环接一环的阶段:

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

waterfall 是流水线。每个监听器除了接收事件参数,还会拿到一个 next() 函数,调用 next() 就把控制权交给链条的下一环,拿到下游返回值后可以再加工,层层包装形成处理链:

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

ctx.on('my-plugin/transform', async (_input, next) => {
  const downstream = await next()
  return downstream.trim()
})

这里有一条必须记住的纪律:waterfall 监听器必须调用 next()。不调用就等于把整条流水线短路,这是故意的设计,用来实现拦截和网关逻辑。换句话说,如果你写的监听器只想观察或标注而不是想拦截,漏调 next() 会无声无息地吞掉所有下游行为,而且不会有任何报错。

类型安全的事件

事件名是字符串,拼错了怎么办?Harness 用 TypeScript 的声明合并解决这个问题:在模块声明里给 @deepseek-ai/cordis 的 Events 接口补充成员,把事件名和监听器签名登记进去,之后 ctx.on 和 ctx.emit 都能自动推断类型,事件名拼错、参数个数不对,编译期就会报错:

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>
  }
}

命名约定与会话事件的边界

Harness 的 Cordis 事件遵循 namespace/action 命名,例如 agent/step、agent/request、agent/request-error、tools/result 和 session/event。每个事件的完整签名与触发模式,可以查仓库子系统文档里生成的 cordis-surface 区块。

会话事件类型与 Cordis 事件的观察方式区别

容易混淆的是另一类名字:turn/、step/、tool/call、tool/result 和 compaction/* 是持久化的会话事件类型,并不是同名的 Cordis 事件。想观察它们,不能直接监听这些字符串,正确做法是监听 session/event,在回调里检查 event.type。

监听器也是效果

通过 ctx.on() 注册的监听器是一个 effect,生命周期与插件绑定,插件卸载时自动移除,不需要手写解绑逻辑:

ts
export function apply(ctx: Context) {
  // 插件卸载时,这个监听器会被自动移除
  ctx.on('tools/result', handler)
}

示例:工具日志插件

把上面几件事拼起来,就是一个能用的日志插件:监听 tools/result,把每次工具调用的名字、参数和结果前 100 个字符记下来。

ts
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) => {
    console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
    const text = result.content
      .map(block => block.type === 'text' ? block.text : '')
      .join('')
    console.log(`[tool result] ${text.slice(0, 100)}`)
  })
}

开头那行对 dsh-tools 的导入只为引入工具子系统的类型声明,让 tools/result 的回调参数带着完整类型;真正的逻辑只有一个 ctx.on。

小结

四种派发模式对应四种交互契约:emit 负责通知,bail 负责表决,serial 负责串行把关,waterfall 负责链式加工与拦截。写插件前先想清楚自己需要哪一种契约;要观察会话层面的持久化事件,记住统一走 session/event。事件加类型合并,构成了这个框架松耦合而不失控的底层机制。

相关文章

分享: