"""File-backed run receipt store.""" from __future__ import annotations import json from pathlib import Path from .models import RunRecord, SkillEffectRecord class RunMemoryStore: def __init__(self, root: str | Path) -> None: self.root = Path(root) self.root.mkdir(parents=True, exist_ok=True) self.runs_path = self.root / "runs.jsonl" self.effects_path = self.root / "skill-effects.jsonl" def append_run_record(self, record: RunRecord) -> None: self._append_jsonl(self.runs_path, record.to_dict()) def update_run_record(self, run_id: str, **updates: object) -> RunRecord | None: records = self.list_runs() updated: RunRecord | None = None for index, record in enumerate(records): if record.run_id != run_id: continue payload = record.to_dict() payload.update(updates) updated = RunRecord.from_dict(payload) records[index] = updated break if updated is None: return None self.runs_path.parent.mkdir(parents=True, exist_ok=True) self.runs_path.write_text( "".join( json.dumps(record.to_dict(), ensure_ascii=False, sort_keys=True) + "\n" for record in records ), encoding="utf-8", ) return updated def append_skill_effect(self, effect: SkillEffectRecord) -> None: self._append_jsonl(self.effects_path, effect.to_dict()) def list_runs(self) -> list[RunRecord]: return [RunRecord.from_dict(item) for item in self._read_jsonl(self.runs_path)] def list_runs_by_skill(self, skill_name: str, version: str | None = None, limit: int | None = None) -> list[RunRecord]: results: list[RunRecord] = [] for record in self.list_runs(): matched = False for receipt in record.activated_skills: if receipt.skill_name != skill_name: continue if version is not None and receipt.skill_version != version: continue matched = True break if matched: results.append(record) if limit is not None: return results[-limit:] return results def list_skill_effects(self, skill_name: str, version: str | None = None, limit: int | None = None) -> list[SkillEffectRecord]: results: list[SkillEffectRecord] = [] for payload in self._read_jsonl(self.effects_path): effect = SkillEffectRecord.from_dict(payload) if effect.skill_name != skill_name: continue if version is not None and effect.skill_version != version: continue results.append(effect) if limit is not None: return results[-limit:] return results @staticmethod def _append_jsonl(path: Path, payload: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as handle: handle.write(json.dumps(payload, ensure_ascii=False, sort_keys=True) + "\n") @staticmethod def _read_jsonl(path: Path) -> list[dict]: if not path.exists(): return [] results: list[dict] = [] for line in path.read_text(encoding="utf-8").splitlines(): cleaned = line.strip() if not cleaned: continue payload = json.loads(cleaned) if isinstance(payload, dict): results.append(payload) return results