런타임 계약과 학습자 흐름 보강

This commit is contained in:
Yun Chan 2026-06-29 08:12:14 +09:00
parent f456b8997a
commit 206018b088
56 changed files with 4306 additions and 1008 deletions

View file

@ -10,6 +10,7 @@ from __future__ import annotations
import asyncio
import json
import logging
import secrets
from datetime import datetime
from typing import Literal, Optional
@ -25,7 +26,16 @@ from ..deps import CurrentPrincipal, Principal, Role
from ..engine_client import EngineError, engine_client
from ..persona_repository import get_catalog_persona
from ..runtime_policy import require_runtime_fallback_allowed
from ..services import evaluator, guardrail, live_coach, memory, orchestrator, rag, state_machine
from ..services import (
evaluator,
guardrail,
live_coach,
memory,
orchestrator,
rag,
session_digest_worker,
state_machine,
)
from ..session_read_model import (
LearnerDashboardResponse,
LearnerSessionsResponse,
@ -58,6 +68,7 @@ from ..session_read_model import (
from ..store import InProcSession, TurnRecord, store
router = APIRouter(prefix="/sessions", tags=["sessions"])
logger = logging.getLogger(__name__)
TheoryMode = Literal["humanistic", "cbt", "integrative"]
EndStateValue = str | int | float | bool | None | dict[str, float]
@ -394,10 +405,12 @@ async def _prepare_turn_context(
card=sess.persona,
state=sess.state,
learner_text=learner_text,
recall_summary=recall.recall_summary,
pinned_facts=recall.pinned_facts,
recent_turns=sess.recent_turns(visible_to="client"),
kb_behavior_cues=kb_cues,
memory=orchestrator.TurnMemory(
recall_summary=recall.recall_summary,
pinned_facts=recall.pinned_facts,
recent_turns=sess.recent_turns(visible_to="client"),
kb_behavior_cues=kb_cues,
),
theory_mode=sess.theory_mode,
)
assert ctx.state_after is not None
@ -502,12 +515,52 @@ async def _end_persisted_session(sess: InProcSession, carry: memory.CarryOver) -
sess.ended = True
sess.ended_at = datetime.now().timestamp()
store.put(sess)
if _should_schedule_session_digest_worker(carry):
asyncio.create_task(_run_session_digest_worker_for_session(sess.session_id))
asyncio.create_task(_write_episodic_embeddings(sess))
return
require_runtime_fallback_allowed("session end")
store.end(sess.session_id)
def _should_schedule_session_digest_worker(carry: memory.CarryOver) -> bool:
return bool(settings.session_digest_worker_enabled and carry.compression_job is not None)
async def _run_session_digest_worker_for_session(session_id: str) -> None:
"""Best-effort M2 LLM digest compressor.
The DB connection is held only for load/apply. Engine generation runs outside
the transaction so a slow provider cannot pin the pool.
"""
try:
db.get_pool()
async with db.acquire(role="admin") as conn:
loaded = await session_digest_worker.load_session_digest_job(conn, session_id)
if loaded is None:
return
model = settings.session_digest_worker_model.strip() or None
worker = await session_digest_worker.run_session_digest_worker(
loaded.job,
engine_client,
existing_case_digest=loaded.existing_case_digest,
model=model,
audit_hook=session_persistence.record_llm_call_audit,
)
if worker.apply_plan is None:
return
async with db.acquire(role="admin") as conn:
await session_digest_worker.apply_session_digest_plan(
conn,
worker.apply_plan,
learner_id=loaded.learner_id,
)
except Exception:
logger.warning("session digest worker failed for session_id=%s", session_id, exc_info=True)
return
async def _write_episodic_embeddings(sess: InProcSession) -> None:
"""Best-effort M2 episodic writer.
@ -601,27 +654,22 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
),
timeout=min(float(settings.engine_timeout), 45.0),
)
status_value = "error" if result.error else "ready"
await session_persistence.save_session_evaluation(
session_id=sess.session_id,
learner_id=sess.learner_id,
status=status_value,
source="engine",
scope=result.scope,
stage=result.stage,
payload=result.to_dict(),
error=result.error,
session_persistence.SessionEvaluationWrite.from_result(
session_id=sess.session_id,
learner_id=sess.learner_id,
result=result,
)
)
except Exception as exc:
await session_persistence.save_session_evaluation(
session_id=sess.session_id,
learner_id=sess.learner_id,
status="error",
source="engine",
scope="session_end",
stage=_stage_label(sess.state.stage),
payload={},
error=str(exc),
session_persistence.SessionEvaluationWrite.from_error(
session_id=sess.session_id,
learner_id=sess.learner_id,
scope="session_end",
stage=_stage_label(sess.state.stage),
error=str(exc),
)
)