harness-evolve 0.1.0__tar.gz
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.
- harness_evolve-0.1.0/.github/workflows/release.yml +40 -0
- harness_evolve-0.1.0/.github/workflows/tests.yml +30 -0
- harness_evolve-0.1.0/.gitignore +8 -0
- harness_evolve-0.1.0/LICENSE +21 -0
- harness_evolve-0.1.0/PKG-INFO +207 -0
- harness_evolve-0.1.0/README.md +190 -0
- harness_evolve-0.1.0/docs/make_chart.py +46 -0
- harness_evolve-0.1.0/docs/methods.md +71 -0
- harness_evolve-0.1.0/docs/results.png +0 -0
- harness_evolve-0.1.0/examples/math_agent/agent.py +106 -0
- harness_evolve-0.1.0/examples/math_agent/bench.py +68 -0
- harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/nudge.prompt +1 -0
- harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/settings.json +8 -0
- harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/system.prompt +1 -0
- harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/task.prompt +5 -0
- harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/tool_descriptions.json +4 -0
- harness_evolve-0.1.0/examples/math_agent/config.py +63 -0
- harness_evolve-0.1.0/examples/math_agent/evaluator.py +67 -0
- harness_evolve-0.1.0/examples/math_agent/fetch_data.py +41 -0
- harness_evolve-0.1.0/examples/math_agent/harness/nudge.prompt +1 -0
- harness_evolve-0.1.0/examples/math_agent/harness/settings.json +8 -0
- harness_evolve-0.1.0/examples/math_agent/harness/system.prompt +1 -0
- harness_evolve-0.1.0/examples/math_agent/harness/task.prompt +5 -0
- harness_evolve-0.1.0/examples/math_agent/harness/tool_descriptions.json +4 -0
- harness_evolve-0.1.0/pyproject.toml +35 -0
- harness_evolve-0.1.0/src/harness_evolve/__init__.py +20 -0
- harness_evolve-0.1.0/src/harness_evolve/acceptance.py +237 -0
- harness_evolve-0.1.0/src/harness_evolve/bench.py +137 -0
- harness_evolve-0.1.0/src/harness_evolve/budget.py +57 -0
- harness_evolve-0.1.0/src/harness_evolve/candidates.py +112 -0
- harness_evolve-0.1.0/src/harness_evolve/cli.py +120 -0
- harness_evolve-0.1.0/src/harness_evolve/critic.py +92 -0
- harness_evolve-0.1.0/src/harness_evolve/evaluator.py +48 -0
- harness_evolve-0.1.0/src/harness_evolve/evidence.py +93 -0
- harness_evolve-0.1.0/src/harness_evolve/grader.py +71 -0
- harness_evolve-0.1.0/src/harness_evolve/harness.py +62 -0
- harness_evolve-0.1.0/src/harness_evolve/health.py +121 -0
- harness_evolve-0.1.0/src/harness_evolve/history.py +91 -0
- harness_evolve-0.1.0/src/harness_evolve/llm.py +56 -0
- harness_evolve-0.1.0/src/harness_evolve/loop.py +857 -0
- harness_evolve-0.1.0/src/harness_evolve/pass_k.py +57 -0
- harness_evolve-0.1.0/src/harness_evolve/profiles.py +177 -0
- harness_evolve-0.1.0/src/harness_evolve/proposer.py +192 -0
- harness_evolve-0.1.0/src/harness_evolve/report.py +99 -0
- harness_evolve-0.1.0/src/harness_evolve/state.py +116 -0
- harness_evolve-0.1.0/tests/__init__.py +0 -0
- harness_evolve-0.1.0/tests/conftest.py +56 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/README.md +32 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/log_context_fetched.prompt +13 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/log_context_missing.prompt +2 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/prior_knowledge.prompt +4 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/retrieval_unavailable.prompt +2 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/settings.json +14 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/stack_trace.prompt +4 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/task_prompt.prompt +270 -0
- harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/tool_descriptions.json +12 -0
- harness_evolve-0.1.0/tests/test_acceptance_phase_d.py +34 -0
- harness_evolve-0.1.0/tests/test_bench.py +89 -0
- harness_evolve-0.1.0/tests/test_example.py +85 -0
- harness_evolve-0.1.0/tests/test_optimizer.py +797 -0
- harness_evolve-0.1.0/tests/test_optimizer_transfer.py +91 -0
- harness_evolve-0.1.0/tests/test_package.py +90 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag (v0.1.0, ...) is pushed. Uses PyPI trusted publishing:
|
|
4
|
+
# no token is stored anywhere; PyPI trusts this workflow in this repo via the "pypi" environment.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
- name: Tag must match the package version
|
|
19
|
+
run: |
|
|
20
|
+
python -c "import tomllib, os; v = tomllib.load(open('pyproject.toml', 'rb'))['project']['version']; t = os.environ['GITHUB_REF_NAME'].removeprefix('v'); assert v == t, f'tag {t} != pyproject version {v}'"
|
|
21
|
+
- run: pip install build twine
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- run: twine check dist/*
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- run: pytest -q
|
|
21
|
+
|
|
22
|
+
lint:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.13"
|
|
29
|
+
- run: pip install ruff
|
|
30
|
+
- run: ruff check .
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siddharth Sukumar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: harness-evolve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Evolve an agent's harness (prompts, tool descriptions, settings) from its own failure traces, accepting only changes that beat measured run-to-run noise.
|
|
5
|
+
Author: Siddharth Sukumar
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agents,evals,harness-engineering,llm,prompt-optimization
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: litellm>=1.40; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
14
|
+
Provides-Extra: litellm
|
|
15
|
+
Requires-Dist: litellm>=1.40; extra == 'litellm'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# harness-evolve
|
|
19
|
+
|
|
20
|
+
[](https://github.com/sidreddy88/harness-evolve/actions/workflows/tests.yml)
|
|
21
|
+
|
|
22
|
+
Improve an AI agent's harness (its prompts, tool descriptions and settings) from the agent's own failure
|
|
23
|
+
traces, and keep a change only when it beats measured run-to-run noise.
|
|
24
|
+
|
|
25
|
+
Agents are noisy: run the same agent on the same task twice and the result can flip. Tune a prompt,
|
|
26
|
+
rerun, see +3 points, and you may have measured nothing. harness-evolve measures that noise first,
|
|
27
|
+
then accepts an edit only if it clears it, checks it doesn't break the cases that already work, and
|
|
28
|
+
reports the result on cases it never saw, against the cheapest alternative: just running the original
|
|
29
|
+
agent twice.
|
|
30
|
+
|
|
31
|
+
## Results
|
|
32
|
+
|
|
33
|
+
Used on a production code-diagnosis agent ([agent-platform](https://github.com/sidreddy88/agent-platform)), on SWE-bench Verified with
|
|
34
|
+
DeepSeek-V4.1-Flash:
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
| Held-out: 66 cases from repositories the optimizer never saw | Original harness | Evolved harness |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| Finds the file the fix must change (single run) | 78.8% | **89.9%** (+11.1, 95% CI +6.1 to +16.2) |
|
|
41
|
+
| Gives up with no answer | 19.7% | 5.6% |
|
|
42
|
+
| Solves it on all 3 trials (reliability) | 60.6% | 78.8% |
|
|
43
|
+
|
|
44
|
+
The evolved harness matched running the original twice (best-of-2) at about 60% of the cost.
|
|
45
|
+
Two edits were accepted in a 7.4-hour run, and the evolved harness went to production.
|
|
46
|
+
|
|
47
|
+
It also says no. An earlier run of the same optimizer on Claude Sonnet 5 accepted nothing, and so
|
|
48
|
+
did a three-round run on the same project's fix agent: every candidate edit either stayed inside the
|
|
49
|
+
noise band or broke a guard case. Halfway through that run, one edit led 7 to 4, and a single
|
|
50
|
+
measurement would have reported it as a gain.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
The example is a small math agent (a calculator tool and a submit tool) on GSM-Hard, GSM8K questions
|
|
55
|
+
with large, awkward numbers that leave room to improve.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/sidreddy88/harness-evolve && cd harness-evolve
|
|
59
|
+
pip install -e ".[litellm]"
|
|
60
|
+
python examples/math_agent/fetch_data.py # downloads 80 problems
|
|
61
|
+
export TOGETHER_API_KEY=... # or any litellm provider; set MODEL to match
|
|
62
|
+
harness-evolve run examples/math_agent/config.py --run-dir runs/math-1 --budget 2
|
|
63
|
+
harness-evolve status runs/math-1
|
|
64
|
+
harness-evolve diff runs/math-1 # accepted edits, as diffs to review
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
What to expect (our run, DeepSeek-V4.1-Flash, $1.06): the starting harness solved 76% of the 50 evolve
|
|
68
|
+
problems, with a measured noise band of ±5.6 points. The proposer read the trajectories and aimed three
|
|
69
|
+
edits at real failures (runs that never submit an answer, then wrong arithmetic); each gained +2 to +3
|
|
70
|
+
points, inside the noise, so none was accepted. With 50 cases only bigger gains are detectable; more
|
|
71
|
+
cases narrow the band.
|
|
72
|
+
|
|
73
|
+
`--budget` is a hard cap in dollars. A run that stops (budget, Ctrl-C, a provider outage) resumes with
|
|
74
|
+
the same command, and nothing already paid for is paid again.
|
|
75
|
+
|
|
76
|
+
## How a run works
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
round 0 run the starting harness on the evolve cases, 2 trials each
|
|
80
|
+
-> the noise band (how much the pass rate moves between identical runs)
|
|
81
|
+
round r propose one edit, aimed at failures in the trajectories (GEPA-style)
|
|
82
|
+
-> structural checks (free)
|
|
83
|
+
-> a critic screens for leakage, gaming and weakened safeguards (cheap)
|
|
84
|
+
-> evaluate the candidate (the only expensive step)
|
|
85
|
+
-> accept only if it beats the noise band, or is cheaper beyond cost noise
|
|
86
|
+
at the same pass rate, and no guard case regresses
|
|
87
|
+
final original vs evolved on held-out cases, k trials each, including the
|
|
88
|
+
original's best-of-2 (the matched-budget rerun baseline)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Accepted edits are written as diffs under `runs/<id>/proposals/` for you to review; your code is never
|
|
92
|
+
changed automatically.
|
|
93
|
+
|
|
94
|
+
Built for long unattended runs: every phase is checkpointed; every case's result is cached by the
|
|
95
|
+
candidate's content hash; a hard budget is checked before each case; provider failures (billing, auth,
|
|
96
|
+
rate limits) stop the run instead of being scored as failures; tripwires pause it when measurements
|
|
97
|
+
look broken; a watchdog exits cleanly if it stops making progress.
|
|
98
|
+
|
|
99
|
+
## Using it on your agent
|
|
100
|
+
|
|
101
|
+
You need five things.
|
|
102
|
+
|
|
103
|
+
**1. A harness directory your agent reads at run time**
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
my_harness/
|
|
107
|
+
system.prompt # templates, rendered with str.format
|
|
108
|
+
task.prompt
|
|
109
|
+
tool_descriptions.json # {"search": "Search the docs ..."}
|
|
110
|
+
settings.json # {"max_turns": 12, "_doc": {"max_turns": "turn budget"}}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Load it with `harness_evolve.load_harness(path)`. The optimizer edits copies of these files, never
|
|
114
|
+
adds or deletes files, and checks every edit still loads.
|
|
115
|
+
|
|
116
|
+
**2. An evaluator**
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
class MyEvaluator:
|
|
120
|
+
async def evaluate(self, harness_dir, case_ids, trials) -> EvalOutcome:
|
|
121
|
+
... # run your agent with harness_dir on each case, `trials` times
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Return pass/fail and cost per trial, and trajectories if you can: the proposer reads them to find
|
|
125
|
+
out why runs fail. Raise `ProviderFailure` for billing, auth or outage errors. Never let the right
|
|
126
|
+
answer reach the proposer: put it after `evidence.HIDDEN_MARKER` in a trajectory's `detail`.
|
|
127
|
+
See `examples/math_agent/evaluator.py`.
|
|
128
|
+
|
|
129
|
+
**3. A profile**
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from harness_evolve import AgentProfile, profiles
|
|
133
|
+
profiles.use(AgentProfile.build(
|
|
134
|
+
name="support-bot", harness_root="my_harness/",
|
|
135
|
+
agent_description="Answers support tickets with search and read tools; finishes by calling reply.",
|
|
136
|
+
safety_mechanisms=["citing a source document for every claim"],
|
|
137
|
+
setting_bounds={"max_turns": (3, 20)}))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The description and safety mechanisms become the proposer's and critic's instructions; setting
|
|
141
|
+
bounds keep an edit from "saving cost" by starving the agent.
|
|
142
|
+
|
|
143
|
+
**4. Cases**: an evolve set (what it improves on, ideally 50+), optional guard cases (must keep
|
|
144
|
+
passing) and held-out cases (never seen during evolution, for the final report).
|
|
145
|
+
|
|
146
|
+
**5. A model for the proposer and critic, and a budget**: `litellm_llm("model-id", prices=(in, out))`
|
|
147
|
+
or any async `(system, prompt) -> LLMReply(text, cost_usd)`.
|
|
148
|
+
|
|
149
|
+
Then:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
opt = Optimizer("runs/r1", "my_harness/", OptimizerConfig(evolve_cases=..., budget_usd=50),
|
|
153
|
+
evaluator=MyEvaluator(), proposer_llm=llm, critic_llm=llm, critic_patterns=[])
|
|
154
|
+
asyncio.run(opt.run_until_stopped())
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or put a `build(run_dir, budget_usd, rounds)` function in a config file and use the CLI, as the
|
|
158
|
+
example does.
|
|
159
|
+
|
|
160
|
+
## Benchmarking the optimizer
|
|
161
|
+
|
|
162
|
+
`harness-evolve bench` measures the optimizer itself, so a change to its method is judged by
|
|
163
|
+
evidence, not opinion. Known-answer scenarios have a right outcome: a scripted proposer puts one known
|
|
164
|
+
edit in front of the acceptance rules. A real fix to a planted flaw must be accepted, a harmful edit
|
|
165
|
+
must be rejected, and a neutral rewording must not be accepted (a false accept). Open scenarios run the
|
|
166
|
+
real proposer and report what it finds and what it gains on held-out cases, per dollar.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
python examples/math_agent/fetch_data.py --n 260 --out examples/math_agent/data/problems-bench.jsonl
|
|
170
|
+
harness-evolve bench examples/math_agent/bench.py --out runs/bench-1
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Results go to `runs/bench-1/bench.md`. Our first run (DeepSeek-V4.1-Flash, $9.37 in total):
|
|
174
|
+
|
|
175
|
+
| Scenario | Expected | Result |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| accept-good: planted flaw (`max_turns` 1) + scripted fix | accepted | PASS: +49 points |
|
|
178
|
+
| reject-bad: scripted "round answers to the nearest hundred" | not accepted | PASS: −21.5 points, rejected |
|
|
179
|
+
| reject-noise: scripted neutral rewording | not accepted | PASS: rejected |
|
|
180
|
+
| find-flaw: planted flaw, real proposer | (reported) | found the flaw from trajectories: held-out 28.3% → 79.0% |
|
|
181
|
+
| standard: normal harness, 200 problems, real proposer | (reported) | one prompt edit accepted: held-out accuracy unchanged (83.3% → 83.9%), ~11% cheaper |
|
|
182
|
+
|
|
183
|
+
The first version of reject-bad ("skip the calculator and estimate") was accepted: it didn't hurt this
|
|
184
|
+
model at all and was 13% cheaper, so the optimizer was right and the test was wrong. A known-answer
|
|
185
|
+
scenario's premise has to be measured, not assumed. Write your own scenarios with `harness_evolve.bench.Scenario`
|
|
186
|
+
and `scripted_llm` (see `examples/math_agent/bench.py`).
|
|
187
|
+
|
|
188
|
+
## Limits
|
|
189
|
+
|
|
190
|
+
- It needs an automatic pass/fail check per case. No grader, no optimizer.
|
|
191
|
+
- A noisy agent needs more cases or trials before any gain is detectable; the noise band will tell
|
|
192
|
+
you, and "nothing accepted" is a real answer.
|
|
193
|
+
- Every candidate costs a full evaluation. Slow or expensive agents make slow, expensive runs.
|
|
194
|
+
|
|
195
|
+
## Credits
|
|
196
|
+
|
|
197
|
+
Full mapping of each component to its source, and what we changed: [docs/methods.md](docs/methods.md).
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
- Reflective, trace-driven proposals: GEPA (Agrawal et al., "GEPA: Reflective Prompt Evolution Can
|
|
201
|
+
Outperform Reinforcement Learning", ICLR 2026).
|
|
202
|
+
- The critic and the one-edit-per-round discipline: RRSI (google-research/rrsi, Apache-2.0); the
|
|
203
|
+
critic's reject classes are adapted from its prompt.
|
|
204
|
+
- Comparing against reruns: AI2's finding that harness evolution rarely beats simply rerunning
|
|
205
|
+
(arXiv 2607.12227), which is why the final report includes the matched-budget rerun baseline.
|
|
206
|
+
|
|
207
|
+
MIT license.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# harness-evolve
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sidreddy88/harness-evolve/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
Improve an AI agent's harness (its prompts, tool descriptions and settings) from the agent's own failure
|
|
6
|
+
traces, and keep a change only when it beats measured run-to-run noise.
|
|
7
|
+
|
|
8
|
+
Agents are noisy: run the same agent on the same task twice and the result can flip. Tune a prompt,
|
|
9
|
+
rerun, see +3 points, and you may have measured nothing. harness-evolve measures that noise first,
|
|
10
|
+
then accepts an edit only if it clears it, checks it doesn't break the cases that already work, and
|
|
11
|
+
reports the result on cases it never saw, against the cheapest alternative: just running the original
|
|
12
|
+
agent twice.
|
|
13
|
+
|
|
14
|
+
## Results
|
|
15
|
+
|
|
16
|
+
Used on a production code-diagnosis agent ([agent-platform](https://github.com/sidreddy88/agent-platform)), on SWE-bench Verified with
|
|
17
|
+
DeepSeek-V4.1-Flash:
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
| Held-out: 66 cases from repositories the optimizer never saw | Original harness | Evolved harness |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Finds the file the fix must change (single run) | 78.8% | **89.9%** (+11.1, 95% CI +6.1 to +16.2) |
|
|
24
|
+
| Gives up with no answer | 19.7% | 5.6% |
|
|
25
|
+
| Solves it on all 3 trials (reliability) | 60.6% | 78.8% |
|
|
26
|
+
|
|
27
|
+
The evolved harness matched running the original twice (best-of-2) at about 60% of the cost.
|
|
28
|
+
Two edits were accepted in a 7.4-hour run, and the evolved harness went to production.
|
|
29
|
+
|
|
30
|
+
It also says no. An earlier run of the same optimizer on Claude Sonnet 5 accepted nothing, and so
|
|
31
|
+
did a three-round run on the same project's fix agent: every candidate edit either stayed inside the
|
|
32
|
+
noise band or broke a guard case. Halfway through that run, one edit led 7 to 4, and a single
|
|
33
|
+
measurement would have reported it as a gain.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
The example is a small math agent (a calculator tool and a submit tool) on GSM-Hard, GSM8K questions
|
|
38
|
+
with large, awkward numbers that leave room to improve.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/sidreddy88/harness-evolve && cd harness-evolve
|
|
42
|
+
pip install -e ".[litellm]"
|
|
43
|
+
python examples/math_agent/fetch_data.py # downloads 80 problems
|
|
44
|
+
export TOGETHER_API_KEY=... # or any litellm provider; set MODEL to match
|
|
45
|
+
harness-evolve run examples/math_agent/config.py --run-dir runs/math-1 --budget 2
|
|
46
|
+
harness-evolve status runs/math-1
|
|
47
|
+
harness-evolve diff runs/math-1 # accepted edits, as diffs to review
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
What to expect (our run, DeepSeek-V4.1-Flash, $1.06): the starting harness solved 76% of the 50 evolve
|
|
51
|
+
problems, with a measured noise band of ±5.6 points. The proposer read the trajectories and aimed three
|
|
52
|
+
edits at real failures (runs that never submit an answer, then wrong arithmetic); each gained +2 to +3
|
|
53
|
+
points, inside the noise, so none was accepted. With 50 cases only bigger gains are detectable; more
|
|
54
|
+
cases narrow the band.
|
|
55
|
+
|
|
56
|
+
`--budget` is a hard cap in dollars. A run that stops (budget, Ctrl-C, a provider outage) resumes with
|
|
57
|
+
the same command, and nothing already paid for is paid again.
|
|
58
|
+
|
|
59
|
+
## How a run works
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
round 0 run the starting harness on the evolve cases, 2 trials each
|
|
63
|
+
-> the noise band (how much the pass rate moves between identical runs)
|
|
64
|
+
round r propose one edit, aimed at failures in the trajectories (GEPA-style)
|
|
65
|
+
-> structural checks (free)
|
|
66
|
+
-> a critic screens for leakage, gaming and weakened safeguards (cheap)
|
|
67
|
+
-> evaluate the candidate (the only expensive step)
|
|
68
|
+
-> accept only if it beats the noise band, or is cheaper beyond cost noise
|
|
69
|
+
at the same pass rate, and no guard case regresses
|
|
70
|
+
final original vs evolved on held-out cases, k trials each, including the
|
|
71
|
+
original's best-of-2 (the matched-budget rerun baseline)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Accepted edits are written as diffs under `runs/<id>/proposals/` for you to review; your code is never
|
|
75
|
+
changed automatically.
|
|
76
|
+
|
|
77
|
+
Built for long unattended runs: every phase is checkpointed; every case's result is cached by the
|
|
78
|
+
candidate's content hash; a hard budget is checked before each case; provider failures (billing, auth,
|
|
79
|
+
rate limits) stop the run instead of being scored as failures; tripwires pause it when measurements
|
|
80
|
+
look broken; a watchdog exits cleanly if it stops making progress.
|
|
81
|
+
|
|
82
|
+
## Using it on your agent
|
|
83
|
+
|
|
84
|
+
You need five things.
|
|
85
|
+
|
|
86
|
+
**1. A harness directory your agent reads at run time**
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
my_harness/
|
|
90
|
+
system.prompt # templates, rendered with str.format
|
|
91
|
+
task.prompt
|
|
92
|
+
tool_descriptions.json # {"search": "Search the docs ..."}
|
|
93
|
+
settings.json # {"max_turns": 12, "_doc": {"max_turns": "turn budget"}}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Load it with `harness_evolve.load_harness(path)`. The optimizer edits copies of these files, never
|
|
97
|
+
adds or deletes files, and checks every edit still loads.
|
|
98
|
+
|
|
99
|
+
**2. An evaluator**
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
class MyEvaluator:
|
|
103
|
+
async def evaluate(self, harness_dir, case_ids, trials) -> EvalOutcome:
|
|
104
|
+
... # run your agent with harness_dir on each case, `trials` times
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Return pass/fail and cost per trial, and trajectories if you can: the proposer reads them to find
|
|
108
|
+
out why runs fail. Raise `ProviderFailure` for billing, auth or outage errors. Never let the right
|
|
109
|
+
answer reach the proposer: put it after `evidence.HIDDEN_MARKER` in a trajectory's `detail`.
|
|
110
|
+
See `examples/math_agent/evaluator.py`.
|
|
111
|
+
|
|
112
|
+
**3. A profile**
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from harness_evolve import AgentProfile, profiles
|
|
116
|
+
profiles.use(AgentProfile.build(
|
|
117
|
+
name="support-bot", harness_root="my_harness/",
|
|
118
|
+
agent_description="Answers support tickets with search and read tools; finishes by calling reply.",
|
|
119
|
+
safety_mechanisms=["citing a source document for every claim"],
|
|
120
|
+
setting_bounds={"max_turns": (3, 20)}))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The description and safety mechanisms become the proposer's and critic's instructions; setting
|
|
124
|
+
bounds keep an edit from "saving cost" by starving the agent.
|
|
125
|
+
|
|
126
|
+
**4. Cases**: an evolve set (what it improves on, ideally 50+), optional guard cases (must keep
|
|
127
|
+
passing) and held-out cases (never seen during evolution, for the final report).
|
|
128
|
+
|
|
129
|
+
**5. A model for the proposer and critic, and a budget**: `litellm_llm("model-id", prices=(in, out))`
|
|
130
|
+
or any async `(system, prompt) -> LLMReply(text, cost_usd)`.
|
|
131
|
+
|
|
132
|
+
Then:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
opt = Optimizer("runs/r1", "my_harness/", OptimizerConfig(evolve_cases=..., budget_usd=50),
|
|
136
|
+
evaluator=MyEvaluator(), proposer_llm=llm, critic_llm=llm, critic_patterns=[])
|
|
137
|
+
asyncio.run(opt.run_until_stopped())
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Or put a `build(run_dir, budget_usd, rounds)` function in a config file and use the CLI, as the
|
|
141
|
+
example does.
|
|
142
|
+
|
|
143
|
+
## Benchmarking the optimizer
|
|
144
|
+
|
|
145
|
+
`harness-evolve bench` measures the optimizer itself, so a change to its method is judged by
|
|
146
|
+
evidence, not opinion. Known-answer scenarios have a right outcome: a scripted proposer puts one known
|
|
147
|
+
edit in front of the acceptance rules. A real fix to a planted flaw must be accepted, a harmful edit
|
|
148
|
+
must be rejected, and a neutral rewording must not be accepted (a false accept). Open scenarios run the
|
|
149
|
+
real proposer and report what it finds and what it gains on held-out cases, per dollar.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
python examples/math_agent/fetch_data.py --n 260 --out examples/math_agent/data/problems-bench.jsonl
|
|
153
|
+
harness-evolve bench examples/math_agent/bench.py --out runs/bench-1
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Results go to `runs/bench-1/bench.md`. Our first run (DeepSeek-V4.1-Flash, $9.37 in total):
|
|
157
|
+
|
|
158
|
+
| Scenario | Expected | Result |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| accept-good: planted flaw (`max_turns` 1) + scripted fix | accepted | PASS: +49 points |
|
|
161
|
+
| reject-bad: scripted "round answers to the nearest hundred" | not accepted | PASS: −21.5 points, rejected |
|
|
162
|
+
| reject-noise: scripted neutral rewording | not accepted | PASS: rejected |
|
|
163
|
+
| find-flaw: planted flaw, real proposer | (reported) | found the flaw from trajectories: held-out 28.3% → 79.0% |
|
|
164
|
+
| standard: normal harness, 200 problems, real proposer | (reported) | one prompt edit accepted: held-out accuracy unchanged (83.3% → 83.9%), ~11% cheaper |
|
|
165
|
+
|
|
166
|
+
The first version of reject-bad ("skip the calculator and estimate") was accepted: it didn't hurt this
|
|
167
|
+
model at all and was 13% cheaper, so the optimizer was right and the test was wrong. A known-answer
|
|
168
|
+
scenario's premise has to be measured, not assumed. Write your own scenarios with `harness_evolve.bench.Scenario`
|
|
169
|
+
and `scripted_llm` (see `examples/math_agent/bench.py`).
|
|
170
|
+
|
|
171
|
+
## Limits
|
|
172
|
+
|
|
173
|
+
- It needs an automatic pass/fail check per case. No grader, no optimizer.
|
|
174
|
+
- A noisy agent needs more cases or trials before any gain is detectable; the noise band will tell
|
|
175
|
+
you, and "nothing accepted" is a real answer.
|
|
176
|
+
- Every candidate costs a full evaluation. Slow or expensive agents make slow, expensive runs.
|
|
177
|
+
|
|
178
|
+
## Credits
|
|
179
|
+
|
|
180
|
+
Full mapping of each component to its source, and what we changed: [docs/methods.md](docs/methods.md).
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
- Reflective, trace-driven proposals: GEPA (Agrawal et al., "GEPA: Reflective Prompt Evolution Can
|
|
184
|
+
Outperform Reinforcement Learning", ICLR 2026).
|
|
185
|
+
- The critic and the one-edit-per-round discipline: RRSI (google-research/rrsi, Apache-2.0); the
|
|
186
|
+
critic's reject classes are adapted from its prompt.
|
|
187
|
+
- Comparing against reruns: AI2's finding that harness evolution rarely beats simply rerunning
|
|
188
|
+
(arXiv 2607.12227), which is why the final report includes the matched-budget rerun baseline.
|
|
189
|
+
|
|
190
|
+
MIT license.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Draws docs/results.png from the held-out numbers in the README (agent-platform's
|
|
2
|
+
diagnosis agent, SWE-bench Verified, 66 cases from repositories the optimizer never saw,
|
|
3
|
+
DeepSeek-V4.1-Flash). Run: python docs/make_chart.py"""
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import matplotlib
|
|
7
|
+
|
|
8
|
+
matplotlib.use("Agg")
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import matplotlib.ticker
|
|
11
|
+
|
|
12
|
+
LABELS = ["Original\n1 run", "Original\nbest of 2 runs*", "Evolved\n1 run"]
|
|
13
|
+
ACC = [78.8, 89.9, 89.9]
|
|
14
|
+
COST = [0.033, 0.066, 0.040]
|
|
15
|
+
COLORS = ["#9aa5b1", "#9aa5b1", "#2563eb"]
|
|
16
|
+
|
|
17
|
+
fig, (a1, a2) = plt.subplots(1, 2, figsize=(9, 3.6), dpi=200)
|
|
18
|
+
for ax in (a1, a2):
|
|
19
|
+
ax.spines[["top", "right"]].set_visible(False)
|
|
20
|
+
ax.tick_params(axis="x", length=0)
|
|
21
|
+
|
|
22
|
+
b = a1.bar(LABELS, ACC, color=COLORS, width=0.6)
|
|
23
|
+
a1.set_ylim(0, 100)
|
|
24
|
+
a1.yaxis.set_major_formatter(matplotlib.ticker.PercentFormatter(decimals=0))
|
|
25
|
+
a1.set_title("Held-out accuracy", fontsize=11, loc="left")
|
|
26
|
+
for rect, v in zip(b, ACC, strict=True):
|
|
27
|
+
a1.text(rect.get_x() + rect.get_width() / 2, v / 2, f"{v:.1f}%", ha="center", fontsize=10,
|
|
28
|
+
color="white", fontweight="bold")
|
|
29
|
+
a1.errorbar(2, 89.9, yerr=[[89.9 - (78.8 + 6.1)], [(78.8 + 16.2) - 89.9]], fmt="none",
|
|
30
|
+
ecolor="#1e3a8a", capsize=4, lw=1)
|
|
31
|
+
|
|
32
|
+
b = a2.bar(LABELS, COST, color=COLORS, width=0.6)
|
|
33
|
+
a2.set_ylim(0, 0.08)
|
|
34
|
+
a2.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda v, _: f"${v:.2f}"))
|
|
35
|
+
a2.set_title("Cost per task", fontsize=11, loc="left")
|
|
36
|
+
for rect, v in zip(b, COST, strict=True):
|
|
37
|
+
a2.text(rect.get_x() + rect.get_width() / 2, v / 2, f"${v:.3f}", ha="center", fontsize=10,
|
|
38
|
+
color="white", fontweight="bold")
|
|
39
|
+
|
|
40
|
+
fig.text(0.01, 0.01, "66 SWE-bench Verified cases from repositories the optimizer never saw; DeepSeek-V4.1-Flash. "
|
|
41
|
+
"Error bar: 95% CI of the gain over one run.\n*Best of 2 assumes a perfect picker between the two answers.",
|
|
42
|
+
fontsize=7.5, color="#52606d")
|
|
43
|
+
fig.tight_layout(rect=(0, 0.08, 1, 1))
|
|
44
|
+
out = Path(__file__).parent / "results.png"
|
|
45
|
+
fig.savefig(out)
|
|
46
|
+
print(out)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Methods: what harness-evolve does, and where each idea comes from
|
|
2
|
+
|
|
3
|
+
Each part of the optimizer, the file it lives in, the source it comes from, and what we changed. Settings
|
|
4
|
+
are fields of `OptimizerConfig` (`loop.py`) or `AcceptanceConfig` (`acceptance.py`); "off by default" means
|
|
5
|
+
you turn it on there.
|
|
6
|
+
|
|
7
|
+
## The loop (`loop.py`)
|
|
8
|
+
|
|
9
|
+
Round 0 runs the starting harness on the evolve cases (2 trials each) and calibrates the noise band. Each
|
|
10
|
+
round then proposes one edit, checks it for free, screens it with a critic, evaluates it (the only
|
|
11
|
+
expensive step), and decides with fixed rules. An optional final phase compares the original and evolved
|
|
12
|
+
harness on held-out cases. Every phase is checkpointed; every case result is cached by the candidate's
|
|
13
|
+
content hash, so a stopped run resumes without paying twice.
|
|
14
|
+
|
|
15
|
+
## Proposing an edit (`proposer.py`, `evidence.py`)
|
|
16
|
+
|
|
17
|
+
| What | How | Source |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Reflective, trace-driven proposals | The proposer reads the agent's trajectories (tool calls, outputs, verdicts, cost) and the edit history, diagnoses what went wrong in words, and targets an edit at that failure, instead of mutating a score blindly | GEPA: Agrawal et al., "GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning", ICLR 2026. Ours is GEPA-style (hand-built; no GEPA library, no candidate pool) |
|
|
20
|
+
| One mechanism per candidate | Each candidate is one component, one hypothesis, one mechanism, so every measured change traces to one idea | RRSI's annealed edit budget at its floor (b = 1): Xia et al., "RRSI: Regularized Recursive Self-Improvement of Agent Harnesses", arXiv 2609.24972 |
|
|
21
|
+
| Two candidates per round | The proposer returns two alternatives with different mechanisms; if the critic rejects the preferred one, the other is tried | RRSI (two proposals per round) |
|
|
22
|
+
| Search/replace edits | Edits are exact find/replace operations on harness files, not full rewrites, so a long prompt isn't reproduced (and silently drifted) to change one paragraph | Ours |
|
|
23
|
+
| Settings shown as behaviour | Each setting appears with what it does and its allowed range | Ours: in our first runs the proposer never touched settings until they were shown this way |
|
|
24
|
+
| Answers kept from the proposer | Anything after `HIDDEN_MARKER` in a trajectory's `detail` is cut before the proposer sees it | Ours |
|
|
25
|
+
| Agent self-feedback | If trajectories include `self_feedback` (the agent's own words about the setup after a failing run), it is shown as evidence | Idea from HarnessCompass, arXiv 2608.01918 ("proactive first-person feedback") |
|
|
26
|
+
| Cost by prompt source | If trajectories include `cost_by_source`, the proposer sees where the money goes and is told to change what the harness asks for, never to ask the agent to be brief | Ours |
|
|
27
|
+
|
|
28
|
+
## Screening an edit (`candidates.py`, `critic.py`)
|
|
29
|
+
|
|
30
|
+
| What | How | Source |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Free structural checks | A candidate must keep the same files, valid JSON, the same setting keys and types, settings within their bounds (ranges or allowed values), the same tools, and no new template placeholders, before anything is spent | RRSI's deterministic checks after the critic |
|
|
33
|
+
| Leakage critic | A regex precheck (credentials, task ids, names from the evaluation data) then an LLM review with reject classes: task specialization, degenerate or safety-weakening changes, grader gaming, undeclared bundling, memory leakage, unbounded work, near-duplicates of rejected ideas, false tool claims. Rejections go back to the proposer for a bounded number of repairs | Adapted from RRSI's critic (google-research/rrsi, Apache-2.0); the instructions are generated from your agent's profile |
|
|
34
|
+
|
|
35
|
+
## Deciding (`acceptance.py`)
|
|
36
|
+
|
|
37
|
+
| Rule | How | Source |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Noise band | δ = 2 × the bootstrapped standard deviation of the pass-rate change between two runs of the unchanged harness (round 0's trial pairs) | RRSI (`calibrate.py`) |
|
|
40
|
+
| Floor | Never accept a candidate clearly worse than the best so far: S′ ≥ S* − δ | RRSI |
|
|
41
|
+
| Clear gain | A gain beyond δ may cost more, up to 10% plus 44.5% per point gained | RRSI's coding settings (β₀ = 0.10, β₁ = 44.5) |
|
|
42
|
+
| Inside the band | A score change counts for nothing (w_s = 0); the candidate wins only if it is cheaper by more than the cost noise band | RRSI (w_s = 0); the cost band is ours, calibrated the same way from trial-pair costs (`cost_band`, off by default) |
|
|
43
|
+
| Give-up guard | Veto if the share of runs that never submit an answer rises by more than 5 points or its own noise band (calibrated from round 0's trial pairs), whichever is larger | RRSI's domain veto on a rising no-submission rate |
|
|
44
|
+
| Guard cases | Veto if a guard case (one that must keep passing) fails on every trial of the candidate while the incumbent passed it at least once. A single failed trial isn't enough: guards that pass 80–95% of the time would veto about a third of candidates by chance | Ours |
|
|
45
|
+
| Cost only on shared successes | Compare cost only on cases both harnesses solve, so a cheaper failure never counts as a saving (`cost_on_shared_successes`, off by default) | Idea from the Hugging Face / FineEnvs "Multi-harness RL" article: efficiency is rewarded only on correct answers |
|
|
46
|
+
| Near-miss archive | Candidates just inside the noise band are kept and shown to later rounds to build on (`near_miss_fraction`, off by default) | Idea from the Darwin Gödel Machine (arXiv 2505.22954) and Huxley-Gödel Machine (arXiv 2510.21614): the best-scoring variant isn't always the best one to build on |
|
|
47
|
+
| Two-model check | An accepted edit must also hold on a second model's evaluation (`transfer_evaluator`, optional) | Ours, motivated by our finding that a prompt edit evolved for one model did nothing on another |
|
|
48
|
+
| Exploration rule | After N rejected candidates in a row, the next must change a component family none of them touched (`diversity_after`, off by default) | Simplified from RRSI's component novelty |
|
|
49
|
+
|
|
50
|
+
## Reporting (`report.py`, `pass_k.py`)
|
|
51
|
+
|
|
52
|
+
| What | How | Source |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Held-out comparison | Original and evolved harness on held-out cases, k ≥ 2 trials each | Standard practice; RRSI shows why (an unregularized evolved harness scored 92.8 on its own tasks and 40.3 held out) |
|
|
55
|
+
| Rerun baseline | The evolved harness's single run is compared with the original's best-of-2, estimated without bias from its own k trials. An evolved harness that costs one run should beat that, or you could have just rerun | AI2: Wang et al., "Rethinking the Evaluation of Harness Evolution for Agents", arXiv 2607.12227 |
|
|
56
|
+
| pass@k and pass^k | pass@k = chance at least one of k tries passes; pass^k = chance all k pass (reliability) | Unbiased estimators as in Chen et al. (HumanEval, 2021); pass^k as used in tau-bench (Yao et al., 2024) |
|
|
57
|
+
| Paired bootstrap | Each difference gets a 95% interval from a bootstrap over cases | Standard |
|
|
58
|
+
|
|
59
|
+
## Running unattended (`loop.py`, `health.py`, `budget.py`, `state.py`)
|
|
60
|
+
|
|
61
|
+
All ours, from running the optimizer for hours unattended: a hard budget checked before every case;
|
|
62
|
+
provider failures (billing, auth, rate limits) stop the run instead of being scored as failures; an
|
|
63
|
+
unpriced model reply stops the run instead of spending blind; tripwires pause it when measurements look
|
|
64
|
+
broken (a tool failing far above normal, $0 costs, errored runs); a watchdog exits cleanly if it stops
|
|
65
|
+
making progress; an optional smoke stage checks a few cases before real spend.
|
|
66
|
+
|
|
67
|
+
## Benchmarking the optimizer (`bench.py`)
|
|
68
|
+
|
|
69
|
+
Known-answer scenarios with a scripted proposer and critic (a real fix must be accepted, a harmful edit and
|
|
70
|
+
a neutral rewording must not), plus open scenarios with the real proposer. Ours. Lesson from the first run:
|
|
71
|
+
measure a planted edit's effect before treating it as a known answer.
|
|
Binary file
|