File size: 13,508 Bytes
3bc7cb3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# TODO: SPRT Early Stopping for AgentArena Evaluation

**Origin:** Gemini research suggestion β€” replace fixed k=3 pass@k with Sequential Probability Ratio Test (SPRT) for cost-aware early stopping.

**Status:** Research/Design β€” not yet scheduled for implementation.

---

## What is SPRT

SPRT (Sequential Probability Ratio Test) is a statistical decision procedure by Abraham Wald (1945). Instead of collecting a fixed sample size and then testing, SPRT tests after each new observation and makes one of three decisions:

1. **Accept H0** (null hypothesis) β€” stop, evidence is clear enough that agent fails
2. **Accept H1** (alternative hypothesis) β€” stop, evidence is clear enough that agent passes
3. **Continue** β€” not yet enough evidence, run another trial

The test maintains a **likelihood ratio** Ξ› that accumulates evidence:

```
Ξ›_n = product of [ P(x_i | H1) / P(x_i | H0) ] for i=1..n
```

For binary pass/fail outcomes with pass probability p0 (null, agent is bad) and p1 (alternative, agent is good):

```
# After each run with outcome x_i in {0, 1}:
Ξ› *= (p1 ** x_i * (1-p1) ** (1-x_i)) / (p0 ** x_i * (1-p0) ** (1-x_i))

if Ξ› >= B:  β†’ Accept H1 (agent passes), stop
if Ξ› <= A:  β†’ Accept H0 (agent fails), stop
else:       β†’ Run another trial
```

The decision boundaries A and B are derived from the acceptable error rates:

```
A = beta / (1 - alpha)      # lower boundary (accept H0)
B = (1 - beta) / alpha      # upper boundary (accept H1)
```

Where alpha = Type I error rate (false positive: calling a bad agent good) and beta = Type II error rate (false negative: calling a good agent bad). Typical values: alpha=0.05, beta=0.10.

Wald proved SPRT is optimal β€” it reaches a decision with the minimum expected number of samples given the error constraints.

---

## Why It Matters for AgentArena

Current state in `core/runner.py`, `run_pass_at_k`:

```python
async def run_pass_at_k(self, db, task, agent, k=3) -> list[Run]:
    group_id = str(uuid.uuid4())[:8]
    results = await asyncio.gather(*[_single(i) for i in range(k)])
    ...
```

All k=3 runs are fired in **parallel** via `asyncio.gather`. There is no mechanism to skip the third run even if the first two are unambiguous. Every agent-task pair always costs exactly 3x whatever a single run costs.

Concrete waste scenarios:

- **Clear failure:** Agent fails runs 1 and 2. If p_fail is very high, run 3 is paying ~$0.30-0.50 (typical Claude Code run) to confirm what is already statistically obvious.
- **Clear pass:** Agent passes runs 1 and 2. Same problem in reverse. pass@k with k=3 needs at least one pass to confirm capability β€” but if both pass, the marginal value of a third pass is low for ranking purposes.
- **Edge cases (1/2 split):** Here k=3 is most valuable and SPRT would correctly continue.

At 84 runs across 7 experiments (from eval dashboard), we are not yet at a scale where this pain is acute. But once we run comparative benchmarks across 5+ agents on 10+ tasks, the cost compounds fast. 50 tasks x 5 agents x 3 runs = 750 runs. SPRT might cut that to ~600.

---

## How It Works β€” Decision Boundaries

For AgentArena, sensible defaults:

```
p0 = 0.3   # null: agent solves the task less than 30% of the time β†’ "bad"
p1 = 0.7   # alt:  agent solves the task more than 70% of the time β†’ "good"
alpha = 0.05  # tolerate 5% false positives (bad agent called good)
beta  = 0.10  # tolerate 10% false negatives (good agent called bad)

A = beta / (1 - alpha)  = 0.10 / 0.95 β‰ˆ 0.105   # stop β†’ reject H1
B = (1 - beta) / alpha  = 0.90 / 0.05 = 18.0     # stop β†’ accept H1
```

Walk through example β€” agent that passes 2/3 runs (sequence: pass, fail, pass):

```
Start: Ξ› = 1.0

Run 1 β†’ pass (x=1):
  Ξ› *= (0.7 / 0.3) = 2.333    β†’ Ξ› = 2.333  (between A and B β†’ continue)

Run 2 β†’ fail (x=0):
  Ξ› *= (0.3 / 0.7) = 0.429    β†’ Ξ› = 1.000  (between A and B β†’ continue)

Run 3 β†’ pass (x=1):
  Ξ› *= (0.7 / 0.3) = 2.333    β†’ Ξ› = 2.333  (between A and B β†’ continue, but k=3 reached)
```

Here SPRT does not help β€” mixed results require all 3 runs. That is correct behavior.

Example β€” clear failure (0/2 runs):

