4. 事件
服务支持直接调用;事件让插件无需知道有哪些插件正在监听,就能发出通知。harness 用事件处理 工具结果、模型请求、审批决定等交互。
声明、发出与监听
dsh 用 declare module 的 interface Events 合并声明事件签名;C# 版事件名是字符串常量,
类型安全由 ctx.On<T1, T2> 这类泛型重载在调用点落地:
public static class StatsEvents // 事件名常量集中定义(如 LlmEvents.Stream)
{
public const string Report = "stats/report"; // namespace/action 命名约定沿用
}
public sealed class StatsService : CordisService
{
private readonly Dictionary<string, int> _counts = new();
public StatsService(Context context) : base(context) => context.Provide(this);
public void Bump(string name)
{
var next = _counts.TryGetValue(name, out var n) ? n + 1 : 1;
_counts[name] = next;
_ = Context.EmitAsync(StatsEvents.Report, name, next);
}
}
ctx.Plugin(Plugin.From(c => _ = new StatsService(c)));
ctx.Plugin(Plugin.From(c =>
{
c.On<string, int>(StatsEvents.Report, (name, count) =>
Console.WriteLine($"[stats] {name} -> {count}"));
var stats = c.Require<StatsService>();
stats.Bump("tool_call");
stats.Bump("tool_call");
stats.Bump("prompt");
}, typeof(StatsService)));
[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1
ctx.On 属于 effect:监听器随插件一同消失,绝不需要手动维护 Off。
派发模式
EmitAsync 是五种派发模式之一。模式是事件约定的一部分,决定监听器能否返回值、能否并发、能否彼此短路:
| 模式 | C# API | 语义 |
|---|---|---|
| emit | EmitAsync(name, args) | 按序执行,忽略返回 |
| parallel | ParallelAsync(name, args) | 全部并发后一起等待 |
| serial | SerialAsync(name, args) | 按序执行;首个非 null/false 值胜出并中断 |
| bail | BailAsync(name, args) / BailAsync<T> | 取首个命中值 |
| waterfall | WaterfallAsync<T>(name, terminal, args) | 环绕中间件,见下文 |
监听器异常:顺序模式在失败处停止;ParallelAsync 等待全部 settled 后聚合失败。只有显式的
EventService.EmitContained 把监听器失败路由到 internal/error(LifecycleEvents.Error)而不影响触发方——
CordisService 启动失败、后台通知等”绝不能打断主流程”的场景使用它。
waterfall:转换或短路
waterfall 是实现拦截的模式。每个监听器收到参数和一个 next() 续延;它可以转换 next() 的返回值,
也可以不调 next() 直接返回,短路链条其余部分:
// 监听器 1:包装下游结果
ctx.OnWaterfall<string, string>("demo/transform", async (input, next) =>
{
var downstream = await next();
return downstream.ToUpperInvariant();
});
// 监听器 2:拥有决策权时短路
ctx.OnWaterfall<string, string>("demo/transform", (input, next) =>
input.Contains("blocked")
? new ValueTask<string>("** blocked **") // 不调 next:terminal 与更外层下游不再运行
: next());
Console.WriteLine(await ctx.WaterfallAsync("demo/transform",
terminal: () => new ValueTask<string>("hello"), "hello"));
Console.WriteLine(await ctx.WaterfallAsync("demo/transform",
terminal: () => new ValueTask<string>("blocked words"), "blocked words"));
HELLO
** BLOCKED **
WaterfallAsync(src/Cordis/Events.cs)把监听器嵌套在 terminal delegate 外层——只有所有监听器都调
next() 才会走到 terminal。由此得到与 dsh 相同的纪律:只负责观察或标注的 waterfall 监听器必须调用
next();不调就直接返回代表有意短路。日志监听器忘记 next() 会悄无声息地吞掉所有下游默认行为。
harness 用 waterfall 处理协作插件可以包装或回答的决策,例如工具流水线的
RegisterPreExecuteListener / RegisterExecuteListener(第 7 章)。
下一章:配置:来自组合文档的插件选项。