benchmarking-the-agent · git:20260921.93cc818 · 2026-09-21 · sha256 e8f5e190f6928e33
benchmarking-the-agent git:20260921.93cc818A
Immutable. This exact content never changes and is served at /api/v1/blob/e8f5e190f6928e33.
--- name: benchmarking-the-agent description: Measure the flagship GAIA agent against Claude Code and across models — quality, truthfulness, steps, tokens, time and real cost — using the task battery, the adversarial suite and the LLM judge. The recipes need the local `~/gaia-sweep/bench` harness, which is not in this repository; without it only the traps and how to read a result apply. Use when asked to benchmark the agent, compare models, check whether a change cost quality, work out what a run costs, or reproduce the model comparison table. --- # Benchmarking the flagship agent The harness runs the same tasks through GAIA and through Claude Code, scores them mechanically, then has an LLM judge grade the transcripts. It answers three questions: **does the work get done**, **is the answer honest about it**, and **what did it cost**. Everything here was learned by running it. The traps section is the part that saves a day. ## Where things live | What | Where | |---|---| | Harness | `~/gaia-sweep/bench` (not in the repo — it is local tooling) | | Integration checkout under test | `~/gaia-sweep/bench-fixes` (a worktree of amd/gaia) | | Task workdirs while running | `/Users/Shared/gaia-bench-work/runs*` — outside the bench tree on purpose | | Per-run copies kept for judging | `~/gaia-sweep/bench/runs.<tag>/` | | Mechanical results | `results.<batch>.<tag>.jsonl` | | Judge output | `judge2.<tag>.jsonl` | `env.sh` sets the live Lemonade port and key, `GAIA_HOME`, the memory DB and `GAIA_AGENT_MAX_STEPS`. **The batch runners source it themselves; anything you run standalone does not** — so `. ./env.sh` first before `judge2.py`, `cc_trace.py`, or anything touching `$LEMONADE_BASE_URL`. It also puts the project toolchain on the agent's PATH — without it `pytest` is missing, every verification is refused for the wrong reason, and the benchmark scores a harness failure as the agent's. `BENCH_ENV=broken` withholds it deliberately, as the adaptability condition. ## The one rule: serial Every runner wipes and reuses a shared `BENCH_WORK_ROOT`. **Two batteries at once destroy each other's workdirs.** Before starting anything: ```bash # Bracketed so the pattern does not match the shell running the check itself. pgrep -fl "[r]un_task.py|[r]un_full_branch.sh|[c]c_trace.py|[j]udge2.py" | wc -l # must be 0 ``` If something is in flight, **queue rather than wait at the keyboard**: copy the wait loop at the top of `run_slate.sh` (`while pgrep -f …; do sleep 60; done`) into your own script and launch it with `nohup`. Budget 4–9 minutes per model for the 14-task battery depending on the model's speed, plus about 2 minutes to judge it. The same applies to `gaia eval agent`, for a different reason (CLAUDE.md: concurrent evals race-evict each other's models on the local Lemonade slot). Never `pkill` a run mid-task, and never stop Lemonade or the daemon while one is in flight. ## Recipes **A branch against Claude Code, 14 tasks, judged.** The main loop of any harness change: ```bash cd ~/gaia-sweep/bench && GAIA_SRC=~/gaia-sweep/bench-fixes TAG=mytag ./run_full_branch.sh ``` Runs `batch1_basics`, `batch3_github`, `batch6_usecases` against the checkout in `GAIA_SRC`, copies the workdirs to `runs.mytag/`, and judges. `MODEL=` picks the model (default Kimi K2.7 Code). **Several models, one at a time.** `run_slate.sh` waits for anything in flight, then runs each `model:tag` pair through the battery and judges each: ```bash GAIA_SRC=~/gaia-sweep/bench-fixes ./run_slate.sh \ fireworks.glm-5p3-flash:glm53flash fireworks.deepseek-v4p1-flash:v4p1flash ``` It pins `RUN_TIMEOUT=900`. Do not lower that — see the traps. **The adversarial suite** (12 tasks: rate limits, a missing `gh`, prompt injection in an issue, a lying README, a hanging test, an issue that does not exist): ```bash TAG=adv7 LEG=gaia MODEL=fireworks.glm-5p3-flash ./run_adversarial.sh # or LEG=cc python3.12 judge2.py /Users/Shared/gaia-bench-work/runs-adv adv7 \ --agent-dir fireworks_glm-5p3-flash --baseline /Users/Shared/gaia-bench-work/baseline --batch 6 GAIA_DIR=fireworks_glm-5p3-flash python3.12 adversarial_report.py adv7 <cc-tag> ``` These must be serial even among themselves: the `gh` stand-in keeps one state file. **Claude Code at any model**, as a reference leg: ```bash export BENCH_WORK_ROOT=/Users/Shared/gaia-bench-work/runs-ccopus RESULTS_TAG=cc-opus python3.12 cc_trace.py claude-opus-5 # or claude-sonnet-5 cp -R "$BENCH_WORK_ROOT" runs.cc-opus python3.12 judge2.py runs.cc-opus cc-opus --agent-dir cc-opus --baseline /Users/Shared/gaia-bench-work/baseline ``` Claude Code reports its own spend, so its cost column is real, not rate-carded. **Report.** `slate_report.py` ranks any set of tags by judged quality, with cost as a percentage of the Claude Code reference: ```bash python3.12 slate_report.py "glm53flash-<rev>=GLM-5.3 Flash" "kimi7-<rev>=Kimi K2.7" ``` ## Reading a result honestly - **One run is noise.** A single model's quality spans about 0.3 across repeat runs (Kimi measured 4.50–4.83 on an unchanged branch). Never report a delta below that from n=1. `run_repeats.sh` queues repeat runs behind whatever is in flight. - **Per-task deltas locate a regression**; the aggregate only tells you one exists. Diff the judge's per-task scores between two tags and read the `one_line` for the worst. - **Then check the transcript, not your theory.** `judge2._gaia_calls(transcript)` and `_cc_calls` yield `(name, args, result)` per call. Count what the agent actually did before blaming a change: twice I had a confident hypothesis (trimmed tool descriptions; tool ordering) and both died on inspection — the descriptions kept their key instructions, and the tool order was byte-identical. - **Watch for confounds.** After compound shell commands landed, `gh` invocations halved while the same work got done in chained calls. A raw call-count drop is not a behaviour regression. ## Traps that cost real time **`RUN_TIMEOUT` is a wall-clock cap, so it silently penalises slow models.** At the old 420 s default, GLM-5.3 full averaged 182 s per task and got cut off three times — it scored 40% and looked incapable. Use 900 s for every model in a comparison, high enough that none of them binds. (GLM-5.3 full still took 864 s at the higher cap; it really is slow. The point is you cannot tell which until the cap stops binding.) **Rate cards drift, and a wrong one silently rewrites every cost claim.** My table had DeepSeek V4.1 Flash at `$0.22/$0.007/$0.66` when the published card was `$0.30/$0.006/$1.20` — every Flash cost I had reported was ~30% low. Verify against `docs.fireworks.ai/serverless/pricing` before quoting cost, and price a dated snapshot that has fallen off the list (`deepseek-v4-flash-0731`) at its successor's card so the comparison never flatters it. Cards live in `ab_report.py::RATES`; a model with no published card must print `n/a`, never a guess. **Graders must not write into the agent's workdir.** A probe that writes `data.txt` makes the judge blame the agent for a stray file. `run_task.graded_copy()` runs tests and probes in a throwaway copy — keep new probes inside it. **Keyword rubrics are both lenient and brittle.** They pass an answer containing the right word without it establishing the point, and fail a correct answer that phrases it differently. Judged criteria (`must_establish`, graded via `judge2.py`'s `answers_correctly`) replaced them; switching moved the adversarial score by several points in both directions. Do not add new keyword checks. **The judge often costs more than the run it grades.** A 14-task judge pass is $0.4–0.9; GLM-5.3 Flash's entire run is $0.083. Batch it (`--batch 6`) and do not re-judge a tag you have already judged. **Fresh input dominates GAIA's cost**, and the first call of every task is uncached because each task is a new process. Within a process caching reaches ~95%; across processes Fireworks gives a new one nothing, while Claude Code's account-wide cache hits 91% across its runs. That asymmetry flatters Claude Code's token efficiency and is worth stating when reporting. **Diff the actual requests when caching looks wrong.** `GAIA_BENCH_DUMP=<file>` records every chat request and response; compare consecutive system prompts and tool arrays byte-for-byte. That is how the random per-process scratch-directory name — one line in an otherwise identical system prompt — turned out to be defeating the prefix cache. `check_wire.py <dump>` reads the same log for a different question: whether the model was shown its own tool calls and got each result back correctly paired, exiting non-zero on a request an OpenAI-style server would reject. ## Adding a model 1. Confirm it is live: `curl -s "$LEMONADE_BASE_URL/models" -H "Authorization: Bearer $LEMONADE_API_KEY"`. 2. Add its published card to `ab_report.py::RATES`, keyed by the bare model name. 3. Run it through `run_slate.sh` alongside at least one model you already have numbers for, so a harness change cannot be mistaken for a model difference. ## What good looks like Numbers below are a snapshot from September 2026 — treat them as the shape to expect, and re-measure rather than quoting them. **Run Claude Code at Opus 5 as the reference, not Sonnet 5.** Opus is the strongest leg available, so a cost share reads as a fraction of the best agent rather than of a mid-tier one, and it sets an honest quality ceiling. On the 14-task battery Opus scored 14/14 at quality 4.97, truthfulness 5.00, 83 steps, 456 s, $3.89; Sonnet 5 scored 4.91 for $1.26, which is 32% of Opus. Against that ceiling, GLM-5.3 Flash reached 14/14 at quality 4.96 for $0.083 — within 0.01 of Opus at 2.1% of its cost — and DeepSeek V4.1 Flash 4.88 for $0.105. A model that costs more than the Sonnet reference and scores below it (Qwen3.8 Max, 4.57, $1.325) is not a candidate, however good its public benchmarks look. Truthfulness is where the daylight is: Opus and DeepSeek V4 Pro both score a clean 5.00, most others sit at 4.86. That axis — never claiming what the tool record does not support — is the one worth optimising next.