M2a:镜树域后端核心——树/节点/引文/归真/移栽全量 API 与 Kernel 完成回调(芽体固化/残答封存),8 用例钉死 PRD 场景 1/2/3/5/6/7/8
This commit is contained in:
1 parent
1cd0069d65
commit
e6a3b7121f
16 files changed
+2002
-74
No files matched your search
@@ -10,7 +10,7 @@ import asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from app.kernel.eventlog import KernelEventLog
|
||||
from app.kernel.loop import KernelRunLoop
|
||||
from app.kernel.loop import KernelRunLoop, OnRunFinal
|
||||
from app.kernel.seat import SeatManager
|
||||
from app.models.runtime import Run
|
||||
from app.runtime.bus import WaterfallBus
|
||||
@@ -47,7 +47,14 @@ class Kernel:
|
||||
self._tasks: set[asyncio.Task[None]] = set()
|
||||
|
||||
async def start_foreground_run(
|
||||
self, *, character_id: str, prompt: str, trace_id: str | None = None
|
||||
self,
|
||||
*,
|
||||
character_id: str,
|
||||
prompt: str,
|
||||
trace_id: str | None = None,
|
||||
system_contract: str | None = None,
|
||||
origin: str = "kernel",
|
||||
on_run_final: OnRunFinal | None = None,
|
||||
) -> Run:
|
||||
"""创建 Run 行(queued)并异步启动循环;前台席位在循环内排队获取。"""
|
||||
profile = resolve_profile()
|
||||
@@ -56,23 +63,81 @@ class Kernel:
|
||||
character_id=character_id,
|
||||
session_id=None,
|
||||
status="queued",
|
||||
kind="foreground",
|
||||
goal=prompt[:300] if prompt else None,
|
||||
origin="kernel",
|
||||
origin=origin,
|
||||
model_hint=str(profile.params.get("slot", "chat")),
|
||||
)
|
||||
session.add(run)
|
||||
await session.commit()
|
||||
run_id = run.id
|
||||
|
||||
task = asyncio.create_task(
|
||||
self.loop.execute(
|
||||
run_id=run_id, character_id=character_id, prompt=prompt, trace_id=trace_id
|
||||
)
|
||||
self._spawn(
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
prompt=prompt,
|
||||
trace_id=trace_id,
|
||||
seat_key=None,
|
||||
system_contract=system_contract,
|
||||
on_final=on_run_final,
|
||||
)
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(self._tasks.discard)
|
||||
|
||||
async with self._sessions() as session:
|
||||
created = await session.get(Run, run_id)
|
||||
assert created is not None
|
||||
return created
|
||||
|
||||
async def start_rehearsal_run(
|
||||
self, *, embryo_id: str, prompt: str, system_contract: str
|
||||
) -> Run:
|
||||
"""照心试演(幻境 Run):无正式人物、不占人物命台、不产生人物经历。"""
|
||||
profile = resolve_profile()
|
||||
async with self._sessions() as session:
|
||||
run = Run(
|
||||
character_id=None,
|
||||
session_id=None,
|
||||
status="queued",
|
||||
kind="rehearsal",
|
||||
goal=prompt[:300] if prompt else None,
|
||||
origin="embryo_rehearse",
|
||||
model_hint=str(profile.params.get("slot", "chat")),
|
||||
)
|
||||
session.add(run)
|
||||
await session.commit()
|
||||
run_id = run.id
|
||||
self._spawn(
|
||||
run_id=run_id,
|
||||
character_id=None,
|
||||
prompt=prompt,
|
||||
trace_id=None,
|
||||
seat_key=f"embryo:{embryo_id}",
|
||||
system_contract=system_contract,
|
||||
on_final=None,
|
||||
)
|
||||
async with self._sessions() as session:
|
||||
created = await session.get(Run, run_id)
|
||||
assert created is not None
|
||||
return created
|
||||
|
||||
def _spawn(
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
character_id: str | None,
|
||||
prompt: str,
|
||||
trace_id: str | None,
|
||||
seat_key: str | None,
|
||||
system_contract: str | None,
|
||||
on_final: OnRunFinal | None,
|
||||
) -> None:
|
||||
task = asyncio.create_task(
|
||||
self.loop.execute(
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
prompt=prompt,
|
||||
trace_id=trace_id,
|
||||
seat_key=seat_key,
|
||||
system_contract=system_contract,
|
||||
on_final=on_final,
|
||||
)
|
||||
)
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(self._tasks.discard)
|
||||
+89
-25
@@ -1,20 +1,22 @@
|
||||
"""Kernel Run 循环骨架(M0 占位形态)。
|
||||
"""Kernel Run 循环骨架。
|
||||
|
||||
事件序列对应 docs/architecture/lingtai-runtime.md §3.1;瀑布节点可被插件否决。
|
||||
M0 的投影为固定占位(系统契约占位 + 用户输入),M2 起接入镜树祖先路径、获准记忆
|
||||
与显式引文三类来源。
|
||||
投影当前为「调用方组装」形态:prompt 由领域层按镜树祖先链组装后传入,
|
||||
三类投影来源(祖先路径/获准记忆/显式引文)的完整接入随 M3 息壤落地。
|
||||
``RunOutcome`` 携带终态与生成内容,供领域层(镜树节点固化等)在 Run 结束时回调。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, UTC
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field, replace
|
||||
from datetime import UTC, datetime
|
||||
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.core.ids import new_trace_id
|
||||
from app.kernel.eventlog import KernelEventLog
|
||||
from app.kernel.invariants import (
|
||||
InvariantViolation,
|
||||
@@ -34,13 +36,29 @@ from app.runtime.bus import (
|
||||
)
|
||||
from app.runtime.profile import Profile, resolve_profile
|
||||
from app.shenshi_client.client import ShenshiClient
|
||||
from app.shenshi_client.types import ShenshiMessage, ShenshiChatRequest
|
||||
from app.shenshi_client.types import ShenshiChatRequest, ShenshiMessage
|
||||
|
||||
_SYSTEM_CONTRACT_PLACEHOLDER = "你是魂镜人物运行时(M0 骨架占位契约)。"
|
||||
_SYSTEM_CONTRACT_PLACEHOLDER = "你是魂镜人物运行时(骨架占位契约)。"
|
||||
|
||||
# Run 结束回调:(生成内容, 终态)。终态仅 completed / interrupted。
|
||||
OnRunFinal = Callable[[str, str], Awaitable[None]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunOutcome:
|
||||
"""Run 终态载体:循环内各终止路径写入,execute 返回前触发回调。"""
|
||||
|
||||
status: str = "running" # running / completed / interrupted / cancelled
|
||||
content: str = ""
|
||||
error_code: str = ""
|
||||
extras: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class KernelRunLoop:
|
||||
"""单次 Run 的执行骨架:命台席位 → 事件序列 → 神识流 → 接纳断言。"""
|
||||
"""单次 Run 的执行骨架:命台席位 → 事件序列 → 神识流 → 接纳断言。
|
||||
|
||||
``character_id`` 允许为空(照心试演无正式人物),此时必须提供 ``seat_key``。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -60,30 +78,62 @@ class KernelRunLoop:
|
||||
self._timeout = timeout_seconds
|
||||
|
||||
async def execute(
|
||||
self, *, run_id: str, character_id: str, prompt: str, trace_id: str | None = None
|
||||
) -> None:
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
character_id: str | None,
|
||||
prompt: str,
|
||||
trace_id: str | None = None,
|
||||
seat_key: str | None = None,
|
||||
system_contract: str | None = None,
|
||||
on_final: OnRunFinal | None = None,
|
||||
) -> RunOutcome:
|
||||
assert character_id or seat_key, "Run 必须绑定人物或提供席位键(试演)"
|
||||
trace = trace_id or new_trace_id()
|
||||
outcome = RunOutcome()
|
||||
try:
|
||||
async with asyncio.timeout(self._timeout):
|
||||
await self._execute(
|
||||
run_id=run_id, character_id=character_id, prompt=prompt, trace_id=trace
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
prompt=prompt,
|
||||
trace_id=trace,
|
||||
seat_key=seat_key,
|
||||
system_contract=system_contract,
|
||||
outcome=outcome,
|
||||
)
|
||||
except TimeoutError:
|
||||
outcome.status = "interrupted"
|
||||
outcome.error_code = "run_timeout"
|
||||
await self._finish_interrupted(
|
||||
run_id, character_id, trace, code="run_timeout", message="运行超时被中断"
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
outcome.status = "cancelled"
|
||||
await self._emit(run_id, character_id, trace, "run.cancelled", visibility="public")
|
||||
await self._update_run(run_id, status="cancelled")
|
||||
raise
|
||||
|
||||
if on_final is not None and outcome.status in ("completed", "interrupted"):
|
||||
await on_final(outcome.content, outcome.status)
|
||||
return outcome
|
||||
|
||||
async def _execute(
|
||||
self, *, run_id: str, character_id: str, prompt: str, trace_id: str
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
character_id: str | None,
|
||||
prompt: str,
|
||||
trace_id: str,
|
||||
seat_key: str | None,
|
||||
system_contract: str | None,
|
||||
outcome: RunOutcome,
|
||||
) -> 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:
|
||||
outcome.status, outcome.error_code = "interrupted", "HJ-1301"
|
||||
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
|
||||
@@ -91,13 +141,26 @@ class KernelRunLoop:
|
||||
return
|
||||
|
||||
await self._emit(run_id, character_id, trace_id, "run.queued")
|
||||
async with self._seats.foreground(character_id):
|
||||
async with self._seats.foreground(seat_key or character_id or run_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)
|
||||
await self._turn(
|
||||
run_id,
|
||||
character_id,
|
||||
trace_id,
|
||||
prompt,
|
||||
system_contract or _SYSTEM_CONTRACT_PLACEHOLDER,
|
||||
outcome,
|
||||
)
|
||||
|
||||
async def _turn(
|
||||
self, run_id: str, character_id: str, trace_id: str, prompt: str
|
||||
self,
|
||||
run_id: str,
|
||||
character_id: str | None,
|
||||
trace_id: str,
|
||||
prompt: str,
|
||||
system_contract: str,
|
||||
outcome: RunOutcome,
|
||||
) -> None:
|
||||
ctx = RunContext(
|
||||
run_id=run_id, character_id=character_id, trace_id=trace_id, payload={"prompt": prompt}
|
||||
@@ -110,7 +173,7 @@ class KernelRunLoop:
|
||||
model=profile.params.get("model"),
|
||||
slot=str(profile.params.get("slot", "chat")),
|
||||
messages=[
|
||||
ShenshiMessage(role="system", content=_SYSTEM_CONTRACT_PLACEHOLDER),
|
||||
ShenshiMessage(role="system", content=system_contract),
|
||||
ShenshiMessage(role="user", content=prompt),
|
||||
],
|
||||
run_id=run_id,
|
||||
@@ -124,13 +187,14 @@ class KernelRunLoop:
|
||||
"context.projected",
|
||||
payload={
|
||||
"profile": profile.snapshot(),
|
||||
"sources": {"system_contract": "placeholder", "user_input": True},
|
||||
"sources": {"system_contract": "assembled", "user_input": True},
|
||||
},
|
||||
visibility="internal",
|
||||
)
|
||||
try:
|
||||
await self._bus.fire(NODE_LLM_PRE_REQUEST, ctx)
|
||||
except Veto as veto:
|
||||
outcome.status, outcome.error_code = "interrupted", "HJ-1301"
|
||||
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
|
||||
@@ -163,6 +227,7 @@ class KernelRunLoop:
|
||||
"finish_reason": delta.finish_reason,
|
||||
}
|
||||
except Exception as exc: # 神识侧失败:如实上报,不伪装完成
|
||||
outcome.status, outcome.error_code = "interrupted", "HJ-2001"
|
||||
await self._emit(
|
||||
run_id,
|
||||
character_id,
|
||||
@@ -187,6 +252,7 @@ class KernelRunLoop:
|
||||
assert_model_visible_logged(request, events)
|
||||
assert_stream_reconstruction(events, accepted_text)
|
||||
except InvariantViolation as violation:
|
||||
outcome.status, outcome.error_code = "interrupted", "HJ-9000"
|
||||
await self._emit(
|
||||
run_id,
|
||||
character_id,
|
||||
@@ -210,9 +276,11 @@ class KernelRunLoop:
|
||||
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")
|
||||
outcome.status = "completed"
|
||||
outcome.content = accepted_text
|
||||
|
||||
async def _record_veto(
|
||||
self, run_id: str, character_id: str, trace_id: str, veto: Veto
|
||||
self, run_id: str, character_id: str | None, trace_id: str, veto: Veto
|
||||
) -> None:
|
||||
await self._emit(
|
||||
run_id,
|
||||
@@ -226,7 +294,7 @@ class KernelRunLoop:
|
||||
async def _finish_interrupted(
|
||||
self,
|
||||
run_id: str,
|
||||
character_id: str,
|
||||
character_id: str | None,
|
||||
trace_id: str,
|
||||
*,
|
||||
code: str,
|
||||
@@ -238,7 +306,7 @@ class KernelRunLoop:
|
||||
async def _emit(
|
||||
self,
|
||||
run_id: str,
|
||||
character_id: str,
|
||||
character_id: str | None,
|
||||
trace_id: str,
|
||||
type: str, # noqa: A002
|
||||
payload: dict[str, Any] | None = None,
|
||||
@@ -273,7 +341,3 @@ class KernelRunLoop:
|
||||
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()
|
||||
Reference in new issue
Block a user