事件系统
一句话版:DSH 有两套事件。Cordis 进程内事件(
ctx.on,插件间通信、hook 链)与 SessionEvent 会话日志(持久化、事件溯源)。它们不是一回事:别把"监听"和"持久化"混为一谈。
这一篇是 DSH"插件间怎么通信、会话怎么被记录"的总纲。读完你清楚:什么时候用 ctx.on,什么时候用 session.append,事件都有哪些,(复数的 tools/) 和 (单数的 tool/) 有什么区别。
一、两套事件平面
| Cordis 进程内事件 | SessionEvent 会话日志 | |
|---|---|---|
| 载体 | 内存,ctx.on/ctx.emit | session append-only 日志(默认 zstd 压缩) |
| 语义 | 瞬时通知、hook 链 | 事件溯源、可恢复的会话事实 |
| 生命 | 随插件卸载自动注销 | 持久化,重启不丢 |
| 例 | agent/status、tools/pre-execute、session/created | user/message、request/header、tool/call、tool/result |
关键:会话日志("唯一真源")承载遥测、投影、恢复;Cordis 事件是进程内反应式扩展。别以为挂 ctx.on 就会落持久化:要持久化用 session.append。
二、五类分发给式(ctx.emit/*)
Cordis 事件的分发模式决定监听器怎么被组合:
| 模式 | 语义 | 典型 |
|---|---|---|
emit | 同步触发,不等监听器 | agent/error |
parallel | 异步并行,await 全部 | session/flush |
serial | 串行依次 | 部分 agent/* |
bail | 首个 bail 结果即停 | : |
waterfall | 监听器连成 next() 链,next() 返回值递下一个 | tools/pre-execute、agent/request |
waterfall 必须调
next():漏调会短路整条链。发布侧用ctx.emit()/ctx.parallel()/ctx.waterfall()。
三、agent/* :agent 生命周期与控制面
| 事件 | 时机 / dispatch |
|---|---|
agent/created / agent/disposed | 创建 / 销毁(配对) |
agent/status | 运行状态变迁 |
agent/pre-step | 每 step 装配(waterfall) |
agent/request | 请求装配替换(waterfall) |
agent/request-error | 模型请求失败恢复(waterfall,dsh-llm-retry 挂这) |
agent/turn-stopping | 轮次开始停止(serial) |
agent/error / agent/session-start | 错误 / 会话开始 |
agent/inbox/* | inserted / claimed / discarded(见主循环) |
agent/* 是控制面;真正的对话内容走 session 日志。
四、tools 流水线(Cordis,复数 tools/)
每次工具调用的扩展点,从 Agent 主循环 延伸:
tools/result(复数,Cordis 通知)≠tool/result(单数,会话日志事件):前者是进程内通知,后者是持久化会话事件。这是最容易混的一对。
五、SessionEvent 会话日志词汇
单数 tool/,持久化。一个会话日志:
{"type":"session","version":0,"id":"...","createdAt":0}
{"type":"user/message","seq":1,"data":{...}}
{"type":"request/header","seq":2,"data":{...}}
{"type":"tool/call","seq":3,"data":{...}}
{"type":"tool/result","seq":4,"data":{...}}
常见类型:user/message、assistant/message、assistant/chunk、request/header、request/context、tool/call、tool/result、step/start、turn/start、turn/end、session/end-seed、compaction/*、permission/preset、feedback/record、todo/write、goal/change。完整目录见 $SRC/packages/core/session/src/known-event-types.ts。
事件信封字段(每个 event 可能有):
| 字段 | 含义 |
|---|---|
seq | 单调递增的持久化排序键 |
sourceEventSeqs | 引用的源事件 seq(chunk→message、压缩替换→被遮蔽条目) |
surfaceOp | append/replace,仅 user/message/assistant/message/tool/result 合法 |
ignorable | 读到不认识可跳过;缺失 = 必选,未知类型拒绝重建 |
六、事件类型约定与格式闸门
seq单调递增(排序键)- 格式版本闸门:
SESSION_FORMAT_VERSION不匹配(final > 支持)时,加载直接拒绝(no-migration),提示升级 harness - 未知类型:无
ignorable→ 拒绝;有ignorable→ 跳过 - 遥测 / 投影 / 恢复消费同一事件流(这就是"日志是唯一真源")
七、插件的两类参与
| 你想 | 怎么做 |
|---|---|
| 监听反应式扩展 | ctx.on('tools/result', handler)(Cordis) |
| 发布持久事实 | session.append(type, data),然后 await ctx.sessions.flush(session) |
| 自定义持久事件 | 需先 declare module '@deepseek-ai/dsh-session/types' augment SessionEventMap,否则 append 拒绝未知类型 |
| 自定义 Cordis 事件 | ctx.emit,随插件卸载自动清理 |
详细的双事件平面 + 代码见 监听事件。
八、消费同一流的几个 seam
| seam | 用事件流干什么 |
|---|---|
session-persistence | 存储/重载(eager write-behind) |
session-projection | 派生视图(缓存) |
session-telemetry | OTel 导出 |
session-title | 标题生成 |
| UI | session/event + agent/* 控制事件渲染对话 |
九、验证
# 事件流 = 完整会话日志(默认 zstd 压缩、两级目录)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | jq -r '.type' | sort | uniq -c | sort -rn | head -12
# 看 agent 控制事件
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"agent/' | head