Source code for factor_agent.ledger
"""The trial ledger: one JSON line per candidate, accepted or not."""
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
[docs]
@dataclass(frozen=True)
class TrialRecord:
"""One candidate's outcome.
``status`` is ``"accepted"``, ``"rejected"`` (refused by the reviewer,
unevaluated, or evaluated and refused by admission) or ``"failed"``
(never evaluated: refused by the parser or the Gate after every retry
or, for the rewrite of a candidate the reviewer sent back, once, or its
graph did not compile). Only an evaluated trial, one with
``metrics``, counts toward the evaluated-trial count. ``stage`` is
where it ended: ``"parse"``, ``"gate"``, ``"review"``, ``"evaluate"``
or ``"admission"``. ``candidate`` is the last version the proposer
wrote, ``attempts`` every refused version before it (``item``,
``stage``, ``reasons``), the one the reviewer sent back to be rewritten
with stage ``"review"`` and the reviewer's reason. ``direction`` is the planned direction the
candidate was written for (``mechanism_type``, ``action``, ``parent``,
``direction``). ``stats`` holds the Gate's statistics, ``graph`` the
graph it built (``Graph.to_json``) and ``metrics`` the evaluation, once
there are any; ``confirmation`` the evaluation on the confirmation
segment, which no prompt shows, when the campaign has one.
"""
round: int
candidate: dict
status: str
stage: str
reasons: list[str]
fingerprint: str | None = None
metrics: dict | None = None
stats: dict | None = None
attempts: list[dict] = field(default_factory=list)
direction: dict | None = None
graph: list | None = None
confirmation: dict | None = None
@property
def evaluated(self) -> bool:
"""Whether the Evaluator scored the candidate: it counts toward the evaluated-trial count."""
return self.metrics is not None
[docs]
class Ledger:
"""Append and read the trial records of a run directory."""
def __init__(self, path: str | Path):
self.path = Path(path)
[docs]
def append(self, records: list[TrialRecord]) -> None:
"""Append ``records``; the file and its directory are created when missing."""
self.path.parent.mkdir(parents=True, exist_ok=True)
with self.path.open("a") as out:
for record in records:
out.write(json.dumps(asdict(record)) + "\n")
[docs]
def keep(self, rounds: int) -> None:
"""Drop the records of every round after ``rounds``, such as a round a crash left half written."""
if not self.path.exists():
return
lines = [line for line in self.path.read_text().splitlines() if line]
kept = []
for line in lines:
try:
record = TrialRecord(**json.loads(line))
except (json.JSONDecodeError, TypeError): # a line a crash cut short belongs to an unfinished round
continue
if record.round <= rounds:
kept.append(record)
if len(kept) == len(lines):
return
tmp = self.path.with_suffix(".tmp")
tmp.write_text("".join(json.dumps(asdict(r)) + "\n" for r in kept))
tmp.replace(self.path)
[docs]
def records(self) -> list[TrialRecord]:
"""Every record written so far, in order."""
if not self.path.exists():
return []
return [TrialRecord(**json.loads(line)) for line in self.path.read_text().splitlines() if line]