"""Contracts: the serialised research contract P=(Q,Θ,D,A,G,V,B,R,L), run manifests, hashing helpers. The contract components map to concrete files: Q, Θ, D -> TASK.md (question, estimands, inputs, information-availability rules) [task bundle] A -> launcher flags (tool inventory, permission policy, model) [configs/*.yaml] G -> phase graph in the treatment protocol (arms B/C/D) and the harness state machine V -> gate_spec.json (oracle-free runtime checks) + hidden evaluate.py (final evaluation) B -> budget envelope (USD cap, wall seconds, turns per session, gate/review cycles) R -> recovery rules (protocol text) + harness resume/retry policy L -> run manifest, event stream, receipts, screenshot manifest (SQLite + files) """ from __future__ import annotations import dataclasses, hashlib, json, os, pathlib, datetime, uuid from dataclasses import dataclass, field, asdict from typing import Optional ARMS = ("A", "B", "C", "D") RUN_STATES = ("not_started", "prepared", "running", "completed", "timeout", "budget_exhausted", "error", "interrupted", "evaluated", "void") # allowed transitions of a run (harness state machine) RUN_TRANSITIONS = { "not_started": {"prepared", "error", "void"}, "prepared": {"running", "error", "void"}, "running": {"completed", "timeout", "budget_exhausted", "error", "interrupted"}, "interrupted": {"running", "error"}, # resume after a standardized fault "completed": {"evaluated", "error"}, "timeout": {"evaluated", "error"}, "budget_exhausted": {"evaluated", "error"}, "error": {"evaluated", "prepared"}, # infra error: can be re-prepared under the rerun policy "evaluated": set(), "void": set(), } PHASE_STATES = ("pending", "running", "passed", "failed", "blocked", "stale", "skipped_optional") def utcnow() -> str: return datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="milliseconds") def sha256_file(path) -> str: h = hashlib.sha256() with open(path, "rb") as fh: for chunk in iter(lambda: fh.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def sha256_bytes(b: bytes) -> str: return hashlib.sha256(b).hexdigest() def sha256_text(s: str) -> str: return hashlib.sha256(s.encode("utf-8")).hexdigest() def dir_manifest(root, exclude=()) -> dict: out = {} root = pathlib.Path(root) for p in sorted(root.rglob("*")): if p.is_file(): rel = p.relative_to(root).as_posix() if any(rel == e or rel.startswith(e.rstrip("/") + "/") for e in exclude): continue out[rel] = sha256_file(p) return out @dataclass class Budget: max_budget_usd: float = 4.0 wall_seconds: int = 1500 max_turns_per_session: int = 80 max_gate_cycles: int = 3 # arms C/D: controller-enforced gate feedback cycles max_review_cycles: int = 1 # arm D min_continuation_usd: float = 0.25 @dataclass class RunManifest: run_id: str task_id: str split: str treatment: str repeat_index: int block_id: str = "" order_in_block: int = 0 node: str = "" task_hash: Optional[str] = None protocol_hash: Optional[str] = None effective_context_hash: Optional[str] = None code_commit: Optional[str] = None environment_id: Optional[str] = None client_version: Optional[str] = None model_requested: Optional[str] = None model_reported: Optional[str] = None session_ids: list = field(default_factory=list) started_at_utc: Optional[str] = None ended_at_utc: Optional[str] = None status: str = "not_started" terminal_reason: Optional[str] = None budget: dict = field(default_factory=dict) usage: dict = field(default_factory=dict) human_intervention_ids: list = field(default_factory=list) artifact_manifest: Optional[dict] = None evaluation_receipt: Optional[str] = None screenshot_ids: list = field(default_factory=list) fault: Optional[dict] = None notes: str = "" def to_json(self) -> str: return json.dumps(asdict(self), indent=1, sort_keys=True) def new_run_id(split: str, seq: int) -> str: return f"R{split[0].upper()}{seq:04d}" def new_session_id() -> str: return str(uuid.uuid4())