事件系统:三大扩展点
本教程共 32 篇 · 第 12 篇 · 更新于 2026-08-15 · 约 6 分钟阅读
本节目标:分清三大事件域(会话 / Agent / 能力),学会发布与订阅,知道什么场景该选哪个域。
在 dsh 里,事件就是扩展点。想观察 Agent 在干什么?监听 agent/*。想给文件写操作加策略?监听 fs/*。选对事件域,是大多数改动的第一个决定。官方文档把事件分成三大域,各管一摊。
三大事件域
| 事件域 | 代表事件 | 特性 | 什么时候用 |
|---|---|---|---|
| 会话事件 | turn/start、step/start、user/message、assistant/*、tool/call、tool/result | 追加进日志并广播,持久事实 | 事实必须在重新加载后仍然存在 |
| Agent 事件 | agent/pre-step、agent/request、agent/status、agent/turn-stopping | 携带活跃 Agent,实时状态与控制 | 观察或拦截进行中的工作 |
| 能力事件 | tools/*、fs/*、llm/stream、telemetry/* | 向 seam 附加策略与适配器 | 给能力缝挂策略,无需导入循环 |
可以想成三种沟通方式:会话事件是写进档案的纪要,Agent 事件是对讲机里的实时喊话,能力事件是贴在设备上的操作标签。
会话事件:持久事实
会话事件是持久事实:先追加进仅追加日志,再通过 session/event 广播。turn/*、step/*、user/message、assistant/*、tool/* 都属于这一域。判断标准一句话:这个事实在重启后还必须存在吗?要,就用会话事件。
它们的消费方很多:持久化、遥测、会话标题、UI 渲染、SDK 回放都监听 session/event。会话事件不承载「实时控制」语义——它是账本,不是指挥棒。
新手最容易犯的错:该用会话事件记的事实,用 emit 广播完就完事——重启后什么都没留下。
Agent 事件:实时生命周期
Agent 事件(agent/*)携带活跃的 Agent,用于观察或拦截进行中的工作:
| 事件 | 模式 | 用途 |
|---|---|---|
agent/created / agent/disposed | emit | Agent 生灭 |
agent/status | emit | 状态变化 |
agent/inbox/inserted / claimed / discarded | emit | 收件箱事件 |
agent/pre-step | waterfall | 决定模型看到什么(可改写、可拒绝) |
agent/request | waterfall | 替换模型调用配置 |
agent/request-error | waterfall | 请求错误处理(重试等) |
agent/turn-stopping | serial | 轮次停止前拦截(无 next) |
拦截和策略优先用事件;直接能力调用优先用服务方法——这是官方实践规则。想让某轮停止,监听 agent/turn-stopping;想改模型看到的上下文,在 agent/pre-step 上动手。
能力事件:给缝挂策略
能力事件无需导入循环即可向某个 seam 附加策略和适配器。典型例子:
fs/*:fs/write-intent、fs/edit-intent(waterfall,写前策略)、fs/observed(emit,写后观察)tools/*:tools/pre-execute、tools/execute、tools/post-execute(waterfall 执行管线)、tools/result(emit 结果)、tools/change(emit 注册表变化)telemetry/*:session-telemetry/record(waterfall)- 其他缝:
llm/stream(waterfall,流式输出)、approval/request(waterfall,审批策略可代替用户作答)、system-prompt/assemble(waterfall)、credentials/updated、settings/updated、skills/change
tools/pre-execute 是给工具执行加守门逻辑的入口;fs/write-intent 是文件写策略的家。它们与能力缝一一对应,结构上天然解耦。
发布与订阅
订阅用 ctx.on,发布用对应分发方法:
// 订阅(属于 effect,插件卸载时自动移除)
ctx.on('tools/result', (exec, result) => {
console.log(`[tool-logger] ${exec.name}`)
})
// 发布(emit:同步广播,不等待、不收集返回值)
ctx.emit('stats/report', name, next)
分发模式是事件契约的一部分:emit(广播)、parallel(并行等待)、serial(按序、第一个非 null/false/undefined 值胜出)、bail(serial 的同步版本)、waterfall(环绕中间件)。每个事件有且只有一种模式,且只能通过对应方法分发——官方事件参考(subsystems/ 页面)会标注每个事件的模式。
waterfall 有一条纪律:只负责观察或标注的监听器必须调用 next(),不调用直接返回代表有意短路。日志监听器忘了 next(),会悄悄吞掉所有下游默认行为。
人话版:waterfall 是一条链,每个监听器都握着下一环。不调 next(),就是故意把链子剪断。
事件名用 namespace/action 约定保持扁平命名空间可读。类型通过声明合并注册:
declare module '@deepseek-ai/cordis' {
interface Events {
'stats/report'(name: string, count: number): void
}
}
监听方用 import type {} from '...' 引入声明,即获得完整类型。
怎么查谁发谁听
官方文档有一张事件生产方与消费方矩阵(event-producer-consumer):每个 harness 事件列出模式、声明位置、派发方与监听方。例如 tools/result 由 tools 派发,hooks-claude-code、agent-instructions 等监听。想知道「这个事件动了会影响谁」,查矩阵;想知道「这个包发了哪些事件」,查它的子系统页面。
Tip三个问题帮你在三大域里选型:重启后还要不要(会话事件);要不要实时拦在途工作(Agent 事件);是不是给某项能力缝挂策略(能力事件)。三者边界清晰,极少需要跨域。
Warning版本基线
@deepseek-ai/dsh0.1.0-rc.6 处于 Developer Preview,事件清单与模式标注会随版本变化,以官方 event-producer-consumer 矩阵与子系统页面为准。
小结
- 事件就是扩展点,三大域:会话事件(持久)、Agent 事件(实时)、能力事件(挂策略)。
- 订阅用
ctx.on,发布按模式选 emit / parallel / serial / bail / waterfall。 - waterfall 监听器必须调
next(),否则悄悄吞掉下游行为。 - 事件名用
namespace/action约定,类型靠声明合并注册。 - 选域三问:重启后还要不要?要不要拦在途工作?是不是给能力缝挂策略?