"""运行时事件总线:瀑布节点 + 可否决监听器。 设计依据 docs/architecture/lingtai-runtime.md §1/§2:注入、审计、安全都在事件层做, 不改循环骨架。监听器按注册序执行;任一监听器抛 ``Veto`` 即中断该节点(否决), 否决事实由调用方记录(safety.ruled 等事件),总线本身不落库。 """ from __future__ import annotations import fnmatch from collections import defaultdict from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from typing import Any # 瀑布节点名(契约词典 §8.3 的可否决子集) NODE_RUN_REQUESTED = "run.requested" NODE_TURN_STARTED = "turn.started" NODE_LLM_PRE_REQUEST = "llm.pre_request" NODE_TOOL_PRE_EXECUTE = "tool.pre_execute" NODE_RESULT_PRE_ACCEPT = "result.pre_accept" WATERFALL_NODES = frozenset( { NODE_RUN_REQUESTED, NODE_TURN_STARTED, NODE_LLM_PRE_REQUEST, NODE_TOOL_PRE_EXECUTE, NODE_RESULT_PRE_ACCEPT, } ) class Veto(Exception): """监听器否决:携带裁决摘要(reason / 规则标识)。""" def __init__(self, reason: str, *, rule: str = "", details: dict[str, Any] | None = None): super().__init__(reason) self.reason = reason self.rule = rule self.details = details or {} @dataclass(frozen=True) class RunContext: """节点监听器可见的运行上下文(只读视图,变更经事件日志)。""" run_id: str character_id: str trace_id: str | None = None payload: dict[str, Any] = field(default_factory=dict) Listener = Callable[[str, RunContext], Awaitable[None]] class WaterfallBus: """瀑布节点订阅与触发。""" def __init__(self) -> None: self._listeners: dict[str, list[Listener]] = defaultdict(list) def on(self, node: str, listener: Listener) -> None: """注册节点监听器;支持 glob 通配(如 ``llm.*``)。""" if not any(fnmatch.fnmatch(node, pattern) for pattern in WATERFALL_NODES): raise ValueError(f"未知瀑布节点:{node}") self._listeners[node].append(listener) async def fire(self, node: str, context: RunContext) -> None: """按注册序触发监听器;任一否决即停止后续。 精确键与通配键(如 ``llm.*``)各自成列表,列表内保持注册序; 两种键之间的相对顺序不保证,需要严格顺序的场景用精确注册。 """ for key, listeners in self._listeners.items(): if key != node and not fnmatch.fnmatch(node, key): continue for listener in list(listeners): await listener(node, context)