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

+279
View File
@@ -0,0 +1,279 @@
"""Kernel Run 循环骨架(M0 占位形态)。
事件序列对应 docs/architecture/lingtai-runtime.md §3.1;瀑布节点可被插件否决。
M0 的投影为固定占位(系统契约占位 + 用户输入),M2 起接入镜树祖先路径、获准记忆
与显式引文三类来源。
"""
from __future__ import annotations
import asyncio
from dataclasses import replace
from datetime import datetime, UTC
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.core.ids import new_run_id, new_trace_id
from app.kernel.eventlog import KernelEventLog
from app.kernel.invariants import (
InvariantViolation,
assert_model_visible_logged,
assert_stream_reconstruction,
)
from app.kernel.seat import SeatManager
from app.models.runtime import Run
from app.runtime.bus import (
NODE_LLM_PRE_REQUEST,
NODE_RESULT_PRE_ACCEPT,
NODE_RUN_REQUESTED,
NODE_TURN_STARTED,
RunContext,
Veto,
WaterfallBus,
)
from app.runtime.profile import Profile, resolve_profile
from app.shenshi_client.client import ShenshiClient
from app.shenshi_client.types import ShenshiMessage, ShenshiChatRequest
_SYSTEM_CONTRACT_PLACEHOLDER = "你是魂镜人物运行时(M0 骨架占位契约)。"
class KernelRunLoop:
"""单次 Run 的执行骨架:命台席位 → 事件序列 → 神识流 → 接纳断言。"""
def __init__(
self,
*,
event_log: KernelEventLog,
seats: SeatManager,
bus: WaterfallBus,
shenshi: ShenshiClient,
session_factory: async_sessionmaker[AsyncSession],
timeout_seconds: float,
) -> None:
self._log = event_log
self._seats = seats
self._bus = bus
self._shenshi = shenshi
self._sessions = session_factory
self._timeout = timeout_seconds
async def execute(
self, *, run_id: str, character_id: str, prompt: str, trace_id: str | None = None
) -> None:
trace = trace_id or new_trace_id()
try:
async with asyncio.timeout(self._timeout):
await self._execute(
run_id=run_id, character_id=character_id, prompt=prompt, trace_id=trace
)
except TimeoutError:
await self._finish_interrupted(
run_id, character_id, trace, code="run_timeout", message="运行超时被中断"
)
except asyncio.CancelledError:
await self._emit(run_id, character_id, trace, "run.cancelled", visibility="public")
await self._update_run(run_id, status="cancelled")
raise
async def _execute(
self, *, run_id: str, character_id: str, prompt: str, trace_id: str
) -> None:
ctx = RunContext(run_id=run_id, character_id=character_id, trace_id=trace_id)
try:
await self._bus.fire(NODE_RUN_REQUESTED, ctx)
except Veto as veto:
await self._record_veto(run_id, character_id, trace_id, veto)
await self._finish_interrupted(
run_id, character_id, trace_id, code="HJ-1301", message=veto.reason
)
return
await self._emit(run_id, character_id, trace_id, "run.queued")
async with self._seats.foreground(character_id):
await self._update_run(run_id, status="running")
await self._emit(run_id, character_id, trace_id, "run.started")
await self._turn(run_id, character_id, trace_id, prompt)
async def _turn(
self, run_id: str, character_id: str, trace_id: str, prompt: str
) -> None:
ctx = RunContext(
run_id=run_id, character_id=character_id, trace_id=trace_id, payload={"prompt": prompt}
)
profile: Profile = resolve_profile()
await self._emit(run_id, character_id, trace_id, "turn.started")
await self._bus.fire(NODE_TURN_STARTED, ctx)
request = ShenshiChatRequest(
model=profile.params.get("model"),
slot=str(profile.params.get("slot", "chat")),
messages=[
ShenshiMessage(role="system", content=_SYSTEM_CONTRACT_PLACEHOLDER),
ShenshiMessage(role="user", content=prompt),
],
run_id=run_id,
trace_id=trace_id,
character_id=character_id,
)
await self._emit(
run_id,
character_id,
trace_id,
"context.projected",
payload={
"profile": profile.snapshot(),
"sources": {"system_contract": "placeholder", "user_input": True},
},
visibility="internal",
)
try:
await self._bus.fire(NODE_LLM_PRE_REQUEST, ctx)
except Veto as veto:
await self._record_veto(run_id, character_id, trace_id, veto)
await self._finish_interrupted(
run_id, character_id, trace_id, code="HJ-1301", message=veto.reason
)
return
# "模型可见即可重建"锚点:完整 IR 请求进事件日志
await self._emit(
run_id,
character_id,
trace_id,
"llm.requested",
payload={"request": request.model_dump(mode="json")},
visibility="internal",
)
text_parts: list[str] = []
usage: dict[str, Any] = {}
try:
async for delta in self._shenshi.stream_chat(request):
if delta.delta:
text_parts.append(delta.delta)
await self._emit(
run_id, character_id, trace_id, "llm.delta", payload={"delta": delta.delta}
)
if delta.finish_reason:
usage = {
"prompt_tokens": delta.usage_prompt_tokens,
"completion_tokens": delta.usage_completion_tokens,
"finish_reason": delta.finish_reason,
}
except Exception as exc: # 神识侧失败:如实上报,不伪装完成
await self._emit(
run_id,
character_id,
trace_id,
"llm.failed",
payload={"error": str(exc)},
visibility="internal",
)
await self._finish_interrupted(
run_id, character_id, trace_id, code="HJ-2001", message=f"神识模型调用失败:{exc}"
)
return
await self._emit(run_id, character_id, trace_id, "llm.completed", payload={"usage": usage})
accepted_text = "".join(text_parts)
await self._bus.fire(
NODE_RESULT_PRE_ACCEPT, replace(ctx, payload={"result": accepted_text})
)
events = await self._log.list_by_run(run_id)
try:
assert_model_visible_logged(request, events)
assert_stream_reconstruction(events, accepted_text)
except InvariantViolation as violation:
await self._emit(
run_id,
character_id,
trace_id,
"run.invariant_violation",
payload={"invariant": violation.invariant, "detail": violation.detail},
visibility="audit",
)
await self._finish_interrupted(
run_id,
character_id,
trace_id,
code="HJ-9000",
message=f"Kernel 不变量违反:{violation.invariant}",
)
return
await self._emit(
run_id, character_id, trace_id, "result.accepted", payload={"content": accepted_text}
)
await self._emit(run_id, character_id, trace_id, "turn.finished")
await self._emit(run_id, character_id, trace_id, "run.finished")
await self._update_run(run_id, status="completed")
async def _record_veto(
self, run_id: str, character_id: str, trace_id: str, veto: Veto
) -> None:
await self._emit(
run_id,
character_id,
trace_id,
"safety.ruled",
payload={"reason": veto.reason, "rule": veto.rule, "details": veto.details},
visibility="audit",
)
async def _finish_interrupted(
self,
run_id: str,
character_id: str,
trace_id: str,
*,
code: str,
message: str,
) -> None:
await self._emit(run_id, character_id, trace_id, "run.interrupted")
await self._update_run(run_id, status="interrupted", error_code=code, error_message=message)
async def _emit(
self,
run_id: str,
character_id: str,
trace_id: str,
type: str, # noqa: A002
payload: dict[str, Any] | None = None,
visibility: str = "public",
) -> None:
await self._log.emit(
type=type,
run_id=run_id,
character_id=character_id,
trace_id=trace_id,
payload=payload,
visibility=visibility,
)
async def _update_run(
self,
run_id: str,
*,
status: str,
error_code: str | None = None,
error_message: str | None = None,
) -> None:
async with self._sessions() as session:
run = await session.get(Run, run_id)
if run is None:
return
run.status = status
run.error_code = error_code
run.error_message = error_message
if status == "running":
run.started_at = datetime.now(UTC)
if status in ("completed", "failed", "cancelled", "interrupted"):
run.completed_at = datetime.now(UTC)
await session.commit()
def new_kernel_run_id() -> str:
return new_run_id()