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

+16
View File
@@ -0,0 +1,16 @@
"""灵台插件化运行时(事件总线 / 插件 / Profile)。Kernel 见 ``app.kernel``。"""
from app.runtime.bus import RunContext, Veto, WaterfallBus
from app.runtime.plugin import Plugin, PluginRegistry
from app.runtime.profile import Profile, ProfileLayer, resolve_profile
__all__ = [
"Plugin",
"PluginRegistry",
"Profile",
"ProfileLayer",
"RunContext",
"Veto",
"WaterfallBus",
"resolve_profile",
]
+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)
+42
View File
@@ -0,0 +1,42 @@
"""插件契约与注册表。
一切能力插件化(docs/architecture/lingtai-runtime.md §4):插件只通过订阅瀑布节点
与提供能力参与运行,彼此不直接调用。M0 只需注册表与装配骨架,首个真实插件(司命
占位裁决器)随 M5 落地;禁域挂载点在 Kernel,不在插件。
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
from app.runtime.bus import WaterfallBus
@runtime_checkable
class Plugin(Protocol):
"""插件契约:稳定标识 + 版本 + 注册入口。"""
@property
def name(self) -> str: ...
@property
def version(self) -> str: ...
def register(self, bus: WaterfallBus) -> None: ...
class PluginRegistry:
"""按注册序持有插件;同名插件后注册覆盖前注册(Profile 层叠语义)。"""
def __init__(self) -> None:
self._plugins: dict[str, Plugin] = {}
def register(self, plugin: Plugin) -> None:
self._plugins[plugin.name] = plugin
def bootstrap(self, bus: WaterfallBus) -> None:
for plugin in list(self._plugins.values()):
plugin.register(bus)
def names(self) -> list[str]:
return list(self._plugins)
+42
View File
@@ -0,0 +1,42 @@
"""Profile:一次 Run 的插件装配清单与参数包。
层叠顺序(产品 PRD §9.2 / lingtai-runtime.md §4.2):
平台硬边界 → 平台目录 → 用户偏好/BYOK → 人物覆写 → 镜树临时选择。
M0 只有平台目录层;后续里程碑逐层接入。Profile 快照写入事件日志,保证可回放。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True)
class ProfileLayer:
"""层叠中的一层:来源标识 + 参数(后者覆盖前者同键)。"""
source: str
params: dict[str, Any] = field(default_factory=dict)
_PLATFORM_LAYER = ProfileLayer(source="platform", params={"slot": "chat"})
@dataclass(frozen=True)
class Profile:
"""解析后的最终装配:插件名列表 + 合并参数。"""
plugins: tuple[str, ...] = ()
params: dict[str, Any] = field(default_factory=dict)
def snapshot(self) -> dict[str, Any]:
"""写入 context.projected 事件的快照视图。"""
return {"plugins": list(self.plugins), "params": dict(self.params)}
def resolve_profile(layers: list[ProfileLayer] | None = None) -> Profile:
"""层叠解析:平台层恒在最底,逐层覆盖参数;M0 无插件装配。"""
merged: dict[str, Any] = {}
for layer in [_PLATFORM_LAYER, *(layers or [])]:
merged.update(layer.params)
return Profile(plugins=(), params=merged)