事件系统与 emit 传播语义

约 20 分钟

区分 emit、bail、serial、waterfall 的传播语义,并说明各语义适合的编排场景。

上一节结束时,你已经带着一个能跑通的插件收尾:用 defineTool 声明了带输入 schema 的工具并注册进插件,在 Web UI 会话里让模型真实调用了它,在 Trajectory 视图验证了工具调用与返回结果,也走过一遍描述与 schema 写法的建议、常见故障排查与自检。工具能跑通只是第一步——工具被调用的那一刻,日志、审计、审批这类插件往往都想介入:记录参数、存下轨迹、拦下危险命令。它们怎么知道工具被调用了?靠事件。事件是 Cordis 内核上的插件间通知通道:一个插件发出事件,其他插件按事件类型订阅并响应。工具不需要知道谁在听,谁在听也不需要改动工具。这一节你只做一件事:分清 emit、bail、serial、waterfall 四种传播语义,并为场景选对语义。

核心概念

事件:按类型订阅的通知

类比办公楼广播:行政部对着整栋楼喊“三点消防演练”,你不需要认识行政部的每个人,只需要关心“演练”这一类消息。发件人只管广播,收件人按消息类型决定听不听。技术定义:事件是按类型组织的解耦通知单元,是插件的扩展点。dsh 把运行中的关键环节划分成不同事件领域,例如 session/event、agent/、fs/、tools/、telemetry/。插件在共享上下文上订阅某一事件类型,另一个插件触发该类型时,订阅者被依次唤醒。发出方不持有订阅者引用,订阅者也不修改发出方代码,双方只通过事件类型松耦合。事件类型就是双方约定的频道号,频道对不上,订阅就收不到。

核心概念

emit:广播给所有订阅者

类比群聊通知:你在群里说“发布完成了”,每个成员都收到同一条消息,各回各的,谁也不等谁,谁也不改谁。技术定义:emit 语义把事件广播给该类型下的所有订阅者。每个订阅者都拿到同一份事件;订阅者之间没有数据传递,也不依赖彼此的处理结果;某个订阅者返回了值,也不会阻止其他订阅者被唤醒——这正是它与 bail 的分界。适合“每个都该知道、彼此无依赖”的场景:执行轨迹记录、遥测上报、审计日志可以各挂各的订阅,互不干扰,加一个新的记录插件不需要改任何旧插件。

核心概念

bail:订阅者短路后续处理

类比安检闸口:一排守卫依次检查,任何一人说“不许过”,后面的人就不用再查了。技术定义:bail 语义按订阅顺序逐个唤醒订阅者,一旦某个订阅者返回一个决定性结果——比如拒绝、放行、已处理——传播立即停止,后续订阅者不再被唤醒。它把“谁先做出决定,谁就定音”变成语言层面的保证,而不是靠每个订阅者自行判断。适合审批拦截、权限门禁、唯一处理者场景。这也是实现工具执行前授权检查的语义基础。

核心概念

serial:串行处理顺序敏感逻辑

类比工厂流水线:拧螺丝的工位必须在装外壳的工位之前完成,顺序就是产品质量的一部分。技术定义:serial 语义让订阅者一个接一个执行,前一个完成后,后一个才开始。它保证唤醒次序,但订阅者之间默认不传递数据——顺序本身就是约束。适合必须按固定次序执行的编排:先校验、再变换、后落库;顺序一旦打乱,结果就错。判断标准:如果次序改变会改变结果,选 serial;如果次序改变不影响结果,可能根本不需要 serial。

核心概念

waterfall:级联传递处理结果

类比瀑布下的接力磨坊:上游的水推动第一台磨,第一台磨的出水再推动第二台,能量逐级传递。技术定义:waterfall 语义在串行的基础上多一步——把上一个订阅者的返回值作为下一个订阅者的输入,逐级加工直到产出最终结果。它同时满足两个约束:次序固定,且结果在订阅者之间流动。适合逐步变换数据的编排:原始数据先清洗、再增强、后汇总,每一步的输入都来自上一步的输出。serial 只保证次序,waterfall 在保证次序的同时传递结果——这是两者唯一的区别。

把四种语义放进同一个例子。你在 headless 流水线里给工具执行挂三个插件。第一个做轨迹记录,只关心“发生了什么”,不关心别人怎么处理——选 emit,每个记录者各记各的,互不阻塞。第二个做危险命令门禁,需要“任何一个守卫说不行就停”——选 bail,先到先决。第三个把工具输出规整成统一格式再写入存储,既要固定次序、又要拿到上一步的结果——选 waterfall。如果第三步的各环节之间不传数据,只是“先清洗再入库”,serial 就够。选语义时按顺序问三个问题:所有人都要执行吗?次序重要吗?结果要传递吗?

三个问题的答案落到这张对照表:

语义订阅者关系是否短路结果传递典型场景
emit广播、互相独立否不传递轨迹记录、遥测、审计
bail按序、先到先决是返回值截断传播审批拦截、权限门禁
serial严格按序否不传递校验→变换→落库
waterfall严格按序否逐级传递清洗→增强→汇总

另一个更早的决定是选对事件领域。dsh 把事件划分成 session/event、agent/、fs/、tools/、telemetry/ 等领域,选错领域等于把订阅挂在了错误的频道上。你不需要在每个领域里都写插件,但要能说出自己的功能挂在哪个领域、订阅哪个事件类型。

自检:你要给一次工具调用同时挂两件事:所有记录插件都要把本次调用追加进各自的日志;命令执行前,任一订阅者拒绝就整体终止。两件事分别选哪种传播语义?理由是什么?

记录用 emit:广播给该类型所有订阅者,订阅者互相独立、都要执行,任何一方的结果都不影响别人。拦截用 bail:订阅者按序唤醒,第一个返回决定性结果(拒绝)的订阅者立即终止传播,后续订阅者不再被唤醒。记忆口诀:全都要、无依赖选 emit;先到先决、要短路选 bail;只保次序选 serial;既保次序又传结果选 waterfall。