```
Run 1 β†’ fail: Ξ› = 0.429      (above A=0.105 β†’ continue)
Run 2 β†’ fail: Ξ› = 0.184      (above A=0.105 β†’ continue)
Run 3 β†’ fail: Ξ› = 0.079      (below A=0.105 β†’ accept H0, stop)
```

With these thresholds, even 3 consecutive failures barely cross the boundary. To stop after 2 failures we would need tighter p0/p1 or higher beta. This is a parameter tuning problem.

Example with tighter priors (p0=0.2, p1=0.8, alpha=0.1, beta=0.2, Aβ‰ˆ0.22, B=9.0):

```
Run 1 β†’ fail: Ξ› = (0.2/0.8) = 0.25   (above A=0.22 β†’ continue)
Run 2 β†’ fail: Ξ› = 0.25*0.25 = 0.0625 (below A=0.22 β†’ STOP after 2 runs)
```

Tighter priors = earlier stopping = less data per decision. This is the core tradeoff.

---

## Implementation Sketch

The fundamental obstacle is that the current implementation uses `asyncio.gather` for parallelism. SPRT is inherently sequential β€” it cannot decide after run 2 if runs 1, 2, 3 all started simultaneously.

Two implementation paths:

### Option A: Sequential SPRT (true early stopping, loses parallelism)

Replace the gather loop with a sequential loop that checks SPRT after each run:

```python
# In runner.py β€” new method alongside run_pass_at_k

async def run_sprt(
    self,
    db: Session,
    task: Task,
    agent: Agent,
    k_max: int = 5,
    p0: float = 0.3,   # null hypothesis pass rate
    p1: float = 0.7,   # alternative hypothesis pass rate
    alpha: float = 0.05,
    beta: float = 0.10,
) -> tuple[list[Run], str]:
    """
    Run agent sequentially, stopping early when SPRT reaches a decision.
    Returns (runs, decision) where decision is 'h0_reject', 'h1_accept', or 'inconclusive'.
    """
    A = beta / (1 - alpha)           # lower boundary β†’ accept H0
    B = (1 - beta) / alpha           # upper boundary β†’ accept H1
    lambda_ratio = 1.0
    runs = []
    group_id = str(uuid.uuid4())[:8]

    for i in range(k_max):
        run = await self._run_single_for_k(db, task, agent, group_id)
        runs.append(run)

        x = 1 if run.status == RunStatus.success else 0
        lambda_ratio *= (p1 ** x * (1 - p1) ** (1 - x)) / \
                        (p0 ** x * (1 - p0) ** (1 - x))

        logger.info(
            "SPRT run %d/%d: status=%s, Ξ›=%.4f (A=%.4f, B=%.4f)",
            i + 1, k_max, run.status.value, lambda_ratio, A, B
        )

        if lambda_ratio <= A:
            return runs, "h0_reject"   # agent fails, stop early
        if lambda_ratio >= B:
            return runs, "h1_accept"   # agent passes, stop early

    return runs, "inconclusive"
```

Caller in the API endpoint or experiment orchestrator:

```python
runs, decision = await runner.run_sprt(db, task, agent, k_max=5)
# Store decision alongside runs for analysis
```

Tradeoff: loses wall-clock parallelism. If each run takes 90s, 3 sequential runs = 270s vs 90s parallel. For cost-optimized batch evaluation this is acceptable; for live benchmarks it is not.

### Option B: Hybrid β€” Parallel batch with SPRT gate

Run 2 runs in parallel first. Check SPRT. If inconclusive, run 1 more (or a second batch of 2). This preserves some parallelism while still saving cost on clear cases.

```python
async def run_sprt_batched(
    self, db, task, agent,
    batch_sizes: list[int] = [2, 1, 1, 1],  # run 2 first, then 1 at a time
    p0=0.3, p1=0.7, alpha=0.05, beta=0.10
) -> tuple[list[Run], str]:
    A = beta / (1 - alpha)
    B = (1 - beta) / alpha
    lambda_ratio = 1.0
    all_runs = []
    group_id = str(uuid.uuid4())[:8]

    for batch_size in batch_sizes:
        # Run batch in parallel
        batch = await asyncio.gather(
            *[self._run_single_for_k(db, task, agent, group_id)
              for _ in range(batch_size)],
            return_exceptions=True
        )
        # Filter exceptions, update lambda
        for run in batch:
            if isinstance(run, Exception):
                continue
            all_runs.append(run)
            x = 1 if run.status == RunStatus.success else 0
            lambda_ratio *= (p1**x * (1-p1)**(1-x)) / (p0**x * (1-p0)**(1-x))

        if lambda_ratio <= A:
            return all_runs, "h0_reject"
        if lambda_ratio >= B:
            return all_runs, "h1_accept"

    return all_runs, "inconclusive"
```

This is probably the right design for AgentArena: run 2 in parallel (quick), check, run a third if uncertain. Best of both worlds.

### Where to add it

File: `/home/smlflg/Projekte/AgentArena/core/runner.py`

