pass@k Scorecard for an Agent Eval Suite
Problem Statement
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.
Background
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.
Your Task
Implement:
def scorecard(runs, k):
- runs: list of {"task_id": str, "tag": str, "passed": bool}, possibly many runs per task. A task's tag is taken from its first run in the list.
Return:
- tasks: number of distinct tasks
- pass_at_k: the unweighted mean of the per-task pass@k over all tasks, rounded to 4 decimal places (0.0 when there are no tasks)
- by_tag: dict of tag -> mean per-task pass@k for that tag, rounded to 4 dp, built by iterating tags in sorted order
- flaky: sorted list of task ids with 0 < c < n
Input/Output Format
Returns the four-key dict. Use math.comb; every averaged float is rounded to 4 dp.
Sample
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.
Example:
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.
Constraints:
0 <= len(runs) <= 5000;k >= 1- Use the unbiased estimator
1 - comb(n - c, k') / comb(n, k')withk' = min(k, n) math.comb(a, b)is 0 whenb > a, which gives pass@k = 1 - handle it, do not special-case it wrongly- Tasks are weighted equally regardless of how many runs they have
- A task's tag comes from its first appearance in
runs by_tagis built by iterating sorted tag names;flakyis sorted ascending- Every returned float is rounded to 4 decimal places
1. Background Knowledge
The pass@k metric is a standard evaluation method in AI agent and code generation research, particularly for benchmarks like HumanEval or SWE-bench. Because Large Language Models (LLMs) are stochastic, a single execution does not reliably reflect an agent's capability. Instead, we sample k independent attempts for a given task. The metric estimates the probability that at least one of these k attempts produces a correct solution.
The formula provided, pass@k=1−(kn)(kn−c), is an unbiased estimator derived from combinatorics. Here, n is the total number of runs for a task, and c is the count of successful runs. The term (kn) represents the total ways to choose k runs from n. The term (kn−c) represents the ways to choose k runs such that all of them are failures (since there are n−c failures total). Subtracting this ratio from 1 gives the probability that the chosen set of k runs contains at least one success.
Crucially, this problem involves aggregation and grouping. You are not just calculating a single metric; you are processing a stream of heterogeneous data points (runs) into structured statistics. This requires grouping data by task_id to compute per-task metrics, and then aggregating those metrics globally and by tag. The concept of a flaky task (0<c<n) highlights the importance of analyzing variance in agent performance, not just the mean.
2. Algorithm Approach
The core algorithmic pattern here is Map-Reduce or Group-By-Aggregate.
- Grouping (Map): Iterate through the input runs list once. Group the runs by task_id. During this pass, you must also capture the tag associated with each task (specifically from the first occurrence of that task in the list) and count the number of passes (c) and total runs (n) for each task.
- Per-Task Calculation (Reduce): For each unique task, apply the pass@k formula. Handle the edge case where n<k by using k′=n.
- Aggregation: Compute the global mean of these per-task scores. Simultaneously, group these scores by tag to compute tag-specific means.
- Filtering: Identify tasks where 0<c<n to populate the flaky list.
This approach ensures that you process the data in O(N) time, where N is the number of runs, avoiding repeated scans of the input list.
3. Step-by-Step Strategy
- Initialize Data Structures: Create a dictionary to store task statistics. Keys will be task_id, and values will be objects or dictionaries containing n (total runs), c (passed count), and tag.
- Process Runs:
- Iterate through each run in the runs list.
- If the task_id is not in your dictionary, initialize it with n=0, c=0, and store the tag from this run.
- Increment n for this task.
- If passed is True, increment c.
- Compute Per-Task Metrics:
- Create a list to hold per-task pass@k scores.
- Create a dictionary to hold tag-specific scores.
- Create a list for flaky tasks.
- Iterate through the collected task statistics.
- For each task, determine the effective k value: keff=min(k,n).
- Calculate pass@keff=1−(keffn)(keffn−c). Note: If n−c<keff, the numerator is 0, resulting in a score of 1.0.
- Append this score to the global list and to the list corresponding to its tag.
- If 0<c<n, add the task_id to the flaky list.
- Final Aggregation:
- Calculate pass_at_k as the mean of the global score list, rounded to 4 decimal places. Handle the empty case.
- Calculate by_tag by iterating through sorted unique tags. For each tag, compute the mean of its associated scores, rounded to 4 decimal places.
- Sort the flaky list alphabetically.
- Return Result: Construct and return the dictionary with keys tasks, pass_at_k, by_tag, and flaky.
4. Common Pitfalls
- Tag Assignment: The problem states the tag is taken from the first run of a task. If you simply update the tag on every run, you might overwrite the correct tag if later runs have different tags (though the problem implies consistency, strict adherence to "first run" is safer).
- Combinatorial Edge Cases: When n<k, you must use k′=n. If you blindly pass k to math.comb(n, k) where k>n, it may raise a ValueError or return 0 depending on the implementation, leading to incorrect division or errors.
- Division by Zero: Ensure you handle the case where there are no tasks (tasks == 0) to avoid division by zero when calculating the mean.
- Rounding Precision: Python's round() function uses "banker's rounding" (round half to even). While usually acceptable, ensure you round the final mean, not intermediate per-task scores, to minimize cumulative floating-point error.
- Sorted Output: The by_tag dictionary keys must be in sorted order. In Python 3.7+, dictionaries maintain insertion order, so you must iterate through sorted tags when building the dictionary. The flaky list must also be sorted.
- Flaky Definition: A task is flaky if it has some passes and some failures. A task with c=0 (all fail) or c=n (all pass) is not flaky.
5. Time & Space Complexity
- Time Complexity: O(N+TlogT), where N is the number of runs and T is the number of unique tasks.
- Iterating through runs takes O(N).
- Calculating combinations is O(1) for small integers or O(k) depending on implementation, but generally negligible compared to N.
- Sorting the flaky list and the tags for by_tag takes O(TlogT).
- Space Complexity: O(T), where T is the number of unique tasks.
- We store statistics for each unique task in a dictionary.
- The output structures (by_tag, flaky) also scale with the number of tasks.
- This is efficient as we do not store the entire input list in memory beyond the initial pass.