TAI BUI
Back to curriculum
CapstonePython (stdlib)

Capstone Lesson 27: Eval Harness with Fixture Tasks

A coding agent is only as good as the tasks you measure it against. This lesson builds an eval harness: it runs a folder of fixture tasks through a candidate agent, scores each run pass or fail with a deterministic verifier, and reports pass@1, pass@k, mean latency, and mean cost. With that report in hand, you can tell a regression from a refactor.

TIME: ~90 minutesPREREQUISITES: Phase 19 · 25 (verification gates), Phase 19 · 26 (sandbox runner), Phase 14 · 30 (eval-driven agent development), Phase 14 · 19 (SWE-bench and GAIA benchmarks)

Learning Objectives

  • Define a fixture task as a triple of goal, setup, and verifier.
  • Score multiple sample runs per task and compute pass@1 and pass@k.
  • Aggregate latency into mean and p95, and cost into a per-task mean and a total.
  • Wire deterministic verifiers (file diff, exit code, regex match) into reusable functions.
  • Emit a structured JSON report a regression-tracking script can ingest.

The Problem

An eval harness is a program that runs a fixed set of tasks through an agent and scores every result with a deterministic check. Without one, agent benchmarks tend to fail in three ways.

The first is unverified pass. The agent says it fixed the bug, a human glances at the diff, the suite is marked green, and three weeks later the regression test surfaces the same bug. The agent reasoned plausibly and fixed nothing.

The second is undetected regression. A change to the prompt template makes the agent 4% better on the loud task and 14% worse on the quiet one. With no goldset and no per-task score, the regression rides into main and surfaces only when a customer complains.

The third is per-task drift. You ran the eval on Monday with 100 tasks and on Friday with 95, because somebody renamed five fixtures. The pass rate looks like a 5% improvement. It isn't.

The harness turns each of these into a fact. It runs every fixture, every time, in a reproducible order, against a verifier that returns true or false on a deterministic check.

Warm-up0/3
1. Which property does a good agent eval verifier need above all others?Warm-up
2. An LLM agent gets the exact same prompt five times with default sampling settings. Why might some runs pass and others fail?Warm-up
3. A CI job runs a command and it exits with a non-zero status code. What does that conventionally signal?Warm-up
Check: The Problem0/5
1. An eval run on Monday used 100 tasks and one on Friday used 95. Why can't you compare their pass rates directly?Explain
2. Why does the report keep a score for each task instead of only one overall pass rate?Explain
3. An agent says it fixed a bug, a person glances at the diff, and the suite goes green. Why is that a weak result?Explain
4. After a prompt change the overall pass rate rises by 2 points, yet users report that one kind of task broke. Which harness feature would have exposed it?Analyze
5. You want to compare prompt A and prompt B using the harness. Which setup keeps the comparison trustworthy?Design

The Concept

Mermaid diagram

A FixtureTask is a directory. Its task.json declares an id, a goal (the prompt fed to the agent), and a verifier block that names a function in the harness's verifier registry and supplies its args. The setup is the buggy/ directory next to it, whose files get copied into the scratch dir. Tasks that use file_equals also have a sibling expected/ directory.

Three verifier shapes cover most useful tasks.

file_equals compares a named file against expected content after the agent runs. Use it for "fix this bug in this exact way" tasks.

regex_match matches the named file's contents against a regex. Use it for "the function must exist and return X" tasks, where many solutions are acceptable.

shell_exit_zero runs a command in the scratch dir with a timeout (timeout_seconds, default 10) and passes the task only if the command exits zero. The demo uses a plain subprocess.run; production wiring goes through the sandbox from lesson 26. Use it for "the tests must pass" tasks.

The harness runs each task k times. Pass@k is 1 - (1 - p)^k, where p is the empirical pass rate. The harness also reports raw counts so you can spot variance. Latency is wall-clock per sample, reported as a mean and a p95. Cost is whatever the candidate reports in cost_units (tokens, USD, or any unit you pick), reported as mean_cost per task and a total_cost across the run.

Check: The Concept0/20
1. Which pieces make up one fixture task directory in this harness?Define
2. How does the harness aggregate latency and cost?Define
3. In a fixture task's task.json, what is the goal?Define
4. What is a verifier in this eval harness?Define
5. What is the verifier registry?Define
6. What does the file_equals verifier check?Define
7. What does the regex_match verifier do?Define
8. What does the shell_exit_zero verifier do?Define
9. What does a p95 latency of 40 ms tell you about a set of samples?Define
10. A task only needs the file to define a function that returns X, and many implementations are fine. Why prefer regex_match over file_equals?Explain
11. Why does the harness let the candidate report cost as free-form cost_units?Explain
12. Using pass@k = 1 - (1 - p)^k with p the empirical pass rate, a candidate passes 3 of 5 samples on a task. What is pass@5, rounded to two decimals?Apply
13. Five samples on one task take 12, 15, 11, 40 and 13 ms. The harness reports the mean and a nearest-rank p95 (the sorted value at index round(0.95 * n) - 1). Which pair is reported?Apply
14. The reference candidate reports cost_units of 1.0 per sample. The harness runs 5 tasks with k = 2. What are mean_cost per task and total_cost?Apply
15. file_equals runs with default args. The scratch file holds 'x = 1' with no trailing newline, and expected/ holds 'x = 1' followed by a newline. What is the verdict?Apply
16. The factorial task uses regex_match with the pattern ^\s*return\s+result\s*$ searched across lines. Which candidate edit passes?Apply
17. A shell_exit_zero task runs argv python3 -c 'import sys; sys.exit(3)' in the scratch dir. What does the verifier return?Apply
18. On the fizzbuzz task a candidate fixes the bug with range(1, n+1), no spaces, and the file_equals verifier reports failure. What explains this?Analyze
19. A candidate writes reverse_list with a for loop that builds and returns a correct reversed list, and the regex_match verifier fails it. What is the underlying problem?Analyze
20. You add a verifier that runs pytest in the scratch dir. Which existing verifier should you base it on?Design

