Files
nextcraft/apps/ai-service/ai_service/agents/lab.py
T
CIAgent 925ab096fb feat(P06): agent re-grounding on real engine inputs + corpus dormancy (Wave 1)
Tasks 6-1-01..04 (REQ-3-007): Lab consumes the LIVE trace digest (compute_digest over
TraceStore events; empty trace coaches the baseline); Assessor renders coaching FROM
the stored GradeRecord (it never invents scores — the grading engine owns that;
evaluate endpoint re-grounded: 404 without a grade); Proctor consumes digest +
DefenseStore long-pause signals + variant seed cross-check. Corpus telemetry/artifacts
DORMANT (headers + AST dormancy test: zero production importers; learner_context stays
active; retained as Phase-3 calibration history). lifespan now adopts a pre-set
provider (state-injection pattern).

v0.2 corpus-path endpoint tests updated honestly to the live contract (learner_id+task_id).
392 tests green; ruff clean.

---ci---
phase: 6
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-007], partial: []}
---/ci---
2026-09-12 05:15:25 +00:00

44 lines
1.6 KiB
Python

"""LabAgent — in-flow feedback over LIVE sandbox telemetry (REQ-3-007).
v0.3 re-grounding: consumes a TraceDigest computed from the learner's real
trace (grading/features.compute_digest over TraceStore events) — the v0.2
corpus scenarios are retired from this path (corpus dormancy, Task 6-1-04).
No session chat — each request is one live-trace read.
"""
from collections.abc import AsyncIterator
from typing import TYPE_CHECKING
from ..config import Settings
from ..corpus.learner_context import LearnerContext, get_learner_context
from ..grading.features import TraceDigest
from ..llm.base import LLMProvider
from ..prompts.lab import SYSTEM_PROMPT, render_context, render_digest_timeline
from .base import BaseAgent
if TYPE_CHECKING: # pragma: no cover
pass
class LabAgent(BaseAgent):
name = "lab"
def __init__(self, provider: LLMProvider, settings: Settings) -> None:
super().__init__(provider, settings)
def system_prompt(self, learner_context: LearnerContext | None = None) -> str:
ctx = learner_context or get_learner_context()
return SYSTEM_PROMPT.format_map(render_context(ctx))
async def stream_feedback(
self,
digest: TraceDigest | None,
learner_context: LearnerContext | None = None,
) -> AsyncIterator[str]:
"""Feedback grounded in the learner's live trace digest."""
timeline = render_digest_timeline(digest)
async for token in self.stream_reply(
history=None, user_input=timeline, learner_context=learner_context
):
yield token