M0:灵台 Kernel 骨架(事件溯源/命台席位/瀑布总线/插件注册)与神识客户端边界,新增 POST /runs 占位闭环与测试,Alembic 基线合并为单一起点

This commit is contained in:
chenxin committed 2026-08-21 21:58:09 +08:00
1 parent 99e55fa0a8
commit 7ef3a15061
23 files changed
+2014 -1399

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
"""运行时事件总线:瀑布节点 + 可否决监听器。
设计依据 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)