"""The dual-output contract for one suspect turn. ``spoken`` is a distinct JSON string node, so hidden state physically cannot leak into the dialogue shown to the player. ``internal`` is parsed for flavour and UI cues only - it is ADVISORY. The deterministic director (engine.director) is the sole authority on whether a lie was caught or a fact was truly revealed. """ from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field from ..constants import DECEPTION_MAX, DECEPTION_MIN from .enums import EvidenceReaction, Intent class InternalState(BaseModel): model_config = ConfigDict(frozen=True, extra="ignore") # Ordered first so the small model "reasons" before committing to a line. private_reasoning: str = "" intent: Intent = Intent.DEFLECT addressed_topic: str = "" is_lying: bool = False active_lie_id: str | None = None lie_refs: tuple[str, ...] = () evidence_reaction: EvidenceReaction = EvidenceReaction.NONE deception_level: int = Field(default=0, ge=DECEPTION_MIN, le=DECEPTION_MAX) stress: float = Field(default=0.0, ge=0.0, le=1.0) revealed_fact_ids: tuple[str, ...] = () contradicted_by_evidence: bool = False consistency_flag: bool = True slip: bool = False class InterrogationTurn(BaseModel): model_config = ConfigDict(frozen=True, extra="ignore") spoken: str narration: str = "" internal: InternalState = InternalState() @staticmethod def safe_default(line: str = "I have nothing more to say about that.") -> InterrogationTurn: """A valid fallback turn used when generation or validation fails.""" return InterrogationTurn(spoken=line, internal=InternalState())