Source Code:
src/gaia/eval/release_scorecard.py (core generator) ·
src/gaia/eval/scorecard_gate.py (release gate)Distinct from src/gaia/eval/scorecard.py — that file is the per-run scenario PASS/FAIL aggregator used internally by gaia eval agent. This document describes the outward-facing release artifact.Overview
Each published hub agent ships a release scorecard — a singleSCORECARD.md file (updated in place per release, versioned via the publish snapshot, the same way README.md works) that records:
- The eval recipe: dataset reference, methodology, configuration, and metric definitions.
- The measured results: per-metric values, number of test cases actually run, and dataset size.
- A single named aggregate score: a deterministic, recomputable percentage so a reviewer can verify the number without re-running the eval.
- A Reproduction section: the exact commands to reproduce the result from scratch.
scorecard_gate.py) blocks packaging when the scorecard is missing, when its aggregate score regresses below the prior version’s, or — when enforcement is on — when it is below an absolute bar or an anti-gaming floor.
Acceptance metric — email triage (#1437)
The email-triage scorecard’s aggregate is within-one-bucket acceptance accuracy, not exact 4-way match. Triage priority is an ordinal scale, so the user-facing question is “how far off”, not “exact match”:|rank(pred) − rank(expected)| ≤ 1) — what a user feels (nothing urgent buried in low-priority). PERSONAL is not a priority level and is absent from the corpus, so it is scored exact-only; re-place it on the scale if the corpus gains PERSONAL examples. Exact 4-way agreement (~40% on a 4B on-device model) is below the realistic ceiling — single-rater human agreement on subjective priority is only ~60–75% — so the 80% bar (#1437) is on this acceptance metric, with these reported (non-gating) secondaries:
urgent_vs_not_accuracy— binary accuracy on the needs-attention axis ({URGENT, NEEDS_RESPONSE}vs rest).urgent_recall— recall on that axis; the input to the gate’s anti-gaming URGENT floor.category_accuracy— exact 4-way match, kept as a reference.
aggregate.value (which stays round(100 × within_one_bucket_accuracy, 2), recomputable from the displayed components per the formula below).
Trustworthy numbers — variance & CI (#1894)
The corpus + greedy decoding (temperature=0.0) are fixed, and the needs_llm routing is a deterministic heuristic, so the only run-to-run noise is GPU floating-point non-determinism (not seedable — measured, not removed). gaia eval benchmark --experiments N runs N times over the same corpus and the adapter records an additive acceptance_variance block (mean / stdev / CV% / 95% CI / n_runs) in recipe.config. It never affects aggregate.value (the mean); it lets the gate tell a real regression from noise.
File format
Scorecard files are Markdown with YAML front matter (.md). The front matter holds all machine-readable fields; the body is a human-readable summary with a worked recomputation and a Reproduction section.
Required fields
A scorecard missing any of these is invalid and will be rejected by the release gate:Optional fields
These blocks are optional and additive — a scorecard is valid with or without them, and none of them ever affectsaggregate.value. They add reviewer-facing detail about how the number was produced and where the misses are:
Token accounting rows (
results.performance, issue #1891) — real LLM token counts including the nested per-email classify calls the outer-turn stats used to miss:
tokens_per_triage is reported, not gated: do not add a release-gate bar for it without a committed, ctx-stamped baseline scorecard to compare against (see _tokens_per_triage_comment in tests/fixtures/email/quality_gate_thresholds.json).ResultPayload.environment / ResultPayload.breakdown when your harness exposes the data, and omit them otherwise.
Two counts — defined distinctly
recipe.dataset.size and results.test_cases_run are intentionally separate fields:
recipe.dataset.size— total labeled examples available in the dataset (fixed for a given dataset version).results.test_cases_run— the subset actually executed in this run (may be limited by--limit). Must be ≤recipe.dataset.size.
Aggregate score formula
valueᵢ is a metric value in [0, 1] and each weightᵢ defaults to 1.0.
The result is a percentage in [0, 100]. For a single metric with weight 1.0:
aggregate.components alone — no eval-harness access needed.
The aggregate.formula field in the front matter states the formula in human-readable form so it is self-documenting.
Storage convention
Each agent package ships a singleSCORECARD.md file, updated in place per release — the same way README.md works. Per-version uniqueness comes from the publish snapshot (R2 stores the file at agents/<id>/<version>/SCORECARD.md; the npm package ships only the current version’s SCORECARD.md).
doc-root is the location of the agent’s canonical README:
The relative link
./SCORECARD.md resolves both in-repo and when the directory is published as an npm package. The npm files array includes SCORECARD.md (not a scorecards/ directory).
Versioning policy
Patch releases — carry forward
For a patch release (samemajor.minor, patch incremented), the prior version’s results are carried forward verbatim using carry_forward(). Pass the path to the agent’s current SCORECARD.md:
inherited_from: "0.2.4" and identical results and aggregate fields. The aggregate score is unchanged, so the release gate’s equal-score case passes.
carry_forward() reads the prior version from the agent.version field in the front matter — not from the filename.
Minor / major releases — re-run required
For a minor or major bump,carry_forward() raises ValueError with a “re-run” message. Run the eval fresh and generate a new scorecard:
hub/agents/npm/agent-email/SCORECARD.md in place.
Release gate
scorecard_gate.py is a standalone script that exits non-zero on failure:
--baseline-file and --baseline-ref are mutually exclusive. If the file doesn’t exist at the given ref, the gate treats it as first adoption (presence-only pass).
Gate logic
- Presence check:
--scorecardpath must exist and be a valid scorecard. → exit 1 if not. - Absolute bar + anti-gaming floor (opt-in, applies to every path including first adoption):
--min-aggregate <N>: exit 1 ifaggregate.value < N(the #1437 80% bar is--min-aggregate 80).--min-urgent-recall <r>: exit 1 if the card’surgent_recallsecondary is belowr(a high aggregate must not come with buried urgent mail). Fails loud if the metric is absent.- Omit both for report mode (presence + regression only). The email release workflow reads these from the thresholds manifest (
acceptance_target,urgent_recall_floor,acceptance_enforce) — data, not code.
- Baseline resolution:
--baseline-file: read the given file directly (no git access; suitable for unit tests).--baseline-ref: resolve viagit show <ref>:<scorecard-path>. If the file does not exist at that ref → first adoption, exit 0.- Neither specified: first adoption, exit 0 (presence-only pass).
- Regression check (variance-aware, #1894): if the baseline records a within-one stdev, a regression is flagged only when
candidate < baseline − k·stdev(--regression-k, default 1; stdev is on the [0,1] scale, scaled ×100 to matchaggregate.value). With no recorded stdev, a strict<is used. - Equal or above the threshold → exit 0.
Exit codes
--allow-regression
When a regression is intentional (e.g. a dataset correction or methodology change), use --allow-regression. The gate prints a GHA ::warning:: annotation naming both versions and scores, then exits 0:
Keeping the scorecard current (the update / reject loop)
The scorecard must move with the agent: when LLM-affecting code changes, the eval is re-run and the committedSCORECARD.md refreshed — upward. A regression is blocked.
Two enforcement points work together:
-
Reject-on-worse (always on, GitHub-hosted). The
scorecard-gatejob inrelease_agent_<id>.ymlruns on every release. It only parses committed files (no eval), so it runs on a standard runner and fails the build if the committed scorecard regressed below the prior version or is missing. This is the hard gate. -
Run-and-refresh (self-hosted AMD, manual dispatch).
gaia eval benchmarkneeds Lemonade on AMD hardware, so it cannot run on GitHub-hosted runners — it runs on the[self-hosted, Windows, stx]pool, serialized against the other evals by the sharedlemonade-evalconcurrency group. TheEmail Agent Eval — scorecard refreshworkflow (.github/workflows/email_scorecard_refresh.yml) isworkflow_dispatch-only (#2094 — it previously also ran on pushes touching the email agent, but a full eval costs ~10.5h and that trigger never once completed a run), and takes two profiles:- full (
limit ≥ 249andexperiments ≥the committed card’sn_runs) — reproduces the committed card’s methodology and is the only profile allowed to commit a refreshed card; - subset (
limit < 249) — an end-to-end smoke run that exercises every step and then never commits (a small-sample number is not a valid stand-in for the published figure).
tests/fixtures/email/quality_gate_thresholds.json). Before any eval spend, the run also fails fast if its basis (ctx_size,experiments) differs from the committed card’s — unlessrebaseline=true(with a requiredrebaseline_reason) marks the basis change deliberate. - full (
gen_scorecard.py + scorecard_gate.py reproduce both steps (see the adding-eval-scorecard skill).
Adding a scorecard for a new agent
- Write a
packaging/gen_scorecard.pyadapter (seehub/agents/python/email/packaging/gen_scorecard.pyfor a reference). The adapter should populatereproduction_commandwith the exact commands needed to reproduce the scorecard. - Run the eval and call the adapter → commit the resulting
SCORECARD.mdto<doc-root>/SCORECARD.md. - Link the scorecard from the README:
./SCORECARD.md. - Add
SCORECARD.mdto the npmpackage.jsonfilesarray (if published on npm); do not add ascorecards/directory. - Wire
scorecard_gateinto the release workflow (seerelease_agent_email.ymlfor the job topology). Use--scorecard <path>/SCORECARD.mdand--baseline-ref <prev-tag>(best-effort).