Architecture

Mermaid diagram

The candidate is a callable: Callable[[FixtureTask, str], SampleResult]. The harness creates the scratch directory with tempfile.mkdtemp() and passes its path as a plain string. How the candidate works is none of the harness's business. It could be a deterministic patch applier (handy for harness self-tests), a real LLM agent, or a fuzzer. The contract is the SampleResult.

Check: Architecture0/2
1. What is the candidate's callable contract in this harness?Define
2. Why does the harness treat the candidate as an opaque callable taking a task and a scratch path?Explain

What you will build

main.py ships:

  1. FixtureTask dataclass.
  2. SampleResult dataclass: task_id, sample_index, latency_ms, cost_units, notes.
  3. TaskReport, EvalReport dataclasses with to_dict().
  4. VERIFIER_REGISTRY, a plain dict mapping verifier name to function. Built-in verifiers: file_equals, regex_match, shell_exit_zero.
  5. EvalHarness class. Runs a directory of tasks against a candidate. Returns EvalReport.
  6. Five fixture tasks bundled in tasks/:
    • off-by-one in fizzbuzz
    • missing return in factorial
    • typo in error message
    • empty function body
    • off-by-one in linked-list traversal
  7. A deterministic reference candidate (apply_known_fixes) the harness uses to demonstrate a clean pass@1 of 1.0.
  8. A demo that runs the reference candidate at k=1 and a noop candidate at k=3, and prints the results.

The fixture tasks are bundled as JSON files in tasks/, with starting files in tasks/<id>/buggy/ and the fixed versions in tasks/<id>/expected/. The harness copies buggy into a scratch dir, hands it to the candidate, and verifies against expected.

Check: What you will build0/5
1. Before every sample, the harness copies the task's buggy/ files into a fresh scratch directory. Why does that matter when k > 1?Explain
2. What does a clean pass@1 of 1.0 from the reference candidate demonstrate?Explain
3. A task.json names the verifier 'file_equal', a typo for file_equals. What does the harness do with that task's samples?Apply
4. After a prompt change, pass@1 drops. Which check best separates a harness bug from a real regression?Analyze
5. The reference candidate scores pass@1 of 0.8 on five fixtures. The failing sample's detail reads 'scratch file missing: fizzbvzz.py'. Where is the likely fault?Analyze

Why pass@k and not just pass@1

Pass@k is the probability of at least one pass in k independent samples. Real LLM agents are stochastic, so a single sample hides what the agent can do. A pass@1 of 0.6 looks like a failure. A pass@5 of 0.95 says the agent usually has the right answer and picks wrong on early samples. That points at sampling and ranking as the fix, not always more training. Pass@k makes the difference visible.

The harness reports pass@1 alongside pass@k because pass@k can paper over a real failure. If the model gets the right answer once in twenty tries, you do not have a useful agent.

Check: Why pass@k and not just pass@10/6
1. What does pass@k measure?Define
2. Why does the harness report pass@1 next to pass@k?Explain
3. Why does the report keep raw pass counts next to pass@1 and pass@k?Explain
4. A report shows pass@1 of 0.6 and pass@5 of 0.95. What does that most likely say about the agent?Analyze
5. A report shows pass@1 of 0.05 and pass@20 of about 0.64 for an agent. What is the right reading?Analyze
6. Your agent is stochastic and each sample costs money. You need to know how often its first answer works and whether it can solve a task at all. What should the report show?Design

How this composes with the rest of Track A

Lesson 25 produced the gate chain. Lesson 26 produced the sandbox, which production wiring of shell_exit_zero should go through. Lesson 28 wraps each harness run in an OTel trace. Lesson 29 runs the end-to-end demo against one of the bundled fixtures and asserts pass@1 = 1.0 for the reference candidate.

Check: How this composes with the rest of Track A0/1
1. In the demo, shell_exit_zero uses a plain subprocess.run. What should production wiring do instead?Explain
Author's Note

I'd point the harness at the reference candidate before I pointed it at a real agent. The reference candidate applies known fixes, so a clean pass@1 of 1.0 is the expected result. If the harness can't produce that, I'd fix the harness before trusting any other run.

I'd also keep the raw pass counts in view next to pass@1 and pass@k, since the report gives you all three and a single rate can hide variance.

For verifiers, I'd reach for regex_match when many solutions are acceptable and save file_equals for tasks where I really do mean "fix it in this exact way."

Running it

bash
cd phases/19-capstone-projects/27-eval-harness-fixture-tasks
python3 code/main.py
python3 -m pytest code/tests/ -v

The demo runs the reference candidate at k=1 and prints the EvalReport as JSON, including pass@1, mean latency, and a per-task breakdown. It then runs a noop candidate at k=3 and prints its pass@1 and pass@k. It exits nonzero if the reference candidate misses pass@1 of 1.0 or the noop candidate passes anything, and zero otherwise. The tests cover the verifier functions, the pass@k math, fixture loading, and the harness end-to-end against the bundled reference candidate.

Check: Running it0/3
1. A harness run has two tasks at k = 2. Task A passes both samples and task B passes neither. What overall pass_at_k does the report show?Apply
2. The reference candidate passes all five fixtures, but the noop candidate at k = 3 passes one task on a single sample. What does the demo do?Apply
3. A regression script ingests the harness's JSON report for a whole run. Which top-level keys does EvalReport.to_dict() emit?Apply