Spaces:
Running on Zero
Running on Zero
| """Pydantic models for world artifacts, runtime state, claims, and events. | |
| These mirror the YAML frontmatter / JSON shapes described in PLAN.md sections | |
| 6, 7, and 8. Artifacts are authored as markdown-with-frontmatter; these models | |
| validate the machine-readable parts after parsing. | |
| """ | |
| from __future__ import annotations | |
| from datetime import UTC, datetime | |
| from enum import StrEnum | |
| from typing import Any, Literal | |
| from pydantic import BaseModel, Field | |
| def utcnow_iso() -> str: | |
| """ISO-8601 UTC timestamp used across ledgers and events.""" | |
| return datetime.now(UTC).isoformat() | |
| # -------------------------------------------------------------------------- | |
| # World artifacts (Section 6) | |
| # -------------------------------------------------------------------------- | |
| class TimelineSlice(BaseModel): | |
| """One row of the structured movement schedule — the spine of the case.""" | |
| time_slice: str | |
| character: str | |
| location: str | |
| action: str | |
| observed_by: list[str] = Field(default_factory=list) | |
| class Timeline(BaseModel): | |
| slices: list[TimelineSlice] = Field(default_factory=list) | |
| def for_character(self, name: str) -> list[TimelineSlice]: | |
| return [s for s in self.slices if s.character == name] | |
| def slice_by_id(self, time_slice: str) -> list[TimelineSlice]: | |
| return [s for s in self.slices if s.time_slice == time_slice] | |
| class EnvObject(BaseModel): | |
| """An authored object/detail in the environment with its true state.""" | |
| id: str | |
| location: str | |
| description_true: str | |
| evidential: bool = False | |
| clue: str | None = None | |
| visible_by_default: bool = True | |
| class ClueNode(BaseModel): | |
| """A discoverable clue node with unlock dependencies and exonerations.""" | |
| id: str | |
| reveals: str | |
| sources: list[str] = Field(default_factory=list) | |
| unlocks: list[str] = Field(default_factory=list) | |
| exonerates: list[str] = Field(default_factory=list) | |
| required_for_solution: bool = False | |
| # Optional prerequisites; if absent, derived from other nodes' `unlocks`. | |
| requires: list[str] = Field(default_factory=list) | |
| CrackBehavior = Literal[ | |
| "deflect", "silence", "anger", "lawyer_up", "leave", "partial_confess" | |
| ] | |
| SecretKind = Literal["guilty", "innocent"] | |
| Role = Literal["victim", "suspect", "witness", "accomplice"] | |
| class KnowledgeBoundary(BaseModel): | |
| witnessed: list[str] = Field(default_factory=list) | |
| topics_known: list[str] = Field(default_factory=list) | |
| topics_unknowable: list[str] = Field(default_factory=list) | |
| class CharacterCard(BaseModel): | |
| """Full character model. Frontmatter (machine) + prose (voice).""" | |
| name: str | |
| role: Role = "suspect" | |
| guilty: bool = False | |
| truth: str = "" | |
| knows: KnowledgeBoundary = Field(default_factory=KnowledgeBoundary) | |
| cover: str = "" | |
| never_admit: list[str] = Field(default_factory=list) | |
| cracks_when: str = "" | |
| crack_behavior: CrackBehavior = "deflect" | |
| tells: list[str] = Field(default_factory=list) | |
| secret_kind: SecretKind = "guilty" | |
| exoneration: str | None = None | |
| # Prose voice section (after frontmatter). | |
| voice: str = "" | |
| class Alibi(BaseModel): | |
| """A pre-authored shared lie rehearsed by accomplices.""" | |
| id: str | |
| characters: list[str] = Field(default_factory=list) | |
| agreed_facts: str = "" | |
| agreed_timeline: str = "" | |
| cover_per_character: dict[str, str] = Field(default_factory=dict) | |
| # The points where the alibi is actually false vs. the real timeline. | |
| seams: list[str] = Field(default_factory=list) | |
| body: str = "" | |
| class Solution(BaseModel): | |
| """GROUND TRUTH. Engine-only; never shown to the player pre-accusation.""" | |
| culprit: str | |
| means: str | |
| motive: str | |
| opportunity: str | |
| true_sequence: str = "" | |
| body: str = "" | |
| class WorldMeta(BaseModel): | |
| world_id: str | |
| created: str = Field(default_factory=utcnow_iso) | |
| seed: str = "" | |
| one_line: str = "" | |
| setting: str = "" | |
| twist_tag: str = "" | |
| play_count: int = 0 | |
| title: str = "" | |
| # -------------------------------------------------------------------------- | |
| # Runtime state (Section 7) | |
| # -------------------------------------------------------------------------- | |
| Polarity = Literal["affirm", "deny", "neutral"] | |
| TruthValue = Literal["true", "false", "unknown"] | |
| class Claim(BaseModel): | |
| """A structured proposition extracted from a character utterance.""" | |
| claim_id: str | |
| topic: str | |
| proposition: str | |
| turn: int | |
| polarity: Polarity = "neutral" | |
| engine_truth_value: TruthValue = "unknown" | |
| class LedgerEntry(BaseModel): | |
| """One committed utterance: verbatim + structured claims.""" | |
| turn: int | |
| raw: str | |
| claims: list[Claim] = Field(default_factory=list) | |
| ts: str = Field(default_factory=utcnow_iso) | |
| class CharacterLedger(BaseModel): | |
| character: str | |
| entries: list[LedgerEntry] = Field(default_factory=list) | |
| def claims(self) -> list[Claim]: | |
| out: list[Claim] = [] | |
| for e in self.entries: | |
| out.extend(e.claims) | |
| return out | |
| class TranscriptEvent(BaseModel): | |
| """One ordered line in the session transcript.""" | |
| turn: int | |
| kind: Literal[ | |
| "player", | |
| "character", | |
| "world", | |
| "crack", | |
| "confront", | |
| "accuse", | |
| "system", | |
| ] | |
| actor: str = "" # character/world name, or "player" | |
| text: str | |
| ts: str = Field(default_factory=utcnow_iso) | |
| meta: dict[str, Any] = Field(default_factory=dict) | |
| class SessionStatus(StrEnum): | |
| active = "active" | |
| solved = "solved" | |
| closed = "closed" | |
| class AccusationResult(BaseModel): | |
| culprit_named: str | |
| culprit_correct: bool | |
| means_supported: bool | |
| motive_supported: bool | |
| opportunity_supported: bool | |
| grade: Literal["solved", "right_culprit_unproven", "wrong"] | |
| debrief: str = "" | |
| missed_clues: list[str] = Field(default_factory=list) | |
| class SessionState(BaseModel): | |
| session_id: str | |
| world_id: str | |
| started_at: str = Field(default_factory=utcnow_iso) | |
| status: SessionStatus = SessionStatus.active | |
| turn: int = 0 | |
| discovered_clues: list[str] = Field(default_factory=list) | |
| cracked: list[str] = Field(default_factory=list) # characters in crack state | |
| accusation: AccusationResult | None = None | |
| # -------------------------------------------------------------------------- | |
| # Token ledger (Section 4) | |
| # -------------------------------------------------------------------------- | |
| class UsageRecord(BaseModel): | |
| ts: str = Field(default_factory=utcnow_iso) | |
| world_id: str = "" | |
| session_id: str = "" | |
| task: str = "" | |
| tier: str = "" | |
| provider: str = "" | |
| model: str = "" | |
| prompt_tokens: int = 0 | |
| completion_tokens: int = 0 | |
| total_tokens: int = 0 | |
| ok: bool = True | |
| retries: int = 0 | |