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语义
emitEmitAsync(name, args)按序执行,忽略返回
parallelParallelAsync(name, args)全部并发后一起等待
serialSerialAsync(name, args)按序执行;首个非 null/false 值胜出并中断
bailBailAsync(name, args) / BailAsync<T>取首个命中值
waterfallWaterfallAsync<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 章)。

下一章:配置:来自组合文档的插件选项。

在 GitHub 上编辑此页