agentstats 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentstats-0.3.0/.github/workflows/release.yml +40 -0
- agentstats-0.3.0/.github/workflows/tests.yml +30 -0
- agentstats-0.3.0/.gitignore +7 -0
- agentstats-0.3.0/CHANGELOG.md +63 -0
- agentstats-0.3.0/LICENSE +21 -0
- agentstats-0.3.0/PKG-INFO +384 -0
- agentstats-0.3.0/README.md +365 -0
- agentstats-0.3.0/examples/judge_labels.jsonl +240 -0
- agentstats-0.3.0/examples/rules.json +5 -0
- agentstats-0.3.0/examples/switch-triage-haiku-5-5/README.md +27 -0
- agentstats-0.3.0/examples/switch-triage-haiku-5-5/analysis.md +25 -0
- agentstats-0.3.0/examples/switch-triage-haiku-5-5/decision/decision.json +711 -0
- agentstats-0.3.0/examples/switch-triage-haiku-5-5/decision/decision.md +41 -0
- agentstats-0.3.0/examples/switch-triage-haiku-5-5/preregistration.md +38 -0
- agentstats-0.3.0/examples/switch-triage-haiku-5-5/rows/candidate.jsonl +456 -0
- agentstats-0.3.0/examples/switch-triage-haiku-5-5/rows/current.jsonl +456 -0
- agentstats-0.3.0/pyproject.toml +38 -0
- agentstats-0.3.0/src/agentstats/__init__.py +3 -0
- agentstats-0.3.0/src/agentstats/adapters.py +115 -0
- agentstats-0.3.0/src/agentstats/analysis.py +342 -0
- agentstats-0.3.0/src/agentstats/checks.py +46 -0
- agentstats-0.3.0/src/agentstats/ci_templates.py +101 -0
- agentstats-0.3.0/src/agentstats/cli.py +191 -0
- agentstats-0.3.0/src/agentstats/comparisons.py +115 -0
- agentstats-0.3.0/src/agentstats/gate.py +382 -0
- agentstats-0.3.0/src/agentstats/intervals.py +123 -0
- agentstats-0.3.0/src/agentstats/judge.py +159 -0
- agentstats-0.3.0/src/agentstats/policy.py +105 -0
- agentstats-0.3.0/src/agentstats/prereg.py +139 -0
- agentstats-0.3.0/src/agentstats/records.py +150 -0
- agentstats-0.3.0/src/agentstats/reliability.py +58 -0
- agentstats-0.3.0/src/agentstats/report.py +152 -0
- agentstats-0.3.0/src/agentstats/sample.py +68 -0
- agentstats-0.3.0/src/agentstats/sizing.py +86 -0
- agentstats-0.3.0/src/agentstats/switch.py +180 -0
- agentstats-0.3.0/src/agentstats/switch_cli.py +140 -0
- agentstats-0.3.0/src/agentstats/switch_record.py +99 -0
- agentstats-0.3.0/src/agentstats/traces.py +161 -0
- agentstats-0.3.0/tests/fixtures/langfuse_traces.json +8 -0
- agentstats-0.3.0/tests/fixtures/otlp_traces.json +20 -0
- agentstats-0.3.0/tests/fixtures/rows_from_agent_platform_triage_gate.jsonl +6 -0
- agentstats-0.3.0/tests/fixtures/rows_from_harness_evolve.jsonl +6 -0
- agentstats-0.3.0/tests/test_adapters.py +50 -0
- agentstats-0.3.0/tests/test_analysis.py +145 -0
- agentstats-0.3.0/tests/test_calibration.py +87 -0
- agentstats-0.3.0/tests/test_gate.py +176 -0
- agentstats-0.3.0/tests/test_hardening.py +129 -0
- agentstats-0.3.0/tests/test_judge.py +64 -0
- agentstats-0.3.0/tests/test_prereg.py +110 -0
- agentstats-0.3.0/tests/test_rule_gates.py +62 -0
- agentstats-0.3.0/tests/test_sizing.py +47 -0
- agentstats-0.3.0/tests/test_stats.py +160 -0
- agentstats-0.3.0/tests/test_switch.py +125 -0
- agentstats-0.3.0/tests/test_traces.py +61 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag (v0.1.0, ...) is pushed. Uses PyPI trusted publishing:
|
|
4
|
+
# no token is stored anywhere; PyPI trusts this workflow in this repo via the "pypi" environment.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
- name: Tag must match the package version
|
|
19
|
+
run: |
|
|
20
|
+
python -c "import tomllib, os; v = tomllib.load(open('pyproject.toml', 'rb'))['project']['version']; t = os.environ['GITHUB_REF_NAME'].removeprefix('v'); assert v == t, f'tag {t} != pyproject version {v}'"
|
|
21
|
+
- run: pip install build twine
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- run: twine check dist/*
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- run: pytest -q
|
|
21
|
+
|
|
22
|
+
lint:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.13"
|
|
29
|
+
- run: pip install "ruff>=0.15"
|
|
30
|
+
- run: ruff check .
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
First release on PyPI.
|
|
6
|
+
|
|
7
|
+
**Model switches** (moved in from the stepdown package)
|
|
8
|
+
- `agentstats preregister`: write the switch rule (margin, confidence, cost ratio, design, provenance, slices, input
|
|
9
|
+
hashes) to `prereg.json` + `prereg.md` before any result exists.
|
|
10
|
+
- `agentstats switch`: SWITCH / KEEP / NOT YET for moving to a cheaper model (non-inferiority at a margin + cost
|
|
11
|
+
ratio, as-is and adapted steps, slices, provenance warning), with a decision record; with `--prereg` the rule comes
|
|
12
|
+
from the registration and is verified (committed, unchanged, before the results, inputs unchanged).
|
|
13
|
+
- Example: `examples/switch-triage-haiku-5-5/` (a production decision).
|
|
14
|
+
|
|
15
|
+
**Gates**
|
|
16
|
+
- `no_answer` outcome: attempts with no usable answer (step limit, unparseable output) count as failures but are
|
|
17
|
+
reported apart in `summary` and the gate; new requirement `max_no_answer`. A gate without it still notes a jump.
|
|
18
|
+
- Per-slice comparisons: `slice_by` in a tier prints candidate vs baseline per slice (fixed-cases, Bonferroni-adjusted
|
|
19
|
+
across slices, intervals clipped to the possible range); `max_slice_drop` fails on a proven slice drop and names
|
|
20
|
+
large unproven drops as leads.
|
|
21
|
+
|
|
22
|
+
- Rule gates: `min_rule_rate` (each trajectory rule holds at least this often, interval-based) and
|
|
23
|
+
`no_rule_regression` (each rule vs the baseline, fixed-cases, Bonferroni across rules). Behaviour checks that need
|
|
24
|
+
no labels; the gate prints a per-rule table.
|
|
25
|
+
|
|
26
|
+
**Inputs**
|
|
27
|
+
- `agentstats convert-traces`: OpenTelemetry (OTLP JSON, GenAI conventions) and Langfuse trace exports to rows; tool
|
|
28
|
+
calls become rule checks; `--verdicts` joins pass/fail by trace id, otherwise rows are rules-only and the rule gates
|
|
29
|
+
(and `summary`'s rule table) still work, while pass-rate requirements report INCONCLUSIVE instead of UNHEALTHY.
|
|
30
|
+
- Rows are validated on load with the file, line and field in the error (e.g. `'steps' must be a number, got list`).
|
|
31
|
+
- Optional `schema_version` (1); newer versions are refused. Contract tests load rows written by harness-evolve and by
|
|
32
|
+
agent-platform's triage gate.
|
|
33
|
+
|
|
34
|
+
**Statistics**
|
|
35
|
+
- Calibration tests: Wilson coverage, fixed-cases coverage, the drop gate's false-alarm rate (12,000 simulations; the
|
|
36
|
+
6% bound fails if the small-sample correction is removed) and power, and the shock check's false-positive rate.
|
|
37
|
+
|
|
38
|
+
## 0.2.0 (not published)
|
|
39
|
+
|
|
40
|
+
**Gates for evals in CI**
|
|
41
|
+
- `agentstats.toml` policy with tiers (smoke / PR / nightly / release); `agentstats gate` exits 0 pass, 1 fail,
|
|
42
|
+
2 inconclusive, 3 unhealthy and writes the GitHub job summary.
|
|
43
|
+
- Two regression rules: `no_significant_drop` (fails only on a proven drop, for pull requests) and `no_regression`
|
|
44
|
+
(non-inferiority at a margin, for releases: an under-powered run is INCONCLUSIVE, with a runs estimate).
|
|
45
|
+
- `agentstats sample`: a deterministic, stratified smoke sample (critical cases always in).
|
|
46
|
+
- `agentstats init-ci`: a starter policy and smoke / nightly / release GitHub workflows.
|
|
47
|
+
- `agentstats size`: simulate a gate rule on your own baseline (pass / fail / inconclusive rates for a range of true
|
|
48
|
+
drops) to choose trials and margins.
|
|
49
|
+
|
|
50
|
+
**Statistics**
|
|
51
|
+
- Fixed-cases test for hand-picked eval sets, now the default for `curated`: per-case pass rates vs the baseline's,
|
|
52
|
+
noise from repeated trials per case, with a small-sample correction.
|
|
53
|
+
- Run-wide shock check (whole runs varying more than per-case randomness predicts): falls back to run-level statistics
|
|
54
|
+
with 3+ runs; with fewer, the gate asks for a rerun.
|
|
55
|
+
- Runs are compared on the cases attempted in every run; an infrastructure failure no longer removes its case.
|
|
56
|
+
- Pass-rate intervals clipped to the possible range.
|
|
57
|
+
- No graded attempts (every attempt an infrastructure failure) is UNHEALTHY, not a crash.
|
|
58
|
+
|
|
59
|
+
## 0.1.0
|
|
60
|
+
|
|
61
|
+
First version: Harbor / ATIF / JSONL inputs; pass rate with intervals chosen by eval-set design (curated / sampled /
|
|
62
|
+
clustered); pass@k and pass^k; infrastructure failures kept apart; efficiency with CIs; trajectory rule checks; judge
|
|
63
|
+
checks (Fleiss / Cohen kappa, strictness, leniency, flip rate); `summary`, `compare`, `judge`, `check`, `convert`.
|
agentstats-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siddharth Sukumar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentstats
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Statistics for agent evals: from trajectories to claims you can defend (CIs chosen by how your eval set was built, pass^k, infra failures kept apart, CI gates, behaviour rules, judge checks, pre-registered model-switch decisions).
|
|
5
|
+
Project-URL: Homepage, https://github.com/sidreddy88/agentstats
|
|
6
|
+
Project-URL: Repository, https://github.com/sidreddy88/agentstats
|
|
7
|
+
Project-URL: Changelog, https://github.com/sidreddy88/agentstats/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Siddharth Sukumar
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ci,confidence-intervals,evals,llm,llm-as-a-judge,non-inferiority,statistics
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: scipy>=1.11
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
17
|
+
Requires-Dist: statsmodels>=0.14; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# agentstats
|
|
21
|
+
|
|
22
|
+
Statistics for agent evals. Point it at your agent's runs (Harbor jobs, ATIF trajectories or a JSONL file) and
|
|
23
|
+
it tells you how good the agent is, how reliable it is, whether version B is really better than A, and whether a
|
|
24
|
+
cheaper model is shown to be no worse. Every number comes with a confidence interval, and the report says which
|
|
25
|
+
claim the numbers support.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install agentstats # Python 3.11+
|
|
29
|
+
agentstats summary jobs/run1 jobs/run2 jobs/run3
|
|
30
|
+
agentstats compare --a jobs/baseline --b jobs/candidate
|
|
31
|
+
agentstats judge labels.jsonl
|
|
32
|
+
agentstats gate --policy agentstats.toml --tier nightly --candidate results/ --baseline baseline/
|
|
33
|
+
agentstats convert-traces traces.json --format otlp --rules rules.json --out rows.jsonl # or --format langfuse
|
|
34
|
+
agentstats preregister --name my-switch --margin 0.03 --provenance independent --out decisions/my-switch
|
|
35
|
+
agentstats switch --prereg decisions/my-switch/prereg.json --current current.jsonl --candidate cheap.jsonl \
|
|
36
|
+
--out decisions/my-switch
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
Agents are stochastic, so a single eval run is a sample, not a measurement. On 100 SWE-bench Verified tasks, the
|
|
42
|
+
same agent with the same model and setup (mini-swe-agent, DeepSeek-V4.1-Flash) scored **48%, then 40%**. Eight
|
|
43
|
+
tasks flipped between identical runs. If you compare two versions on one run each, that noise is your result.
|
|
44
|
+
|
|
45
|
+
Three other things go wrong in practice:
|
|
46
|
+
|
|
47
|
+
- **Infrastructure failures get scored as agent failures.** A quota error or a billing outage looks like a bad
|
|
48
|
+
agent. agentstats keeps them out of pass rates, counts them, and warns if one arm had more.
|
|
49
|
+
- **The wrong interval for how the eval set was built.** Most eval sets are hand-picked. Treating each case as an
|
|
50
|
+
independent sample of real traffic gives intervals that are too narrow, and a claim the data can't support.
|
|
51
|
+
- **An unchecked LLM judge.** A lenient judge passes behaviour humans would reject, and that's how regressions
|
|
52
|
+
ship.
|
|
53
|
+
|
|
54
|
+
## What it does differently
|
|
55
|
+
|
|
56
|
+
Other good tools exist (see [Related tools](#related-tools)). agentstats is built around agent runs:
|
|
57
|
+
|
|
58
|
+
1. **Reads trajectories, not score tables.** Harbor job directories and ATIF trajectories directly: pass/fail,
|
|
59
|
+
infrastructure errors, tokens, cost, latency, steps and tool calls, plus deterministic rule checks on the
|
|
60
|
+
tool calls (`called`, `not_called`, `before`, `args`).
|
|
61
|
+
2. **Picks the statistics from how the eval set was built, and says what claim they support.**
|
|
62
|
+
|
|
63
|
+
| `--design` | Your eval set | Interval / comparison | Claim |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `curated` (default) | hand-picked cases, most eval sets | fixed cases: noise from repeated trials of each case (falls back to Student's t / Welch's t over complete runs when runs vary more than that predicts) | "on this eval set" |
|
|
66
|
+
| `sampled` | drawn at random from real traffic | per-case t / paired per-case differences | "on the population the cases came from" |
|
|
67
|
+
| `clustered` | sampled, in groups that fail together | cluster-robust standard errors | "on the population the groups came from" |
|
|
68
|
+
|
|
69
|
+
For a hand-picked set the cases are fixed, so the only randomness is the agent's own: the fixed-cases test
|
|
70
|
+
measures it from repeated trials of each case, and cases that always (or never) pass contribute nothing. It's
|
|
71
|
+
the paired test from a production diagnosis gate, plus a small-sample correction that brought simulated false
|
|
72
|
+
alarms from 6.8% to 5.8% at a nominal 5% (the rest is the discreteness of pass/fail; a simulated threshold
|
|
73
|
+
would close it). It checks for run-wide shocks (whole runs varying more than per-case randomness predicts, as
|
|
74
|
+
when a provider has a bad hour) and then falls back to treating each complete run as one observation. With one
|
|
75
|
+
run it uses a Wilson interval or an exact McNemar test. Overlapping groups can't be modelled credibly, so `clustered` refuses them and explains why.
|
|
76
|
+
3. **Reliability as its own result.** pass@k (capability) and pass^k (consistency) with CIs, and lists of
|
|
77
|
+
always-pass, never-pass and flaky cases.
|
|
78
|
+
4. **Gates that can say "I can't tell".** Two regression rules: `no_significant_drop` fails only on a proven drop
|
|
79
|
+
(for pull requests); `no_regression` passes only when the change is shown to be no worse than a margin
|
|
80
|
+
(non-inferiority, for releases), so an under-powered run is INCONCLUSIVE, with an estimate of the runs that would
|
|
81
|
+
settle it, never PASS. A run-wide shock with too few runs to correct for it is also INCONCLUSIVE ("rerun").
|
|
82
|
+
Infrastructure failures (Harbor exceptions or an `infra_error` field) are kept out of pass rates and make a run
|
|
83
|
+
UNHEALTHY (exit 3), as eval-sprint also does.
|
|
84
|
+
5. **Judge checks:** human agreement (Fleiss' κ), judge vs humans (accuracy, strictness, leniency, Cohen's κ,
|
|
85
|
+
each with a CI, on a held-out split if you mark one) and judge consistency over repeated runs (flip rate, mean
|
|
86
|
+
pairwise disagreement).
|
|
87
|
+
6. **Model switches decided by a rule written down first.** `agentstats switch` answers SWITCH / KEEP / NOT YET for
|
|
88
|
+
moving to a cheaper model: non-inferiority at your margin plus a cost-ratio limit, the model as-is and with an
|
|
89
|
+
adapted harness, per-slice results, and a warning when the eval set came from the current model.
|
|
90
|
+
`agentstats preregister` fixes the rule before any result exists, and the decision only counts as pre-registered
|
|
91
|
+
if the registration was committed before the results and its inputs are unchanged. See
|
|
92
|
+
[Deciding a model switch](#deciding-a-model-switch-agentstats-preregister-and-agentstats-switch).
|
|
93
|
+
|
|
94
|
+
## Example: did giving the fixing agent a diagnosis help?
|
|
95
|
+
|
|
96
|
+
mini-swe-agent on 100 SWE-bench Verified tasks, once plain and once with a diagnosis from a separate diagnosis
|
|
97
|
+
agent added to the task as a hint. One Harbor job each.
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
$ agentstats compare --a jobs-plain100/... --b jobs-hint100/... --a-name plain --b-name with-diagnosis
|
|
101
|
+
|
|
102
|
+
NO DETECTABLE DIFFERENCE: with-diagnosis minus plain = +1.0 pts (95% CI -6.2 pts to +8.2 pts),
|
|
103
|
+
on this eval set, from a single run per arm (indicative: run-to-run noise is unmeasured).
|
|
104
|
+
Method: exact McNemar test + paired differences. 100 matched cases; plain 48.0%, with-diagnosis 49.0%.
|
|
105
|
+
|
|
106
|
+
- McNemar (exact): 7 cases pass only with with-diagnosis, 6 only with plain; p = 1.
|
|
107
|
+
- Smallest difference this setup can reliably detect (80% power): about +10.2 pts.
|
|
108
|
+
- Cost per attempt: plain $0.0858, with-diagnosis $0.0765; difference $-0.0093 (95% CI -0.0182 to -0.0005).
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The +1 point is noise: 13 tasks moved, in both directions, and this setup can't see anything smaller than about
|
|
112
|
+
10 points. The cost difference is real (its CI excludes zero): the hint made the agent about 11% cheaper.
|
|
113
|
+
|
|
114
|
+
The same plain setup, run twice:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
$ agentstats summary jobs-plain100/... jobs-all500/...
|
|
118
|
+
|
|
119
|
+
Pass rate: 44.0% (95% CI 41.2% to 46.8%), on this eval set.
|
|
120
|
+
Method: fixed cases, noise from repeated trials per case. 100 cases, 2 run(s).
|
|
121
|
+
- Not used: Student's t over complete runs would give 44.0% (95% CI 0.0% to 94.8%) (for hand-picked sets
|
|
122
|
+
with 3+ runs that show run-wide shocks).
|
|
123
|
+
- Note: Whole runs vary 8.0x more than per-case randomness predicts (p = 0.005): a run-wide shock. With fewer
|
|
124
|
+
than 3 runs there's nothing safer to fall back to; treat this with care.
|
|
125
|
+
|
|
126
|
+
| k | pass@k (capability) | pass^k (consistency) |
|
|
127
|
+
| 1 | 44.0% (95% CI 34.5% to 53.5%) | 44.0% (95% CI 34.5% to 53.5%) |
|
|
128
|
+
| 2 | 48.0% (95% CI 38.0% to 58.0%) | 40.0% (95% CI 30.0% to 50.0%) |
|
|
129
|
+
Always pass: 40, never pass: 52, flaky: 8
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
48% and 40% is more than per-case randomness explains: something changed across the whole run between the two
|
|
133
|
+
(they ran at different times, in jobs of different sizes). A narrow interval from these two runs would be false
|
|
134
|
+
confidence, and the report says so. A third run lets it fall back to run-level statistics.
|
|
135
|
+
|
|
136
|
+
## In production: an incident-triage agent's CI gate
|
|
137
|
+
|
|
138
|
+
agentstats runs the regression gate for a production incident-triage agent (classifies alerts as real / noise /
|
|
139
|
+
duplicate with a P0-P3 severity): 114 held-out cases, 2 trials per pull request, compared case by case with a 4-trial
|
|
140
|
+
baseline using the fixed-cases test, `no_significant_drop` at a 90% interval.
|
|
141
|
+
|
|
142
|
+
**Sized by simulation from the baseline's own per-case rates** (each case's true rate drawn from its baseline trials,
|
|
143
|
+
then candidate and baseline simulated from it; `agentstats size` does the same for your baseline), against the gate it
|
|
144
|
+
replaced (fail if more than 6% of 80 filtered cases miss, one run):
|
|
145
|
+
|
|
146
|
+
| True drop | agentstats gate catches it | Old threshold gate |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| none (false alarm) | 5% | ~0% |
|
|
149
|
+
| ~5 points | 64% | 48% |
|
|
150
|
+
| ~8 points | 94% | 82% |
|
|
151
|
+
| ~12 points | 100% | 97% |
|
|
152
|
+
|
|
153
|
+
The old gate also had to drop 32 noisy cases to get under its threshold; the paired test keeps them, since each case is
|
|
154
|
+
compared with its own baseline rate.
|
|
155
|
+
|
|
156
|
+
**A skipped tool the labels rewarded.** The triage agent (then on Claude Haiku 4.5) skipped a required lookup tool
|
|
157
|
+
and wrote an invented count instead. The eval labels had been built from its own answers, so the accuracy gate couldn't
|
|
158
|
+
see it. With behaviour rules, Haiku 4.5 as the candidate against Haiku 5.5: accuracy +0.9 pts (CI -2.1 to +3.9), PASS;
|
|
159
|
+
`min_rule_rate` FAIL (lookup called in 25.4% of attempts, CI 21.0% to 30.5%); `no_rule_regression` FAIL (-74.6 pts vs
|
|
160
|
+
100%). On 61 real-incident traces exported from Langfuse and read with `convert-traces` (rules-only, no labels): the
|
|
161
|
+
lookup was called in 52.5% (CI 40.2% to 64.5%). The rules now run in that agent's CI gate.
|
|
162
|
+
|
|
163
|
+
**What it caught in its first week:**
|
|
164
|
+
- **An environment offset.** Six CI trials of unchanged code averaged 89.9% against a baseline measured on a laptop
|
|
165
|
+
(93.2%): -3.3 points, 95% CI -6.1 to -0.5, spread over many cases, no run-wide shock. One pull request had already
|
|
166
|
+
failed for it. The baseline is now measured in CI; the first gate run against it: -0.7 points (CI -3.4 to +2.1), PASS.
|
|
167
|
+
- **Outages, as UNHEALTHY instead of FAIL.** Once when the API credit ran out mid-run (69 of 228 attempts failed:
|
|
168
|
+
exit 3, the cause listed, the remaining attempts compared separately), once when a new major SDK release rejected a
|
|
169
|
+
request parameter on every call.
|
|
170
|
+
- **A doubtful "same setup".** Two runs of the same coding agent on the same 100 tasks scored 48% and 40%: the shock
|
|
171
|
+
check flagged it (whole runs varying 8x more than per-case randomness predicts, p = 0.005), so the narrow interval
|
|
172
|
+
wasn't trusted (see the example above).
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
## Deciding a model switch: `agentstats preregister` and `agentstats switch`
|
|
177
|
+
|
|
178
|
+
A cheaper model comes out. "No significant difference" on one eval run isn't "no worse": a small run usually just has
|
|
179
|
+
a wide interval. `agentstats switch` asks the question the other way round, as a non-inferiority test: is the cheaper
|
|
180
|
+
model **shown** to lose no more than your margin, at no higher cost? It answers **SWITCH**, **KEEP** or **NOT YET**
|
|
181
|
+
and writes a decision record (`decision.json` + `decision.md`) with the rule, the numbers and hashes of the inputs.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
agentstats preregister --name my-switch --margin 0.03 --provenance independent \
|
|
185
|
+
--input labels=evals/labels.jsonl --slice-by template --out decisions/my-switch
|
|
186
|
+
git add decisions/my-switch && git commit -m "pre-register my-switch" # before running anything
|
|
187
|
+
# ... run both models on the same cases, a few trials each, same session ...
|
|
188
|
+
agentstats switch --prereg decisions/my-switch/prereg.json \
|
|
189
|
+
--current current.jsonl --candidate cheap.jsonl [--adapted cheap-adapted.jsonl] --out decisions/my-switch
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Interval for (candidate - current) | Decision |
|
|
193
|
+
|---|---|
|
|
194
|
+
| lower bound above -margin, and the cost ratio's upper bound at most `--max-cost-ratio` (default 1.0) | SWITCH |
|
|
195
|
+
| upper bound below -margin (worse even at the optimistic end), or more expensive | KEEP |
|
|
196
|
+
| anything else | NOT YET |
|
|
197
|
+
|
|
198
|
+
- **Pre-registration is checked, not trusted.** With `--prereg` the rule comes from the file (contradicting flags are
|
|
199
|
+
an error), and the decision only counts as pre-registered if the registration is committed and unchanged, was
|
|
200
|
+
committed before every result file was last modified, and every declared `--input` still has its registered hash.
|
|
201
|
+
- **Two steps:** the cheaper model as-is, then with an adapted harness (`--adapted`, e.g. rows from
|
|
202
|
+
`harness-evolve export`); the better decision wins.
|
|
203
|
+
- **Slices** (`--slice-by`) are reported per slice; `--strict-slices` makes the decision no better than the worst one.
|
|
204
|
+
- **Provenance:** `--provenance current-model` says the eval set was made or filtered with the current model, and the
|
|
205
|
+
record warns that the comparison favours it.
|
|
206
|
+
- **Cost per accepted result** is reported for each arm. Exit codes: 0 SWITCH, 1 KEEP, 2 NOT YET, 3 UNHEALTHY.
|
|
207
|
+
|
|
208
|
+
**In production:** my triage agent moved from Claude Haiku 4.5 to Haiku 5.5 on a pre-registered decision: +2.2 pts
|
|
209
|
+
(95% CI -0.7 to +5.1) at 0.09x the cost per accepted result. An earlier comparison had said 10 points worse; the gap
|
|
210
|
+
came from labels that repeated counts the old model made up. Rule, rows and record:
|
|
211
|
+
[examples/switch-triage-haiku-5-5](https://github.com/sidreddy88/agentstats/tree/main/examples/switch-triage-haiku-5-5).
|
|
212
|
+
|
|
213
|
+
These two commands started as a separate package, stepdown. They're a thin layer on agentstats' own tests, so they
|
|
214
|
+
live here for now. If the model-switch side grows into its own system (suggesting candidate models, adapting the
|
|
215
|
+
harness with harness-evolve, deciding as runs accumulate, shadow runs on live traffic), it may become a separate
|
|
216
|
+
package again.
|
|
217
|
+
|
|
218
|
+
## Evals in CI, in tiers
|
|
219
|
+
|
|
220
|
+
`agentstats init-ci` writes a policy (`agentstats.toml`) and three GitHub workflows. You replace one step in
|
|
221
|
+
each, running your agent, and agentstats decides:
|
|
222
|
+
|
|
223
|
+
| Tier | When | What runs | Typical requirements |
|
|
224
|
+
|---|---|---|---|
|
|
225
|
+
| smoke | every pull request, minutes | a stratified sample (`agentstats sample`) + all rule checks | `rules`, `min_pass_rate` (a crash threshold) |
|
|
226
|
+
| nightly | every night | the whole eval set, k runs per case | `no_regression` vs the baseline, `max_infra` |
|
|
227
|
+
| release | before shipping | the whole set against the production baseline | `no_regression`, `max_cost_ratio`, `max_p95_latency_ratio`, `min_pass_hat_k` |
|
|
228
|
+
|
|
229
|
+
**Behaviour rules as a regression gate (no labels needed).** `min_rule_rate` requires every trajectory rule (the
|
|
230
|
+
`checks` in each row, e.g. `{"called_lookup_tool": true}`) to hold at least that often, judged on its interval: FAIL
|
|
231
|
+
only when the whole interval is below, INCONCLUSIVE when it can't tell. `no_rule_regression` compares each rule with
|
|
232
|
+
the baseline, case by case (fixed-cases test, Bonferroni across rules), and fails when a rule is proven to hold less
|
|
233
|
+
often. These catch what an accuracy gate can't when the labels are wrong: a triage agent that skipped a required
|
|
234
|
+
lookup tool half the time and invented the number passed every accuracy check, because the labels had been built
|
|
235
|
+
from its own answers. The gate prints a per-rule table either way.
|
|
236
|
+
|
|
237
|
+
Two more requirements, both from real misses: `max_no_answer` caps the share of attempts with no usable answer, and
|
|
238
|
+
`max_slice_drop` (with `slice_by = "<tag prefix>"` in the tier) fails when any slice, e.g. one error template, is
|
|
239
|
+
proven worse than the baseline by more than the limit. Slice intervals are Bonferroni-adjusted across slices, slices
|
|
240
|
+
whose estimate is past the limit but not proven are named as leads, and with `slice_by` set the per-slice table is
|
|
241
|
+
printed even without the requirement.
|
|
242
|
+
|
|
243
|
+
`agentstats gate --policy agentstats.toml --tier nightly --candidate results/ --baseline baseline/` exits
|
|
244
|
+
**0 pass, 1 fail, 2 inconclusive, 3 unhealthy** (too many infrastructure failures to judge the agent), and writes
|
|
245
|
+
its table to the GitHub job summary.
|
|
246
|
+
|
|
247
|
+
**Size a gate before trusting it:** `agentstats size --baseline baseline.jsonl --trials 2 --rule drop` simulates the
|
|
248
|
+
rule on your baseline's own per-case rates and prints how often it would pass or fail for a range of true drops, so you
|
|
249
|
+
can pick trials and margins (e.g. no change passes >= 90%, an 8-point drop fails >= 90%).
|
|
250
|
+
|
|
251
|
+
Comparisons with the baseline are **non-inferiority tests**. "The interval includes zero" doesn't show that
|
|
252
|
+
nothing regressed, so `no_regression = 0.02` passes only when the lower confidence bound of (candidate − baseline)
|
|
253
|
+
is at least −2 points. An under-powered run is INCONCLUSIVE, with an estimate of how many runs would settle it,
|
|
254
|
+
never PASS. Small smoke tiers say what they can't see: "with 20 cases and 1 run, this tier can only see drops
|
|
255
|
+
larger than about 40 points: it catches breakage, not regressions."
|
|
256
|
+
|
|
257
|
+
On the real runs above (with-diagnosis as the candidate, plain as the baseline):
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
## agentstats gate: nightly ⚠️ INCONCLUSIVE
|
|
261
|
+
|
|
262
|
+
| requirement | measured | threshold | result | detail |
|
|
263
|
+
| runs | 1 | >= 3 | ⚠️ INCONCLUSIVE | the nightly tier expects 3 complete runs |
|
|
264
|
+
| max_infra | 0.0% | <= 5.0% | ✅ PASS | |
|
|
265
|
+
| no_regression | +1.0 pts (CI -6.2 pts to +8.2 pts, mcnemar) | lower bound >= -2.0 pts | ⚠️ INCONCLUSIVE | about 6 runs per arm would settle it if the estimate holds |
|
|
266
|
+
| max_cost_ratio | 0.89x (CI 0.80x to 0.99x) | <= 1.10x | ✅ PASS | |
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Input
|
|
270
|
+
|
|
271
|
+
One row per attempt (`agentstats convert` writes this from Harbor jobs):
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{"arm": "baseline", "run": 1, "case": "django-11099", "passed": true, "infra_error": null,
|
|
275
|
+
"cost_usd": 0.12, "input_tokens": 41000, "output_tokens": 2100, "steps": 14, "tool_calls": 9,
|
|
276
|
+
"latency_s": 33.0, "tags": ["feature:search"], "checks": {"no_refund_before_lookup": true}}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A *run* is one full pass over the eval set. Several paths for one arm are several runs. Runs are compared on the
|
|
280
|
+
cases attempted in every run.
|
|
281
|
+
|
|
282
|
+
`no_answer` (optional, a reason such as `"step_limit"` or `"parse_error"`) marks an attempt where the agent produced
|
|
283
|
+
no usable answer at all. It counts as a failure, like a wrong answer, but is reported apart: a jump in no-answer
|
|
284
|
+
attempts usually means the harness or the agent loop broke, not that the agent's judgement got worse. Rows are
|
|
285
|
+
validated on load (the error names the file, line and field), and an optional `schema_version` (currently 1) lets a
|
|
286
|
+
newer format be refused instead of half-read. `harness-evolve export` and agent-platform's triage gate both write this
|
|
287
|
+
format; `tests/test_hardening.py` loads samples from each.
|
|
288
|
+
|
|
289
|
+
**From traces** (`agentstats convert-traces`): an OpenTelemetry OTLP JSON export (tool calls are spans with
|
|
290
|
+
`gen_ai.operation.name = "execute_tool"`, name in `gen_ai.tool.name`, arguments in `gen_ai.tool.call.arguments`;
|
|
291
|
+
tokens from `gen_ai.usage.*`) or a Langfuse export (traces with their observations; TOOL observations are tool
|
|
292
|
+
calls). One trace is one attempt; `--rules` fills its checks. Pass/fail comes from `--verdicts` (JSONL with
|
|
293
|
+
`trace_id`, `passed`, optional `case`, `run`, `infra_error`); without it the rows are **rules-only**: no pass rate, but
|
|
294
|
+
the rule gates work, which is what production traces without labels need.
|
|
295
|
+
|
|
296
|
+
Judge labels (`agentstats judge`), one row per verdict:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{"item": "conv-07", "rater": "human:alice", "verdict": "pass", "split": "heldout"}
|
|
300
|
+
{"item": "conv-07", "rater": "judge", "verdict": "fail", "run": 2}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Trajectory rules (`agentstats check JOB --rules rules.json --out results.jsonl`): see `examples/rules.json`.
|
|
304
|
+
A synthetic judge-labels file is in `examples/judge_labels.jsonl`.
|
|
305
|
+
|
|
306
|
+
## Methods and sources
|
|
307
|
+
|
|
308
|
+
- Error bars for evals, clustered standard errors: Miller 2024, [arXiv 2411.00640](https://arxiv.org/abs/2411.00640).
|
|
309
|
+
- Choosing the method by how the eval set was built, evals in CI tiers, judge alignment and consistency metrics:
|
|
310
|
+
Yesha Shah's "Agentic Evals" series (see [Credits](#credits)).
|
|
311
|
+
- pass@k: Chen et al. 2021, [arXiv 2107.03374](https://arxiv.org/abs/2107.03374). pass^k: τ-bench, Yao et al.,
|
|
312
|
+
[arXiv 2406.12045](https://arxiv.org/abs/2406.12045). Randomness in agentic evals: Bjarnason et al. 2026,
|
|
313
|
+
[arXiv 2602.07150](https://arxiv.org/abs/2602.07150).
|
|
314
|
+
- Judge self-inconsistency: Haldar & Hockenmaier 2025 (EMNLP Findings); Yagubyan 2026,
|
|
315
|
+
[arXiv 2606.13685](https://arxiv.org/abs/2606.13685).
|
|
316
|
+
- Pre-registration (fix the analysis before seeing the data): Nosek et al. 2018, "The preregistration revolution",
|
|
317
|
+
[PNAS](https://doi.org/10.1073/pnas.1708274114). The switch rule is a standard non-inferiority test (lower interval
|
|
318
|
+
bound above -margin) on the same fixed-cases interval the gates use.
|
|
319
|
+
- Cost per accepted result rather than cost per call: from Grep AI's "The Managed Agent Wars".
|
|
320
|
+
|
|
321
|
+
Every statistic is tested against scipy or statsmodels (`tests/test_stats.py`, `tests/test_judge.py`). The only
|
|
322
|
+
runtime dependency is scipy.
|
|
323
|
+
|
|
324
|
+
## Credits
|
|
325
|
+
|
|
326
|
+
The framing behind several features comes from Yesha Shah's **Agentic Evals** series:
|
|
327
|
+
[Part 1, "A Software Engineer's Guide"](https://yeshas93.substack.com/p/agentic-evals-a-software-engineers)
|
|
328
|
+
(2026-09-21); [Part 2, "Statistical Tools for Measuring Agent Performance"](https://yeshas93.substack.com/p/agentic-evals-statistical-tools-for)
|
|
329
|
+
(2026-09-30); [Part 3, "Introduction to Autoraters"](https://substack.com/home/post/p-220996535) (2026-10-19):
|
|
330
|
+
|
|
331
|
+
- choosing the statistics by how the eval set was built (benchmark vs generalised performance; curated, sampled and
|
|
332
|
+
correlated eval sets);
|
|
333
|
+
- pass@k and pass^k for agents, and reporting intervals rather than single runs;
|
|
334
|
+
- evals in CI in tiers (smoke, nightly, release), a stratified smoke sample, and rule-based checks on trajectories;
|
|
335
|
+
- the judge checks: strictness, leniency, Cohen's and Fleiss' κ, a held-out split, verdict flip rate and mean pairwise
|
|
336
|
+
disagreement.
|
|
337
|
+
|
|
338
|
+
The statistical methods themselves come from the papers listed under [Methods and sources](#methods-and-sources),
|
|
339
|
+
which the series also cites.
|
|
340
|
+
|
|
341
|
+
Original to agentstats:
|
|
342
|
+
|
|
343
|
+
- the fixed-cases test with its small-sample correction (from a production diagnosis regression gate), and the
|
|
344
|
+
run-wide shock check;
|
|
345
|
+
- the gate rules: `no_significant_drop` and non-inferiority, INCONCLUSIVE with a runs estimate, a rerun on a shock;
|
|
346
|
+
- gate sizing by simulation on your own baseline (`agentstats size`);
|
|
347
|
+
- **behaviour rules as a regression gate**: `no_rule_regression` compares each trajectory rule with the baseline, case
|
|
348
|
+
by case with intervals, and `min_rule_rate` sets an interval-based floor. They need no labels, which is what caught a
|
|
349
|
+
production bug an accuracy gate passed (see the production section);
|
|
350
|
+
- the **no-answer outcome** (a broken loop or unparseable output reported apart from wrong answers) and **per-slice
|
|
351
|
+
gates** with Bonferroni-adjusted intervals and named leads;
|
|
352
|
+
- **rules-only rows from traces**: OpenTelemetry (GenAI conventions) and Langfuse exports, so behaviour can be gated on
|
|
353
|
+
production traces that have no labels;
|
|
354
|
+
- **model-switch decisions** (`agentstats switch`): non-inferiority plus cost per accepted result, as-is and adapted
|
|
355
|
+
steps, an eval-set provenance warning, and **checked pre-registration** (committed before the results, inputs
|
|
356
|
+
hashed), with a decision record;
|
|
357
|
+
- **calibration tests** that fail if the statistics drift (e.g. the drop gate's false-alarm rate);
|
|
358
|
+
- reading Harbor jobs and ATIF trajectories, and the production results above.
|
|
359
|
+
|
|
360
|
+
UNHEALTHY on infrastructure failures and keeping them out of pass rates are also in eval-sprint.
|
|
361
|
+
|
|
362
|
+
## Related tools
|
|
363
|
+
|
|
364
|
+
- [evalstats](https://github.com/ianarawjo/evalstats) ([arXiv 2609.35815](https://arxiv.org/abs/2609.35815)):
|
|
365
|
+
judge-corrected tests (PPI) and methods tuned for small samples. Use it when your verdicts come from an LLM
|
|
366
|
+
judge with a small human-labelled set.
|
|
367
|
+
- [evalci](https://github.com/Shreyaskc/evalci) ([arXiv 2607.04429](https://arxiv.org/abs/2607.04429)): CIs,
|
|
368
|
+
paired tests, power analysis and multiple-comparison correction for per-item tables (lm-eval, HELM).
|
|
369
|
+
- [eval-sprint](https://github.com/renanpgonzalez/eval-sprint): an eval kit with deterministic and LLM judges, a
|
|
370
|
+
labelling UI, and a CI regression gate (exact McNemar on paired flips, errors kept out of pass rates, an
|
|
371
|
+
unhealthy-run exit code). Its judge calibration (blind expert labels, Horvitz-Thompson weights, Rogan-Gladen
|
|
372
|
+
correction) is more thorough than agentstats' judge check.
|
|
373
|
+
|
|
374
|
+
## Limitations
|
|
375
|
+
|
|
376
|
+
- Pass rates need pass/fail verdicts; numeric scores are used for efficiency metrics and judge consistency.
|
|
377
|
+
- Power analysis is by simulation (`agentstats size`) for the two gate rules; no closed-form sample-size planner.
|
|
378
|
+
- The judge check uses the judge's most common verdict per item; it does not correct downstream statistics for
|
|
379
|
+
judge error (evalstats does).
|
|
380
|
+
- `agentstats switch` decides on fixed batches: run both models, then decide. Deciding as runs accumulate
|
|
381
|
+
(anytime-valid intervals) isn't built. The margin is your judgement; agentstats makes you state it and checks you
|
|
382
|
+
stated it first. Cost is whatever your rows record (my runs: token counts at list prices, no cache discounts).
|
|
383
|
+
|
|
384
|
+
MIT licence.
|