"""Controller run state model and persistence.""" from __future__ import annotations import json from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from pathlib import Path from typing import Any, Optional def _now() -> str: return datetime.now(timezone.utc).isoformat() @dataclass class ControllerState: controller_run_id: str original_goal: str decision_mode: str # "real" | "stub" iteration_count: int = 0 decisions: list[dict[str, Any]] = field(default_factory=list) linked_bridge_run_ids: list[str] = field(default_factory=list) bridge_run_results: list[dict[str, Any]] = field(default_factory=list) final_status: str = "running" # running | done | failed | stopped recovery_attempted: bool = False # True after rewrite rule applied recovery_reason: Optional[str] = None # rewrite_explain() text or skip reason rewritten_goal: Optional[str] = None # goal after rewrite (if recovery applied) created_at: str = field(default_factory=_now) updated_at: str = field(default_factory=_now) @property def trace_id(self) -> str: """trace_id == controller_run_id by definition.""" return self.controller_run_id def to_dict(self) -> dict[str, Any]: return asdict(self) def touch(self) -> None: self.updated_at = _now() # --------------------------------------------------------------------------- # Persistence helpers # --------------------------------------------------------------------------- def state_path(runs_dir: Path, ctrl_id: str) -> Path: return runs_dir / ctrl_id / "controller_state.json" def save(state: ControllerState, runs_dir: Path) -> None: p = state_path(runs_dir, state.controller_run_id) p.parent.mkdir(parents=True, exist_ok=True) p.write_text(json.dumps(state.to_dict(), indent=2), encoding="utf-8") def load(runs_dir: Path, ctrl_id: str) -> Optional[ControllerState]: p = state_path(runs_dir, ctrl_id) if not p.exists(): return None data = json.loads(p.read_text(encoding="utf-8")) return ControllerState(**data)