Agents are stochastic, so a single run per task tells you almost nothing - the same agent can solve a SWE-bench task on attempt three and fail it on attempts one and two. Eval-driven development needs a metric that accounts for that: pass@k, the probability that at least one of k sampled attempts succeeds.
Taking the empirical rate of "at least one pass in the first k runs" is biased and noisy. The standard unbiased estimator instead uses the counts directly: for a task with n total runs of which c passed,
pass@k=1−(kn)(kn−c)
The numerator counts the ways to draw k runs that are all failures. When c = 0 this is 1, giving pass@k = 0; when n - c < k there is no all-failure draw, so the binomial is 0 and pass@k = 1.
If a task has fewer than k runs, evaluate it at k' = n instead of discarding it.
A task where 0 < c < n is flaky - it neither reliably passes nor reliably fails, and those are the tasks worth reading traces for.
Implement:
def scorecard(runs, k):
Return:
Returns the four-key dict. Use math.comb; every averaged float is rounded to 4 dp.
runs = ([{"task_id": "t1", "tag": "web", "passed": True}]
+ [{"task_id": "t1", "tag": "web", "passed": False}] * 3)
print(scorecard(runs, 2)["pass_at_k"])
# 0.5
One pass in four runs: 1 - C(3,2)/C(4,2) = 1 - 3/6 = 0.5.
runs = [{'task_id':'t1','tag':'web','passed':True}] + [{'task_id':'t1','tag':'web','passed':False}] * 3
print(scorecard(runs, 2)['pass_at_k'])0.5
The single task has n = 4 runs with c = 1 pass. The number of all-failure pairs is C(3,2) = 3 out of C(4,2) = 6 possible pairs, so pass@2 = 1 - 3/6 = 0.5. With one task the suite mean is that same value.
0 <= len(runs) <= 5000; k >= 11 - comb(n - c, k') / comb(n, k') with k' = min(k, n)math.comb(a, b) is 0 when b > a, which gives pass@k = 1 - handle it, do not special-case it wronglyrunsby_tag is built by iterating sorted tag names; flaky is sorted ascending