区分 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/ 等领域,选错领域等于把订阅挂在了错误的频道上。你不需要在每个领域里都写插件,但要能说出自己的功能挂在哪个领域、订阅哪个事件类型。
bail 是天然的授权门禁语义:把“拒绝”作为订阅者的决定性返回值,就能在工具真正执行前终止链路。这是防御性用法,站在授权边界之内。反过来,用事件广播传递凭据、在门禁之外偷听敏感参数、或用订阅去绕过既有权限检查,都属于越权行为,不在本课程范围内。涉及敏感数据时,让订阅者只接收执行所必需的最小字段,并把日志写进只追加的会话记录,便于事后核对。
自检:你要给一次工具调用同时挂两件事:所有记录插件都要把本次调用追加进各自的日志;命令执行前,任一订阅者拒绝就整体终止。两件事分别选哪种传播语义?理由是什么?
记录用 emit:广播给该类型所有订阅者,订阅者互相独立、都要执行,任何一方的结果都不影响别人。拦截用 bail:订阅者按序唤醒,第一个返回决定性结果(拒绝)的订阅者立即终止传播,后续订阅者不再被唤醒。记忆口诀:全都要、无依赖选 emit;先到先决、要短路选 bail;只保次序选 serial;既保次序又传结果选 waterfall。