监听事件
一句话版:
ctx.on(event, handler)让插件在会话/工具/agent 生命周期上挂钩:最常用的是工具流水线(tools/pre-execute门禁、tools/post-execute改写)。这是绝大多数"行为扩展"的入口。
一、监听基础
import { Context } from '@deepseek-ai/cordis'
export const name = 'event-watcher'
export function apply(ctx: Context) {
// 会话生命周期
ctx.on('session/created', (session) => {
console.log('新会话:', session.id)
})
// 工具门禁(可 veto):签名 (execution, next),返回 PreToolDecision
ctx.on('tools/pre-execute', async (execution, next) => {
if (execution.name === 'forbidden_tool') {
return { kind: 'deny', reason: '此工具被策略禁止' }
}
return next() // 放行,交给后续监听器
})
// 工具结果改写
ctx.on('tools/post-execute', async (execution, result, next) => {
const decision = await next()
// 检查结果、附加信息、或改判
return decision
})
}
二、事件钩子模式
| 模式 | 用途 | 示例 |
|---|---|---|
| 监听通知 | 只观察 | ctx.on('agent/error', ...) 记录 |
| 门禁 | 拦截/放行 | tools/pre-execute 返回 deny |
| 环绕 | 包一层 | tools/execute 加超时/指标 |
| 改写 | 改结果 | tools/post-execute 附加上下文 |
三、五种 dispatch 模式
| 模式 | 语义 | 典型 |
|---|---|---|
emit | 同步触发,不等 | agent/error |
parallel | 异步并行,await 全部 | session/flush |
serial | 串行依次 | 部分 agent/* |
bail | 首个 bail 即停 | : |
waterfall | 连成 next() 链 | tools/pre-execute、agent/request |
waterfall 必须调
next():漏调会短路整条链。发布侧用ctx.emit()/ctx.parallel()/ctx.waterfall()。
四、可监听事件目录(节选)
| 域 | 事件 |
|---|---|
| agent 生命周期 | agent/created /disposed /status /session-start;waterfall 的 /pre-step /request /request-error;/turn-stopping、agent/inbox/* |
| 会话 | session/created /disposed /event /flush |
| 工具流水线 | tools/pre-execute execute post-execute(waterfall)、tools/code-dispatch-log、tools/result(emit) |
| 跨域 | fs/edit-intent fs/write-intent、llm/stream、approval/request、goal/changed、settings/document-updated、credentials/updated、commands/change |
完整矩阵见
$SRC/docs/event-producer-consumer.md(含每个事件的 dispatch 模式)。
五、双事件平面(别混用)
| 平面 A:Cordis 进程内 | 平面 B:SessionEvent 持久日志 | |
|---|---|---|
| 何时用 | 瞬时信号、插件通信 | 需要持久化/可恢复的事实 |
| 发布 | ctx.emit('my/event', ...) | session.append('my/thing-happened', {...}) |
| 订阅 | ctx.on('my/event', handler) | 只能经 session/event 观察 |
tools/result(宿主内通知)≠tool/result(会话日志事件)。
自定义持久事件要先声明(augment 进 SessionEventMap),否则 session.append 拒绝未知类型:
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
'my-plugin/thing-happened': { type: 'my-plugin/thing-happened'; detail: number }
}
}
下游监听走 session/event,不能直接 ctx.on('my-plugin/thing-happened'):
ctx.on('session/event', (session, event) => {
if (event.type === 'my-plugin/thing-happened') {
console.log('thing:', event.detail)
}
})
六、实战:一个"策略门禁"插件
场景:某环境禁止模型调用 web_fetch,但允许 web_search。
export function apply(ctx: Context) {
ctx.on('tools/pre-execute', async (execution, next) => {
if (execution.name === 'web_fetch') {
return { kind: 'deny', reason: '此环境策略禁止 web_fetch' }
}
return next()
})
}
packages/guard包不是这个门禁:它是循环卫生守卫族(repeat-tool-reminder / timeout-policy),职责不同。allow/deny/ask 通过tools/pre-execute的PreToolDecision实现。
七、验证
# 事件流都在会话 JSONL 里(默认 zstd、两级目录)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | jq -r '.type' | sort | uniq -c