Project Write-Up

EvolveBench: Evaluating Coding Agents on Real Repositories

TL;DR. I co-developed EvolveBench to move evolutionary coding-agent evaluation from isolated benchmark functions to 11 real open-source repositories. My work focused on the evaluation problem: defining optimization targets, comparing handwritten and LLM-generated evaluators, and carrying the failures we found back into OpenEvolve through adaptive model routing, novelty detection, and automated warmup. We found that LLM-generated evaluators reached a 1.35× aggregate score versus 1.12× for handwritten evaluators, although the better evaluator still depended strongly on the task.

Why I Built EvolveBench

The AlgoTune experiments taught me how much an evolutionary coding agent depends on its evaluator. It also left an important question unanswered. AlgoTune presents a clean function, a reference implementation, and a narrow objective. Real software rarely does. The relevant code is embedded in a repository, performance depends on surrounding behavior, and the evaluator itself can encode the wrong goal.

I wanted to test whether the same approach would survive that messier setting. We built EvolveBench around real functions in actively maintained libraries, including CPython, pandas, SymPy, NetworkX, and python-jsonschema. Instead of asking only whether an agent could optimize code, we asked whether we could construct an evaluation signal precise enough to guide that optimization without rewarding regressions or superficial shortcuts.

Building the Benchmark

I helped turn 11 repository-level bottlenecks into repeatable optimization tasks. Each task needed a clearly bounded edit target, representative inputs, correctness checks, a stable performance measurement, and an aggregation rule that made results comparable. The targets covered algorithmic complexity, data structures, parser loops, cache policies, numerical routines, and library-specific regressions.

RepositoryOptimization target
BayesianOptimizationAcquisition-function random sampling
difflib (CPython)Redundant ratio computations in Differ._fancy_replace
python-jsonschemaEquality checks affected by a Python 3.12 regression
LMCacheLFU cache policy and minimum-frequency tracking
markoNested parser loops
NetworkXGraph algorithms
pandasrolling_rank for small windows
pymooNon-dominated sorting
python-chessChess-engine algorithms
python-pathfindingHeap operations
SymPyMin/Max local-zero discovery

Table 1. The 11 repositories and the performance bottleneck selected in each one.

The most important design choice was to treat the evaluator as an experimental variable. Each task ships with a handwritten evaluator built from direct inspection of the target, an LLM-generated evaluator synthesized from the task description, and a config for an LLM-as-a-judge evaluator for objectives that resist a fixed metric, though I only ran the first two below. This separated two capabilities that are often conflated: improving a program and specifying what improvement should mean.

Inside a Generated Evaluator

An evaluator has to do more than run a test suite. It must reject behaviorally invalid programs, expose a useful gradient among valid candidates, and measure the workload we actually care about. If any one of those pieces is weak, evolution can optimize the measurement instead of the program.

Evaluator propertyWhat it contributesTypical failure
Correctness oracleCompares candidate behavior with a trusted implementationA fast approximation passes incomplete tests
Input coverageExercises sizes, modes, and edge cases that change the algorithmic trade-offThe agent overfits one convenient workload
Continuous rewardDistinguishes incremental improvements instead of returning only pass/failSearch receives no direction until a large change succeeds
Stable timingSeparates steady-state execution from setup, compilation, and noiseThe score rewards measurement artifacts

Table 2. The four properties I use to review an optimization evaluator.

Example: rolling rank

For pandas_rolling_rank, the generated evaluator used pandas as the oracle and built a two-stage test surface. The quick stage covered five representative cases, while the comprehensive stage expanded to 12, including NaNs, infinities, ascending and descending order, three tie-breaking methods, percentile mode, and several array/window sizes. The core loop looked like this, abridged for readability1:

test_cases = generate_test_cases_stage2()

