1"""Resource envelope accounting: one envelope per run, shared by executor, gate cycles and reviewer sessions."""2from __future__ import annotations3from dataclasses import dataclass, field45@dataclass6class Envelope:7 max_budget_usd: float8 wall_seconds: int9 max_turns_per_session: int10 spent_usd: float = 0.011 spent_wall_s: float = 0.012 sessions: list = field(default_factory=list) # [{"session_id","role","cost","wall_s","turns"}]13 min_continuation_usd: float = 0.251415 def remaining_usd(self) -> float:16 return max(0.0, self.max_budget_usd - self.spent_usd)1718 def remaining_wall(self) -> int:19 return int(max(0, self.wall_seconds - self.spent_wall_s))2021 def can_continue(self, min_wall_s: int = 120) -> bool:22 return self.remaining_usd() >= self.min_continuation_usd and self.remaining_wall() >= min_wall_s2324 def charge(self, session_id: str, role: str, cost: float, wall_s: float, turns: int | None):25 cost = float(cost or 0.0)26 self.spent_usd += cost; self.spent_wall_s += float(wall_s or 0.0)27 self.sessions.append({"session_id": session_id, "role": role, "cost_usd": cost, "wall_s": wall_s, "turns": turns})2829 def summary(self) -> dict:30 return {"max_budget_usd": self.max_budget_usd, "wall_seconds": self.wall_seconds, "max_turns_per_session": self.max_turns_per_session,31 "spent_usd": round(self.spent_usd, 6), "spent_wall_s": round(self.spent_wall_s, 1),32 "executor_usd": round(sum(s["cost_usd"] for s in self.sessions if s["role"] == "executor"), 6),33 "reviewer_usd": round(sum(s["cost_usd"] for s in self.sessions if s["role"] == "reviewer"), 6),34 "n_sessions": len(self.sessions), "sessions": self.sessions}35