harness-evolve 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ """
2
+ harness-evolve: improve an agent's harness from its own failure traces.
3
+
4
+ from harness_evolve import AgentProfile, Optimizer, OptimizerConfig, profiles
5
+ profiles.use(AgentProfile.build(name=..., harness_root=..., agent_description=...))
6
+ opt = Optimizer(run_dir, base_harness, OptimizerConfig(evolve_cases=[...]),
7
+ evaluator=MyEvaluator(), proposer_llm=llm, critic_llm=llm, critic_patterns=[])
8
+ state = asyncio.run(opt.run_until_stopped())
9
+ """
10
+ from harness_evolve.acceptance import CaseResult, EvalResult
11
+ from harness_evolve.evaluator import EvalOutcome, Evaluator, ProviderFailure, UnpricedModel
12
+ from harness_evolve.harness import Harness, load_harness
13
+ from harness_evolve.llm import LLMReply, litellm_llm
14
+ from harness_evolve.loop import LocalPublisher, Optimizer, OptimizerConfig
15
+ from harness_evolve.profiles import AgentProfile
16
+
17
+ __all__ = ["AgentProfile", "CaseResult", "EvalOutcome", "EvalResult", "Evaluator", "Harness", "LLMReply",
18
+ "LocalPublisher", "Optimizer", "OptimizerConfig", "ProviderFailure", "UnpricedModel",
19
+ "litellm_llm", "load_harness"]
20
+ __version__ = "0.1.0"
@@ -0,0 +1,237 @@
1
+ """
2
+ Acceptance rules: when does a candidate harness replace the incumbent?
3
+
4
+ The rules follow RRSI (Xia et al., "RRSI: Regularized Recursive
5
+ Self-Improvement of Agent Harnesses", arXiv 2609.24972; reference
6
+ implementation github.com/google-research/rrsi, Apache-2.0,
7
+ rrsi/selection.py), reimplemented here for our setting:
8
+
9
+ floor S' >= S* - delta (RRSI Eq. floor)
10
+ clear gain if dS > delta: dC <= beta0 + beta1 * dS (RRSI Eq. tokenbudget)
11
+ in band otherwise: w_s*dS - w_c*dC > 0 (RRSI Alg. 2 l.5)
12
+ guards domain vetoes (RRSI's eng domain vetoes a rising no-submission
13
+ rate; ours is the escalation rate, the same failure shape)
14
+
15
+ dC is RELATIVE (C'/C - 1), as in RRSI. Defaults mirror RRSI's coding config,
16
+ where w_s = 0: inside the noise band a candidate can only win by being
17
+ cheaper, never by a within-noise score bump. Our noise is far larger than
18
+ theirs (delta ~0.2-0.3 on ~17 cases vs their 0.017 on 178 trials), so in
19
+ practice most gains are "in band" and the rule reduces to "no worse, and
20
+ cheaper".
21
+
22
+ What differs from RRSI, and why:
23
+
24
+ - **Paired, per-case comparison instead of one aggregate score each.** The
25
+ candidate and incumbent run on the same cases, so S and C are compared
26
+ case by case, and each decision records which cases improved or
27
+ regressed. With ~20% of cases flipping between identical runs, knowing
28
+ *which* cases moved matters as much as the mean.
29
+ - **Calibration from trial pairs.** RRSI estimates delta by re-running the
30
+ unchanged base harness. We get the same null distribution from round 0's
31
+ k=2 trials (trial 1 vs trial 2 of the same harness), bootstrapped over
32
+ cases: no extra run.
33
+ - **Guard cases.** The evolve set's always-pass regression guards must keep
34
+ passing: a candidate that fails a guard on every trial (where the
35
+ incumbent passed it) is vetoed outright.
36
+ - **No novelty term, pruning window or annealing.** Those need many rounds;
37
+ we can afford a handful. See docs/blog-drafts/harness-evolution-v2.md §0.4.
38
+ """
39
+ from __future__ import annotations
40
+
41
+ import random
42
+ import statistics
43
+ from dataclasses import asdict, dataclass, field
44
+
45
+
46
+ @dataclass
47
+ class CaseResult:
48
+ """One case's outcome across its trials under one harness."""
49
+ passes: int
50
+ trials: int
51
+ escalations: int = 0 # attempts that never got an accepted submission
52
+ cost_usd: float = 0.0 # summed over this case's trials
53
+ verdicts: list[str] = field(default_factory=list)
54
+
55
+ @property
56
+ def score(self) -> float:
57
+ return self.passes / self.trials if self.trials else 0.0
58
+
59
+
60
+ @dataclass
61
+ class EvalResult:
62
+ """A harness evaluated on a set of cases. S and C are means over cases."""
63
+ per_case: dict[str, CaseResult]
64
+
65
+ @property
66
+ def S(self) -> float:
67
+ return statistics.fmean(c.score for c in self.per_case.values()) if self.per_case else 0.0
68
+
69
+ @property
70
+ def C(self) -> float:
71
+ """Mean cost per trial."""
72
+ trials = sum(c.trials for c in self.per_case.values())
73
+ return sum(c.cost_usd for c in self.per_case.values()) / trials if trials else 0.0
74
+
75
+ @property
76
+ def escalation_rate(self) -> float:
77
+ trials = sum(c.trials for c in self.per_case.values())
78
+ return sum(c.escalations for c in self.per_case.values()) / trials if trials else 0.0
79
+
80
+ def to_json(self) -> dict:
81
+ return {"S": self.S, "C": self.C, "escalation_rate": self.escalation_rate,
82
+ "per_case": {k: asdict(v) for k, v in self.per_case.items()}}
83
+
84
+ @classmethod
85
+ def from_json(cls, data: dict) -> EvalResult:
86
+ return cls({k: CaseResult(**v) for k, v in data["per_case"].items()})
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class AcceptanceConfig:
91
+ delta: float = 0.25 # noise band; replaced by calibrate() after round 0
92
+ beta0: float = 0.10 # RRSI coding: 10% more cost for free on a clear gain
93
+ beta1: float = 44.5 # RRSI coding: each +1pp of S buys +44.5% cost
94
+ w_s: float = 0.0 # RRSI coding: no credit for within-band score change
95
+ w_c: float = 15.0 # RRSI coding
96
+ max_escalation_rise: float = 0.05
97
+ guard_cases: tuple[str, ...] = ()
98
+ # Noise band on the relative cost change, calibrated like delta from round
99
+ # 0's trial pairs. 0 = the original rule (in band, any saving counts).
100
+ cost_delta: float = 0.0
101
+ # Compare cost only on cases both harnesses resolved on every trial, so a
102
+ # cheaper failure never counts as a saving (multi-harness RL article: reward
103
+ # efficiency only on correct answers). Off = cost over all shared cases.
104
+ cost_on_shared_successes: bool = False
105
+ min_shared_successes: int = 5
106
+ # A rejected in-band candidate with dS >= this fraction of delta is kept as a
107
+ # near miss the proposer can build on (DGM/HGM archives). 0 = off.
108
+ near_miss_fraction: float = 0.0
109
+
110
+
111
+ @dataclass
112
+ class Decision:
113
+ accept: bool
114
+ reason: str
115
+ delta_S: float
116
+ delta_C: float # relative
117
+ improved: list[str] = field(default_factory=list)
118
+ regressed: list[str] = field(default_factory=list)
119
+ vetoes: list[str] = field(default_factory=list)
120
+ near_miss: bool = False
121
+
122
+ def to_json(self) -> dict:
123
+ return asdict(self)
124
+
125
+
126
+ def paired_deltas(incumbent: EvalResult, candidate: EvalResult,
127
+ cfg: AcceptanceConfig | None = None) -> tuple[float, float, list, list]:
128
+ """dS over the cases both evaluated, relative dC, and which cases moved.
129
+ With cfg.cost_on_shared_successes, dC is over the cases both resolved on
130
+ every trial (0 if fewer than cfg.min_shared_successes)."""
131
+ shared = sorted(set(incumbent.per_case) & set(candidate.per_case))
132
+ if not shared:
133
+ raise ValueError("candidate and incumbent share no evaluated cases")
134
+ inc = EvalResult({c: incumbent.per_case[c] for c in shared})
135
+ cand = EvalResult({c: candidate.per_case[c] for c in shared})
136
+ dS = cand.S - inc.S
137
+ dC = (cand.C / inc.C - 1.0) if inc.C > 0 else 0.0
138
+ if cfg is not None and cfg.cost_on_shared_successes:
139
+ both = [c for c in shared if inc.per_case[c].score == 1.0 and cand.per_case[c].score == 1.0]
140
+ if len(both) < cfg.min_shared_successes:
141
+ dC = 0.0
142
+ else:
143
+ inc_s = EvalResult({c: inc.per_case[c] for c in both})
144
+ cand_s = EvalResult({c: cand.per_case[c] for c in both})
145
+ dC = (cand_s.C / inc_s.C - 1.0) if inc_s.C > 0 else 0.0
146
+ improved = [c for c in shared if cand.per_case[c].score > inc.per_case[c].score]
147
+ regressed = [c for c in shared if cand.per_case[c].score < inc.per_case[c].score]
148
+ return dS, dC, improved, regressed
149
+
150
+
151
+ def decide(incumbent: EvalResult, candidate: EvalResult, S_star: float,
152
+ cfg: AcceptanceConfig) -> Decision:
153
+ dS, dC, improved, regressed = paired_deltas(incumbent, candidate, cfg)
154
+ d = Decision(False, "", dS, dC, improved, regressed)
155
+
156
+ # A guard vetoes only when it fails EVERY trial of the candidate and the
157
+ # incumbent passed it at least once. The first rule (any failed trial)
158
+ # was noise at 2 trials a case: a guard that passes 80-95% of the time
159
+ # (astropy-14309 went PASS/FAIL on the unchanged harness) would have
160
+ # vetoed about a third of candidates by chance across 4 guards. Breaking
161
+ # the easy path shows up as all-fail, not one-in-two.
162
+ for g in cfg.guard_cases:
163
+ cr = candidate.per_case.get(g)
164
+ inc = incumbent.per_case.get(g)
165
+ if cr is not None and cr.trials and cr.passes == 0 and (inc is None or inc.passes > 0):
166
+ d.vetoes.append(f"guard case {g} failed {cr.trials}/{cr.trials}")
167
+ rise = candidate.escalation_rate - incumbent.escalation_rate
168
+ if rise > cfg.max_escalation_rise:
169
+ d.vetoes.append(f"escalation rate rose {rise:+.3f} (limit {cfg.max_escalation_rise})")
170
+ if d.vetoes:
171
+ d.reason = "vetoed: " + "; ".join(d.vetoes)
172
+ return d
173
+
174
+ floor = S_star - cfg.delta
175
+ if candidate.S < floor:
176
+ d.reason = f"below noise floor: S' {candidate.S:.3f} < S* {S_star:.3f} - delta {cfg.delta:.3f}"
177
+ return d
178
+
179
+ if dS > cfg.delta:
180
+ budget = cfg.beta0 + cfg.beta1 * dS
181
+ d.accept = dC <= budget
182
+ d.reason = (f"clear gain dS {dS:+.3f} > delta {cfg.delta:.3f}; relative cost "
183
+ f"{dC:+.3f} {'<=' if d.accept else '>'} budget {budget:.3f}")
184
+ return d
185
+
186
+ # In band a candidate must be cheaper, and with a cost band, cheaper by
187
+ # more than cost noise: the same "submit sooner" edit measured dC anywhere
188
+ # from -1.8% to +4.8% across evaluations, so comparing with 0 let noise decide.
189
+ shaped = cfg.w_s * dS - cfg.w_c * (dC + cfg.cost_delta)
190
+ d.accept = shaped > 0
191
+ d.near_miss = (not d.accept and cfg.near_miss_fraction > 0 and dS > 0
192
+ and dS >= cfg.near_miss_fraction * cfg.delta)
193
+ band = f", cost band {cfg.cost_delta:.3f}" if cfg.cost_delta else ""
194
+ d.reason = (f"within noise band (dS {dS:+.3f}, delta {cfg.delta:.3f}{band}); "
195
+ f"{cfg.w_s}*dS - {cfg.w_c}*(dC{' + band' if cfg.cost_delta else ''}) = {shaped:+.3f} "
196
+ f"{'>' if d.accept else '<='} 0"
197
+ + ("" if d.accept else " (in band, a candidate must be cheaper"
198
+ + (" by more than cost noise)" if cfg.cost_delta else ")")))
199
+ return d
200
+
201
+
202
+ def calibrate_cost_delta(trial_costs: dict[str, tuple[float, float]], z: float = 2.0,
203
+ trials_per_eval: int = 1, reps: int = 2000, seed: int = 7) -> float:
204
+ """Noise band on the RELATIVE cost change, from two trials of the same
205
+ harness per case: the null dC is (sum trial1 - sum trial2) / mean total,
206
+ bootstrapped over cases, times z, shrunk by sqrt(trials_per_eval)."""
207
+ pairs = list(trial_costs.values())
208
+ if len(pairs) < 2:
209
+ raise ValueError("need at least 2 cases with two trials each")
210
+ rng = random.Random(seed)
211
+ boot = []
212
+ for _ in range(reps):
213
+ sample = [rng.choice(pairs) for _ in pairs]
214
+ a, b = sum(x for x, _ in sample), sum(y for _, y in sample)
215
+ boot.append((a - b) / ((a + b) / 2) if a + b else 0.0)
216
+ return z * statistics.pstdev(boot) / trials_per_eval ** 0.5
217
+
218
+
219
+ def calibrate_delta(trial_pairs: dict[str, tuple[bool, bool]], z: float = 2.0,
220
+ trials_per_eval: int = 1, reps: int = 2000, seed: int = 7) -> float:
221
+ """Noise band from two trials of the SAME harness on each case.
222
+
223
+ The null difference of two independent evaluations is observed directly:
224
+ dS_null = mean over cases of (trial1 - trial2). Its standard deviation is
225
+ bootstrapped over cases, and delta = z * sd, so an unchanged harness
226
+ clears the floor about 97.5% of the time at z = 2 (as in rrsi/calibrate.py).
227
+
228
+ The pairs measure single-trial evaluations. If each later evaluation
229
+ averages `trials_per_eval` trials per case, its noise shrinks by
230
+ sqrt(trials_per_eval), and so does delta.
231
+ """
232
+ diffs = [float(a) - float(b) for a, b in trial_pairs.values()]
233
+ if len(diffs) < 2:
234
+ raise ValueError("need at least 2 cases with two trials each to calibrate")
235
+ rng = random.Random(seed)
236
+ boot = [statistics.fmean(rng.choice(diffs) for _ in diffs) for _ in range(reps)]
237
+ return z * statistics.pstdev(boot) / trials_per_eval ** 0.5
@@ -0,0 +1,137 @@
1
+ """
2
+ Benchmark the optimizer itself, so "this change makes harness-evolve better" is a measured claim.
3
+
4
+ A benchmark is a list of scenarios. Each builds an Optimizer in its own run directory and may state
5
+ what must happen:
6
+
7
+ - Known-answer scenarios: a scripted proposer (and critic) puts one known edit in front of the
8
+ acceptance rules, so there is a right answer. A real fix to a planted flaw must be accepted (the
9
+ rules can see a real gain); a harmful edit must be rejected; a neutral rewording must not be
10
+ accepted (false accepts).
11
+ - Open scenarios: the real proposer, reported but not pass/fail: does it find a planted flaw, and
12
+ what does a standard run gain on held-out cases, per dollar?
13
+
14
+ harness-evolve bench examples/math_agent/bench.py --out runs/bench-1
15
+
16
+ Run directories persist, so a stopped benchmark resumes like any run. Results go to
17
+ <out>/bench.json and <out>/bench.md.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from collections.abc import Callable
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ from harness_evolve.llm import LLM, LLMReply
27
+
28
+ ACCEPTED, NOT_ACCEPTED = "accepted", "not_accepted"
29
+
30
+
31
+ @dataclass
32
+ class Scenario:
33
+ name: str
34
+ description: str
35
+ build: Callable[[Path], object] # run_dir -> Optimizer
36
+ expect: str | None = None # ACCEPTED, NOT_ACCEPTED, or None (report only)
37
+
38
+
39
+ def scripted_llm(edit: dict, critic_accepts: bool = True) -> LLM:
40
+ """A stand-in for the proposer and critic: the proposer always proposes `edit`
41
+ ({"component", "hypothesis", "edits": [...]}); the critic always accepts (or rejects).
42
+ With the critic scripted too, the scenario tests the acceptance rules alone."""
43
+ async def call(system: str, prompt: str) -> LLMReply:
44
+ if "strict reviewer" in system:
45
+ verdict = "accept" if critic_accepts else "reject"
46
+ return LLMReply(json.dumps({"verdict": verdict, "reasons": ["scripted"]}), 0.0)
47
+ return LLMReply(json.dumps(edit), 0.0)
48
+ return call
49
+
50
+
51
+ def outcome(run_dir: Path) -> dict:
52
+ """What a finished scenario run did: rounds, acceptances, held-out numbers, spend."""
53
+ state = json.loads((run_dir / "state.json").read_text())
54
+ rounds = []
55
+ hist = run_dir / "history.jsonl"
56
+ if hist.exists():
57
+ for line in hist.read_text().splitlines():
58
+ e = json.loads(line)
59
+ if not str(e.get("outcome", "")).startswith("pilot_"):
60
+ rounds.append({k: e.get(k) for k in ("round", "component", "outcome", "delta_S", "delta_C",
61
+ "reason", "hypothesis")})
62
+ out = {"phase": state.get("phase"), "stop_reason": state.get("stop_reason"),
63
+ "accepted": state.get("accepted") or [], "spent_usd": round(state.get("spent_usd") or 0.0, 3),
64
+ "S_star": state.get("S_star"), "delta": state.get("delta"), "rounds": rounds}
65
+ rep = run_dir / "report" / "report.json"
66
+ if rep.exists():
67
+ r = json.loads(rep.read_text())
68
+ out["heldout"] = {
69
+ "original_pass@1": (r.get("original") or {}).get("pass@1"),
70
+ "evolved_pass@1": (r.get("evolved") or {}).get("pass@1"),
71
+ "vs_original_single_run": (r.get("comparisons") or {}).get("vs_original_single_run"),
72
+ "vs_rerun_baseline_best_of_2": (r.get("comparisons") or {}).get("vs_rerun_baseline_best_of_2"),
73
+ }
74
+ return out
75
+
76
+
77
+ def judge(expect: str | None, result: dict) -> bool | None:
78
+ if result.get("phase") != "done":
79
+ return False
80
+ if expect == ACCEPTED:
81
+ return bool(result["accepted"])
82
+ if expect == NOT_ACCEPTED:
83
+ return not result["accepted"]
84
+ return None
85
+
86
+
87
+ async def run(scenarios: list[Scenario], out_dir: Path, only: list[str] | None = None) -> list[dict]:
88
+ out_dir.mkdir(parents=True, exist_ok=True)
89
+ results = []
90
+ for sc in scenarios:
91
+ if only and sc.name not in only:
92
+ continue
93
+ run_dir = out_dir / sc.name
94
+ opt = sc.build(run_dir)
95
+ try:
96
+ await opt.run_until_stopped()
97
+ except Exception as exc: # one broken scenario shouldn't hide the rest
98
+ results.append({"name": sc.name, "description": sc.description, "expect": sc.expect,
99
+ "passed": False, "error": f"{type(exc).__name__}: {exc}"})
100
+ continue
101
+ res = outcome(run_dir)
102
+ results.append({"name": sc.name, "description": sc.description, "expect": sc.expect,
103
+ "passed": judge(sc.expect, res), **res})
104
+ if only and (out_dir / "bench.json").exists():
105
+ # A partial re-run updates its scenarios in the existing results instead of replacing them.
106
+ previous = json.loads((out_dir / "bench.json").read_text())
107
+ fresh = {r["name"]: r for r in results}
108
+ names = [r["name"] for r in previous] + [n for n in fresh if n not in {r["name"] for r in previous}]
109
+ old = {r["name"]: r for r in previous}
110
+ results = [fresh.get(n, old.get(n)) for n in names]
111
+ (out_dir / "bench.json").write_text(json.dumps(results, indent=1))
112
+ (out_dir / "bench.md").write_text(render(results))
113
+ return results
114
+
115
+
116
+ def render(results: list[dict]) -> str:
117
+ def pct(x):
118
+ return "-" if x is None else f"{x:.3f}"
119
+ lines = ["# harness-evolve benchmark", "",
120
+ "| Scenario | Expect | Result | Accepted | First round (dS, dC) "
121
+ "| Held-out pass@1 (orig → evolved) | Spent |",
122
+ "|---|---|---|---|---|---|---|"]
123
+ for r in results:
124
+ verdict = {True: "PASS", False: "FAIL", None: "report"}[r.get("passed")]
125
+ if r.get("error"):
126
+ lines.append(f"| {r['name']} | {r.get('expect') or '-'} | ERROR | - | {r['error'][:80]} | - | - |")
127
+ continue
128
+ first = next(iter(r.get("rounds") or []), None)
129
+ fr = (f"{first['outcome']} ({first['delta_S']:+.3f}, {first['delta_C']:+.0%})"
130
+ if first and first.get("delta_S") is not None and first.get("delta_C") is not None
131
+ else (first or {}).get("outcome", "-"))
132
+ h = r.get("heldout") or {}
133
+ ho = (f"{pct(h.get('original_pass@1'))} → {pct(h.get('evolved_pass@1'))}" if h else "-")
134
+ lines.append(f"| {r['name']} | {r.get('expect') or '-'} | {verdict} | {r.get('accepted') or 'none'} | "
135
+ f"{fr} | {ho} | ${r.get('spent_usd', 0):.2f} |")
136
+ lines += ["", *[f"- **{r['name']}**: {r['description']}" for r in results]]
137
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,57 @@
1
+ """
2
+ Hard spending cap, enforced in code, never left to the model.
3
+
4
+ Before an evaluation starts, the optimizer asks for a reservation sized by
5
+ the expected cost (per-case cost measured so far x cases x trials, with a
6
+ safety margin). If the reservation would take total spend past the cap, the
7
+ evaluation doesn't start and the run stops with a clear reason, rather than
8
+ discovering the overrun (or an empty credit balance, twice this week) halfway
9
+ through. Actual spend comes from the cost meter and is written into the run
10
+ state, so a resumed run knows what it has already spent.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+
16
+
17
+ class BudgetExceeded(RuntimeError):
18
+ pass
19
+
20
+
21
+ @dataclass
22
+ class Budget:
23
+ cap_usd: float
24
+ spent_usd: float = 0.0
25
+ margin: float = 1.25 # reserve 25% above the estimate
26
+ # Reservations held by evaluations still running. With several running in
27
+ # parallel, each checking only `spent` could pass the cap together and
28
+ # overshoot it; counting in-flight reservations prevents that.
29
+ reserved_usd: float = 0.0
30
+
31
+ @property
32
+ def remaining_usd(self) -> float:
33
+ return max(0.0, self.cap_usd - self.spent_usd - self.reserved_usd)
34
+
35
+ def reserve(self, estimate_usd: float, what: str) -> float:
36
+ """Hold `estimate x margin` until release(); returns the amount held."""
37
+ need = estimate_usd * self.margin
38
+ if self.spent_usd + self.reserved_usd + need > self.cap_usd:
39
+ raise BudgetExceeded(
40
+ f"{what}: needs ~${need:.2f} (estimate ${estimate_usd:.2f} x {self.margin} margin), "
41
+ f"but only ${self.remaining_usd:.2f} of the ${self.cap_usd:.2f} cap remains"
42
+ + (f" (${self.reserved_usd:.2f} held by evaluations in flight)" if self.reserved_usd else "")
43
+ )
44
+ self.reserved_usd += need
45
+ return need
46
+
47
+ def release(self, held_usd: float, actual_usd: float) -> None:
48
+ """Swap a reservation for what the evaluation actually cost."""
49
+ self.reserved_usd = max(0.0, self.reserved_usd - held_usd)
50
+ self.spent_usd += actual_usd
51
+
52
+ def record(self, actual_usd: float) -> None:
53
+ self.spent_usd += actual_usd
54
+
55
+
56
+ def estimate_eval_cost(n_cases: int, trials: int, per_case_usd: float) -> float:
57
+ return n_cases * trials * per_case_usd
@@ -0,0 +1,112 @@
1
+ """
2
+ Candidate harnesses: copies of a harness directory with one edit applied.
3
+
4
+ The edit surface is bounded to the files that already exist in the harness
5
+ directory: an edit may rewrite them, never add or delete files. Before anything is spent
6
+ evaluating a candidate, it must pass
7
+ structural validation, so a candidate that can't even load (bad JSON, a
8
+ template placeholder the code never fills, a removed tool description, a
9
+ turn budget of 500) is rejected for free. RRSI does the same with its
10
+ "deterministic compile/constructor/smoke checks" after the critic.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import difflib
15
+ import hashlib
16
+ import json
17
+ import shutil
18
+ import string
19
+ from pathlib import Path
20
+
21
+ IGNORED = {"README.md"} # documentation, never loaded, never edited
22
+
23
+ class InvalidCandidate(ValueError):
24
+ pass
25
+
26
+
27
+ def harness_files(directory: Path) -> dict[str, str]:
28
+ return {p.name: p.read_text() for p in sorted(Path(directory).iterdir())
29
+ if p.is_file() and p.name not in IGNORED}
30
+
31
+
32
+ def content_hash(directory: Path) -> str:
33
+ h = hashlib.sha256()
34
+ for name, text in harness_files(directory).items():
35
+ h.update(name.encode() + b"\0" + text.encode() + b"\0")
36
+ return h.hexdigest()[:16]
37
+
38
+
39
+ def _placeholders(template: str) -> set[str]:
40
+ return {f for _, f, _, _ in string.Formatter().parse(template) if f}
41
+
42
+
43
+ def apply_edit(base: Path, dest: Path, files: dict[str, str]) -> None:
44
+ """dest = copy of base, with `files` (name -> full new content) overwritten."""
45
+ base_files = harness_files(base)
46
+ unknown = sorted(set(files) - set(base_files))
47
+ if unknown:
48
+ raise InvalidCandidate(f"edit touches files outside the harness surface: {unknown}")
49
+ if not any(files[n] != base_files[n] for n in files):
50
+ raise InvalidCandidate("edit changes nothing")
51
+ if dest.exists():
52
+ shutil.rmtree(dest)
53
+ shutil.copytree(base, dest)
54
+ for name, text in files.items():
55
+ (dest / name).write_text(text)
56
+
57
+
58
+ def validate(base: Path, candidate: Path) -> None:
59
+ """Raise InvalidCandidate unless `candidate` is loadable and in bounds."""
60
+ b, c = harness_files(base), harness_files(candidate)
61
+ if set(b) != set(c):
62
+ raise InvalidCandidate(f"file set changed: {sorted(set(b) ^ set(c))}")
63
+ try:
64
+ b_set = json.loads(b["settings.json"]) if "settings.json" in b else {}
65
+ c_set = json.loads(c["settings.json"]) if "settings.json" in c else {}
66
+ b_tools = json.loads(b["tool_descriptions.json"]) if "tool_descriptions.json" in b else {}
67
+ c_tools = json.loads(c["tool_descriptions.json"]) if "tool_descriptions.json" in c else {}
68
+ except json.JSONDecodeError as exc:
69
+ raise InvalidCandidate(f"invalid JSON: {exc}") from exc
70
+ if set(c_set) != set(b_set):
71
+ raise InvalidCandidate(f"settings keys changed: {sorted(set(b_set) ^ set(c_set))}")
72
+ for key, value in c_set.items():
73
+ if key.startswith("_"):
74
+ continue
75
+ if type(value) is not type(b_set[key]):
76
+ raise InvalidCandidate(f"setting {key} changed type")
77
+ from harness_evolve import profiles
78
+ lo_hi = profiles.active().setting_bounds.get(key)
79
+ if isinstance(lo_hi, frozenset):
80
+ if value not in lo_hi:
81
+ raise InvalidCandidate(f"setting {key}={value!r} not one of {sorted(lo_hi)}")
82
+ elif lo_hi and not lo_hi[0] <= value <= lo_hi[1]:
83
+ raise InvalidCandidate(f"setting {key}={value} outside {lo_hi}")
84
+ if set(c_tools) != set(b_tools):
85
+ raise InvalidCandidate(f"tool set changed: {sorted(set(b_tools) ^ set(c_tools))}")
86
+ if any(not str(v).strip() for v in c_tools.values()):
87
+ raise InvalidCandidate("a tool description is empty")
88
+ for name in b:
89
+ if not name.endswith(".prompt"):
90
+ continue
91
+ try:
92
+ extra = _placeholders(c[name]) - _placeholders(b[name])
93
+ except ValueError as exc: # unbalanced braces
94
+ raise InvalidCandidate(f"{name}: malformed template: {exc}") from exc
95
+ if extra:
96
+ raise InvalidCandidate(f"{name}: uses placeholders the code never fills: {sorted(extra)}")
97
+
98
+
99
+ def diff(base: Path, candidate: Path) -> str:
100
+ b, c = harness_files(base), harness_files(candidate)
101
+ out = []
102
+ for name in sorted(b):
103
+ if b[name] != c.get(name):
104
+ out.extend(difflib.unified_diff(b[name].splitlines(keepends=True),
105
+ c.get(name, "").splitlines(keepends=True),
106
+ f"a/{name}", f"b/{name}"))
107
+ return "".join(out)
108
+
109
+
110
+ def changed_files(base: Path, candidate: Path) -> list[str]:
111
+ b, c = harness_files(base), harness_files(candidate)
112
+ return sorted(n for n in b if b[n] != c.get(n))