for case in test_cases:
    expected = pandas_rolling_rank(case)
    roller = RollingRank(**case.parameters)

    roller.compute(warmup_values)       # exclude JIT compilation
    start = time.perf_counter()
    result = roller.compute(case.values)
    elapsed = time.perf_counter() - start

    correct = compare_results(result, expected)
    reward = sigmoid_score(elapsed, case.target_time)

score = 0.7 * correctness + 0.3 * performance
Evaluator excerpt. Correctness comes from differential testing against pandas, while performance becomes a smooth reward relative to a calibrated target.

The staging matters. Small cases cheaply eliminate broken candidates before the full suite consumes the budget. Differential testing avoids maintaining hand-calculated outputs, while fixed random seeds make failures reproducible. Warming the Numba kernel before starting the timer measures steady-state throughput rather than compilation latency2.

The generated evaluator was not simply longer than the handwritten one. It encoded a different search landscape: more workload variation, measured baseline targets at three scales, and a smooth sigmoid reward around each target. This gave the agent credit for partial performance gains while keeping correctness dominant.

Leaderboard

EvolveBench scores AI-driven optimization systems on real repositories, not synthetic functions. The EvolveBench score is the harmonic mean of per-task speedups, following the aggregation convention used in AlgoTune. OpenEvolve is currently the only system benchmarked; each evaluator design it was run with counts as a separate config below. Submit a result for another system and it will be added here.

RankAgentModel / ConfigAggregate score
1OpenEvolveGemini 2.5 Flash (evolution) + Claude Sonnet 4.5 (LLM-generated evaluator)1.35×
2OpenEvolveGemini 2.5 Flash (evolution) + handwritten evaluator1.12×

Full per-task scores behind each aggregate are in the breakdown below.

What the Evaluator Comparison Showed

We ran the same 11 tasks with handwritten and LLM-generated evaluators. The EvolveBench score is the harmonic mean of per-task speedups, following the aggregation convention used in AlgoTune.

TaskHandwrittenLLM-generated
BayesianOptimization1.54×1.82×
difflib0.99×3.54×
jsonschema2.20×2.24×
lmcache0.65×1.02×
marko1.00×1.01×
networkx1.07×1.03×
pandas_rolling_rank1.00×2.31×
pymoo1.00×1.28×
python-chess1.01×1.01×
python-pathfinding1.27×1.39×
sympy2.40×0.99×
Aggregate score1.12×1.35×

Table 3. Per-task speedup and aggregate EvolveBench score under each evaluator design. Higher is better.

The aggregate result favored generated evaluators, but the task-level failures were more informative than the headline number. These numbers are post-run measurements of the selected programs, not the internal reward returned during evolution3. That separation is important: the evaluator guides search, while an independent benchmark tells us whether the resulting program improved.

Where the generated evaluator won

Rolling rank is the clearest example. The handwritten run returned an implementation that was effectively unchanged and measured at 1.00×. The generated-evaluator run instead produced a specialized Numba kernel: for every output position it scans the small active window, counts values below and equal to the current value, and computes the requested tie rule. Although that kernel is O(nw) rather than the skiplist's O(n log w), the JIT-compiled implementation and a smaller constant factor made it faster in the benchmark's small-window regime.

InputBaselineGenerated-evaluator resultSpeedup
1,000 values, window 500.295 ms0.077 ms3.82×
5,000 values, window 501.367 ms0.542 ms2.52×
10,000 values, window 502.691 ms1.165 ms2.31×

Table 4. Held-out rolling-rank timings. All three implementations passed the same eight correctness checks.

Here is the actual change, abridged to the two compute methods that decide the algorithm:

Before · seed program
    def compute(self, values):
        values = np.asarray(values, dtype=np.float64)
        series = pd.Series(values)
        result = series.rolling(
            window=self.window_size,
            min_periods=self.window_size
        ).rank(
            method=self.method,
            ascending=self.ascending,
            pct=self.pct
        ).values
        return result

pandas' own C-based skiplist, O(n log w). Correct by construction, but general-purpose.

