"""Append-only event records with deterministic replay and artifact checks.""" from __future__ import annotations import json import os from datetime import datetime, timezone from pathlib import Path from typing import Any from .schema import canonical, digest class Trace: def __init__(self, path: str | Path): self.path = Path(path) self.path.parent.mkdir(parents=True, exist_ok=True) self.events = self.read(self.path) if self.path.exists() else [] @staticmethod def read(path: Path) -> list[dict]: events = [] previous = "0" * 64 for line_no, line in enumerate(path.read_text().splitlines(), 1): event = json.loads(line) checksum = event.pop("hash") if event["previous"] != previous or event["index"] != len(events) or digest(event) != checksum: raise ValueError(f"trace integrity failure at line {line_no}") event["hash"] = checksum previous = checksum events.append(event) return events def append(self, kind: str, payload: Any) -> dict: event = {"index": len(self.events), "previous": self.events[-1]["hash"] if self.events else "0" * 64, "time": datetime.now(timezone.utc).isoformat(), "kind": kind, "payload": payload} event["hash"] = digest(event) # Flush each record before executing the next external action. with self.path.open("a", encoding="utf-8") as f: f.write(canonical(event) + "\n") f.flush() os.fsync(f.fileno()) self.events.append(event) return event def replay(path: str | Path) -> dict: """Reconstruct scientific state without invoking tools or language models.""" state = {"spec": None, "candidates": {}, "measurements": [], "evidence": {}, "spent": {}, "plan": {}, "artifacts": {}, "stopped": False, "errors": [], "controller_feedback": [], "tool_messages": []} for event in Trace.read(Path(path)): p, kind = event["payload"], event["kind"] if kind == "initialize": if state["spec"] is not None: raise ValueError("duplicate initialization") state["spec"] = p elif kind == "reserve": for key, value in p["cost"].items(): state["spent"][key] = state["spent"].get(key, 0) + value elif kind == "result": from .schema import Candidate for c in p["candidates"]: state["candidates"][Candidate.model_validate(c).id] = c for e in p["evidence"]: state["evidence"][e["id"]] = e state["measurements"].extend(p["measurements"]) state["artifacts"].update(p["artifacts"]) if p.get("message"):state["tool_messages"].append(p["message"]) elif kind == "revise": state["plan"].update(p) elif kind == "error": state["errors"].append(p) elif kind == "feedback": state["controller_feedback"].append(p["error"]) elif kind == "stop": state["stopped"] = True if state["spec"] is None: raise ValueError("trace has no initial specification") return state