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.
Files changed (62) hide show
  1. harness_evolve-0.1.0/.github/workflows/release.yml +40 -0
  2. harness_evolve-0.1.0/.github/workflows/tests.yml +30 -0
  3. harness_evolve-0.1.0/.gitignore +8 -0
  4. harness_evolve-0.1.0/LICENSE +21 -0
  5. harness_evolve-0.1.0/PKG-INFO +207 -0
  6. harness_evolve-0.1.0/README.md +190 -0
  7. harness_evolve-0.1.0/docs/make_chart.py +46 -0
  8. harness_evolve-0.1.0/docs/methods.md +71 -0
  9. harness_evolve-0.1.0/docs/results.png +0 -0
  10. harness_evolve-0.1.0/examples/math_agent/agent.py +106 -0
  11. harness_evolve-0.1.0/examples/math_agent/bench.py +68 -0
  12. harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/nudge.prompt +1 -0
  13. harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/settings.json +8 -0
  14. harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/system.prompt +1 -0
  15. harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/task.prompt +5 -0
  16. harness_evolve-0.1.0/examples/math_agent/bench_harnesses/max_turns_1/tool_descriptions.json +4 -0
  17. harness_evolve-0.1.0/examples/math_agent/config.py +63 -0
  18. harness_evolve-0.1.0/examples/math_agent/evaluator.py +67 -0
  19. harness_evolve-0.1.0/examples/math_agent/fetch_data.py +41 -0
  20. harness_evolve-0.1.0/examples/math_agent/harness/nudge.prompt +1 -0
  21. harness_evolve-0.1.0/examples/math_agent/harness/settings.json +8 -0
  22. harness_evolve-0.1.0/examples/math_agent/harness/system.prompt +1 -0
  23. harness_evolve-0.1.0/examples/math_agent/harness/task.prompt +5 -0
  24. harness_evolve-0.1.0/examples/math_agent/harness/tool_descriptions.json +4 -0
  25. harness_evolve-0.1.0/pyproject.toml +35 -0
  26. harness_evolve-0.1.0/src/harness_evolve/__init__.py +20 -0
  27. harness_evolve-0.1.0/src/harness_evolve/acceptance.py +237 -0
  28. harness_evolve-0.1.0/src/harness_evolve/bench.py +137 -0
  29. harness_evolve-0.1.0/src/harness_evolve/budget.py +57 -0
  30. harness_evolve-0.1.0/src/harness_evolve/candidates.py +112 -0
  31. harness_evolve-0.1.0/src/harness_evolve/cli.py +120 -0
  32. harness_evolve-0.1.0/src/harness_evolve/critic.py +92 -0
  33. harness_evolve-0.1.0/src/harness_evolve/evaluator.py +48 -0
  34. harness_evolve-0.1.0/src/harness_evolve/evidence.py +93 -0
  35. harness_evolve-0.1.0/src/harness_evolve/grader.py +71 -0
  36. harness_evolve-0.1.0/src/harness_evolve/harness.py +62 -0
  37. harness_evolve-0.1.0/src/harness_evolve/health.py +121 -0
  38. harness_evolve-0.1.0/src/harness_evolve/history.py +91 -0
  39. harness_evolve-0.1.0/src/harness_evolve/llm.py +56 -0
  40. harness_evolve-0.1.0/src/harness_evolve/loop.py +857 -0
  41. harness_evolve-0.1.0/src/harness_evolve/pass_k.py +57 -0
  42. harness_evolve-0.1.0/src/harness_evolve/profiles.py +177 -0
  43. harness_evolve-0.1.0/src/harness_evolve/proposer.py +192 -0
  44. harness_evolve-0.1.0/src/harness_evolve/report.py +99 -0
  45. harness_evolve-0.1.0/src/harness_evolve/state.py +116 -0
  46. harness_evolve-0.1.0/tests/__init__.py +0 -0
  47. harness_evolve-0.1.0/tests/conftest.py +56 -0
  48. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/README.md +32 -0
  49. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/log_context_fetched.prompt +13 -0
  50. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/log_context_missing.prompt +2 -0
  51. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/prior_knowledge.prompt +4 -0
  52. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/retrieval_unavailable.prompt +2 -0
  53. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/settings.json +14 -0
  54. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/stack_trace.prompt +4 -0
  55. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/task_prompt.prompt +270 -0
  56. harness_evolve-0.1.0/tests/fixtures/diagnosis_harness/tool_descriptions.json +12 -0
  57. harness_evolve-0.1.0/tests/test_acceptance_phase_d.py +34 -0
  58. harness_evolve-0.1.0/tests/test_bench.py +89 -0
  59. harness_evolve-0.1.0/tests/test_example.py +85 -0
  60. harness_evolve-0.1.0/tests/test_optimizer.py +797 -0
  61. harness_evolve-0.1.0/tests/test_optimizer_transfer.py +91 -0
  62. 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,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ runs/
6
+ dist/
7
+ *.egg-info/
8
+ examples/math_agent/data/
@@ -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
+ [![tests](https://github.com/sidreddy88/harness-evolve/actions/workflows/tests.yml/badge.svg)](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
+ ![Held-out accuracy and cost per task: original one run 78.8% at $0.033, original best of 2 runs 89.9% at $0.066, evolved one run 89.9% at $0.040](https://raw.githubusercontent.com/sidreddy88/harness-evolve/main/docs/results.png)
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
+ [![tests](https://github.com/sidreddy88/harness-evolve/actions/workflows/tests.yml/badge.svg)](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
+ ![Held-out accuracy and cost per task: original one run 78.8% at $0.033, original best of 2 runs 89.9% at $0.066, evolved one run 89.9% at $0.040](https://raw.githubusercontent.com/sidreddy88/harness-evolve/main/docs/results.png)
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