After · evolved program
@njit(nopython=True, nogil=True)
def _numba_rolling_rank(values, window_size, method_code, ascending, pct, min_periods):
    n = len(values)
    result = np.full(n, np.nan, dtype=np.float64)
    for i in range(window_size - 1, n):
        current_val = values[i]
        if not np.isfinite(current_val):
            continue
        less, equal, valid_count = 0, 0, 0
        for j in range(i - window_size + 1, i + 1):
            val = values[j]
            if not np.isfinite(val):
                continue
            valid_count += 1
            if ascending:
                if val < current_val: less += 1
                elif val == current_val: equal += 1
            else:
                if val > current_val: less += 1
                elif val == current_val: equal += 1
        if valid_count < min_periods:
            continue
        min_rank = less + 1
        if method_code == 0:  # average
            max_rank = less + equal
            rank = (min_rank + max_rank) / 2.0
        elif method_code == 1:  # min
            rank = float(min_rank)
        else:  # max
            rank = float(less + equal)
        result[i] = rank / valid_count if pct else rank
    return result

    def compute(self, values):
        values = np.asarray(values, dtype=np.float64)
        return _numba_rolling_rank(
            values, self.window_size, self.method_code,
            self.ascending, self.pct, self.min_periods,
        )

Hand-rolled O(nw) windowed scan, JIT-compiled by Numba. Slower in theory, faster in practice for small windows.

Figure 1. The initial program vs. the LLM-generated-evaluator run's best program for pandas_rolling_rank, trimmed from the actual evolved files. The evaluator never told the agent to use Numba. It only rewarded a lower elapsed at each calibrated target.

The difflib run found a different kind of optimization. Its selected program cached cheap similarity bounds and skipped the expensive full ratio whenever the bound could not beat the current match. Across seven pathological input sizes, the generated-evaluator result reduced total time from 7.773 seconds to 2.196 seconds, a 3.54× speedup. The handwritten-evaluator result took 7.862 seconds, or 0.99×. The useful lesson was not that generation is inherently superior. It was that broader cases and a smoother signal can expose an optimization path that a carefully written but narrower harness fails to reward.

Where the handwritten evaluator won

SymPy provides the counterexample. The task optimizes Min/Max local-zero discovery over partially ordered symbolic values. Here, domain structure mattered more than generic workload diversity: the handwritten evaluator guided the run toward a 2.40× result, while the generated-evaluator run remained at 0.99×. This kind of symbolic code has semantic traps, such as comparability, transitivity, and antichains, that are difficult to infer from an interface alone.

Making OpenEvolve More Adaptive

EvolveBench grew out of problems we had already observed in the AlgoTune experiments. Naive model ensembles overwrote one another's progress, manual parameter sweeps consumed substantial effort, and incremental evolution sometimes proposed near-duplicate candidates. We used those failures as requirements for three additions to OpenEvolve.

Adaptive Model Selection

We replaced equal model alternation with Thompson sampling1. The router samples one model per iteration from a distribution fitted to recent reward history, while a minimum round-robin phase prevents early lockout. We also tested island-based variants that let different models preserve separate search trajectories.

ObservationEvidence
Best ensemble beat the best single model9 of 11 re-tested tasks
Largest improvementConnected components: 2.79× to 5.62×
Tasks where the ensemble regressedCholesky factorization and graph Laplacian

Table 5. Directional results from single, unrepeated ensemble runs. They are not seed-averaged.

The result changed how I think about multi-model systems. Diversity is useful only when the system protects useful state and assigns work based on evidence. On connected components, removing migration let a Union-Find strategy mature without being replaced by incompatible breadth-first-search edits. No ensemble configuration won everywhere, so the routing policy remains part of the optimization problem.

Novelty Detection

We added a local embedding-based novelty check that compares a new program against the current island population before it consumes an expensive evaluation. This turns a pattern I previously found by manually reading trajectories into an explicit filter in the search loop:

