"""Compact memory for a suspect prompt. Two cheap, bounded sources keep a small model consistent without a vector store: a structured ledger (what has been established) and a rolling transcript buffer. """ from __future__ import annotations from typing import TYPE_CHECKING from ..constants import ROLLING_BUFFER_TURNS from ..schemas.case import CaseFile from ..schemas.suspect import Suspect if TYPE_CHECKING: # avoid a runtime cycle: engine imports the suspects layer from ..engine.game_state import SuspectState def _fact_lookup(case: CaseFile) -> dict[str, str]: return {f.fact_id: f.statement for f in case.facts} def ledger_text(case: CaseFile, suspect: Suspect, state: SuspectState) -> str: facts = _fact_lookup(case) lines: list[str] = [] if state.revealed_fact_ids: admitted = "; ".join(sorted(facts.get(fid, fid) for fid in state.revealed_fact_ids)) lines.append(f"You have already admitted: {admitted}") if state.evidence_shown: shown = "; ".join( sorted(_safe_clue_name(case, cid) for cid in state.evidence_shown) ) lines.append(f"The detective has already shown you: {shown}") if state.broken_lie_ids: topics = "; ".join( sorted( lie.topic for lie in suspect.anchored_lies if lie.lie_id in state.broken_lie_ids ) ) lines.append(f"You have already been caught lying about: {topics}. Do not repeat that lie.") if state.stress >= 0.7: lines.append("You are visibly rattled and close to breaking.") elif state.stress >= 0.4: lines.append("You are tense and guarded.") return "\n".join(lines) if lines else "Nothing has been established yet in this interrogation." def buffer_text(state: SuspectState, n: int = ROLLING_BUFFER_TURNS) -> str: recent = state.transcript[-n:] if not recent: return "(no questions asked yet)" return "\n".join(f'Detective: "{e.question}"\nYou: "{e.answer}"' for e in recent) def _safe_clue_name(case: CaseFile, clue_id: str) -> str: try: return case.clue(clue_id).name except KeyError: return clue_id