- Add `run_sprt` and/or `run_sprt_batched` as new methods on `Runner`
- Keep `run_pass_at_k` unchanged (backward compat)
- Add a `sprt_decision` column to the `Run` model or store it in `metrics_json`
- The API endpoint that triggers evaluation (wherever `run_pass_at_k` is called) gets a flag: `early_stopping: bool = False`

---

## Expected Cost Savings

Assumptions:
- Average run cost: $0.40 (Claude Code, mid-complexity task)
- k=3 baseline cost per agent-task: $1.20
- Typical outcome distribution at current scale: 40% clearly pass (β‰₯2/3), 30% clearly fail (≀1/3), 30% mixed

With SPRT batched (2+1 scheme):
- Clear pass after 2 runs (call it 30% of cases): saves 1 run = $0.40
- Clear fail after 2 runs (call it 25% of cases): saves 1 run = $0.40
- Inconclusive (45%): runs all 3, no savings

Expected savings per agent-task pair:
```
0.30 * $0.40 + 0.25 * $0.40 = $0.22 saved per agent-task (out of $1.20)
β‰ˆ 18% cost reduction
```

At scale (50 tasks x 6 agents = 300 agent-task pairs):
```
Baseline:   300 * $1.20 = $360
With SPRT:  300 * $0.98 = $294
Savings:    ~$66 per full benchmark round
```

These numbers are rough. The actual savings depend heavily on the real pass-rate distribution. If most agents are mediocre (lots of mixed 1/3, 2/3 results), SPRT saves little. If agents polarize (clearly good or clearly bad), savings are higher.

The 18% figure in the briefing request assumes a generous distribution. A conservative estimate is 10-12%.

---

## Risks

**Reduced statistical power.** SPRT with tight boundaries makes decisions faster but with higher error rates in edge cases. An agent that truly passes 50% of the time (neither clearly good nor clearly bad) might get mislabeled.

**Bias toward early evidence.** If run infrastructure has systematic variance (cold-start latency, model version drift, time-of-day effects), the first 2 runs may not be representative. SPRT assumes i.i.d. observations. AgentArena runs are not perfectly i.i.d. β€” there can be sandbox setup jitter, API rate limit effects, etc.

**Parameter sensitivity.** The p0/p1 thresholds encode an opinion about what "good" and "bad" means. Wrong priors = wrong decisions. These need to be calibrated empirically from existing run data before deploying.

**Loss of parallelism (Option A only).** Sequential SPRT triples wall-clock time for most cases. For interactive benchmarking this is unusable. Option B (batched) avoids most of this.

**Audit trail complexity.** Experiments with different numbers of runs per agent are harder to compare. Pass@k requires knowing k. If k varies per agent, the formula `pass@k = 1 - C(n-c, k)/C(n,k)` needs adjustment. SPRT outcomes need their own metric column, not overloading pass@k.

**Inconclusive results are a new failure mode.** The current system always produces a definitive pass/fail count. SPRT introduces a third state. The dashboard and ranking logic need to handle this.

---

## Decision: Is It Worth Implementing Now?

**Current scale:** 84 runs across 7 experiments. At $0.40/run that is ~$34 total. SPRT would have saved maybe $6. Not meaningful.

**Recommendation: Defer to milestone.**

Implement SPRT when we hit one of these triggers:
1. A single benchmark round (one full agent x task matrix) costs more than $50
2. We run more than 5 agents against more than 10 tasks
3. We want to add a "fast screening" mode that does a cheap 2-run sanity check before committing to a full k=3 evaluation

Until then, k=3 fixed parallel is simpler, faster wall-clock, easier to reason about statistically, and the cost savings are not worth the added complexity.

**When we do implement:** start with Option B (batched 2+1) as a separate method `run_sprt_batched`. Keep `run_pass_at_k` unchanged. Add a feature flag in the experiment config. Calibrate p0/p1 from existing run data first.

---

## References

- Wald, A. (1945). *Sequential Tests of Statistical Hypotheses.* Annals of Mathematical Statistics, 16(2), 117–186. https://doi.org/10.1214/aoms/1177731118
- Wald, A. (1947). *Sequential Analysis.* Wiley. (The textbook β€” Chapter 3 has the boundary derivation.)
- Wikipedia overview (good starting point): https://en.wikipedia.org/wiki/Sequential_probability_ratio_test
- MLflow / Statsig implementation references for A/B testing context: https://www.statsig.com/blog/sequential-testing-on-statsig
- Python implementation (scipy-compatible): https://github.com/dmarx/sprt β€” small, readable, MIT license
- OpenAI Evals discussion on sequential testing: https://github.com/openai/evals/discussions/780 (community thread, not official)
- Chen et al. (2021) HumanEval pass@k formula: https://arxiv.org/abs/2107.03374 β€” relevant because AgentArena's pass@k is modeled on this; SPRT interacts with how k is defined here

---

*Written: 2026-03-19. Revisit when benchmark scale crosses the thresholds above.*