class NoveltyChecker:
    def __init__(self, embedding_client, similarity_threshold: float = 0.95):
        self.embedding_client = embedding_client
        self.similarity_threshold = similarity_threshold

    async def check_novelty(self, candidate_code, island_programs):
        candidate_embedding = await self.embedding_client.generate_embedding_for_code(candidate_code)
        max_sim = max(
            cosine_similarity(candidate_embedding, prog.metadata["code_embedding"])
            for prog in island_programs
        )
        return NoveltyCheckResult(is_novel=max_sim <= self.similarity_threshold, max_similarity=max_sim)

A candidate above the 0.95 threshold is not simply discarded. OpenEvolve regenerates from the same parent up to a fixed number of attempts, raising the sampling temperature after each one and telling the model what its last attempt was too similar to, so a rejection becomes feedback rather than a dead end. Only after exhausting those attempts does it fall back to the least similar candidate produced. Embeddings run locally (all-MiniLM-L6-v2), so novelty checking adds no LLM cost.

Two-Phase Warmup

We also automated the prompt and hyperparameter sweeps that had been launched manually in the AlgoTune experiments, as a fixed-budget phase that runs before the main evolution loop:

@dataclass
class WarmupConfig:
    prompt_optimization_budget: int = 5   # Phase 1: prompt variants
    hyperparameter_budget: int = 5        # Phase 2: configs to test
    iterations_per_config: int = 5        # Phase 2: iterations per config
    temperature_min: float = 0.2
    temperature_max: float = 0.9
    exploration_min: float = 0.1
    exploration_max: float = 0.4

Phase 1 runs pairwise feedback descent: generate a prompt variation, evaluate the original and the variant on a one-iteration mini-experiment, then ask the model what made the winner better and fold that into an improved prompt. Phase 2 tests up to hyperparameter_budget hand-designed presets (conservative, exploratory, balanced, and focused combinations of temperature and exploration/exploitation ratio), runs each for iterations_per_config iterations, and keeps whichever preset produced the largest measured improvement. At the reference 50-call budget, that is 5 calls for prompt optimization, 25 for hyperparameter trials, and 20 left for the main search.

The class implementing this is named BayesianWarmupOptimizer, but the name is aspirational rather than descriptive. Phase 2 has no surrogate model and no acquisition function. It picks the best of a small, fixed set of presets by direct measurement, which is closer to a greedy grid search than to Bayesian optimization. Fixing that gap, along with running the ablation above, is the concrete next step.

What I Learned

  1. An evaluator is an executable specification. It determines which behavior the agent learns to preserve and which shortcuts it is allowed to exploit.
  2. Real repositories expose hidden dependencies. A locally faster function is not an improvement if it breaks callers, changes numerical behavior, or moves work elsewhere.
  3. Automating evaluator construction changes the bottleneck. It reduces manual setup, but shifts more responsibility to validation and adversarial testing of the generated objective.
  4. Adaptive systems need evidence at every layer. Model routing, novelty filtering, and warmup tuning are useful only when their feedback is comparable and reproducible.

Notes

  1. The excerpt preserves the generated evaluator's control flow but shortens its dictionaries and helper names. The source uses time.time(), though perf_counter() is shown here because it states the intended monotonic timing semantics more clearly.
  2. Excluding compilation is appropriate for repeated, steady-state calls. A deployment dominated by first-call latency should report compilation separately rather than hide it.
  3. The difflib comparison used 200 repetitions at each of seven sizes, rolling rank used 10 runs per configuration, and SymPy used 10 runs at each of five symbol counts. These repetitions characterize timing noise for the selected programs, not variance across independent evolution seeds.

References

  1. On the Likelihood that One Unknown Probability Exceeds Another in View of the Evidence of Two Samples[DOI]
    Thompson, W.R., 1933. Biometrika, 25(3–4), pp.285–294.