Conversation
`build_decider` legality-gates every slot output and catches every slot exception, substituting a legal fallback so one bad state cannot kill a 240-seed battery. That robustness is correct and stays. Its invisibility does not: nothing a reader looks at distinguishes a result produced by an agent from one produced by the harness standing in for it. Reproduced by writing a contributed agent from docs/adding-an-agent.md and making one ordinary mistake -- omitting the imports its shop policy needs, which src/bench/agents.py does not already have. The policy then raised NameError on all 40 of its shop decisions. Every step of the advertised loop reported success: evaluate.py printed `0 fails` (a "fail" is a crashed game, not a dead policy), the artifact summary had no field for it, inspect_run.py -- the tool whose job is "inspect failures" -- rendered a normal-looking timeline, and the paired comparison published -1.750 ante [-2.500, -0.875], an interval excluding zero, from code that never executed once. The truth existed only in events[].reasoning in the raw JSONL. Fallback stays; the silence goes. - metrics.fallback_stats(runs): counts substitutions, runs affected, and a by-reason breakdown, isolating `fallback-error:*` (the policy raised -- always an agent defect) from `fallback-illegal` and the benign `fallback-phase`. - summary.fallback in the result artifact, via metrics.summarize. Additive, so v1 consumers are unaffected; no existing field changes. - evaluate.py prints each arm's substitution rate on every run, not only bad ones, and warns explicitly when any arm's policy raised. - inspect_run.py labels every decision with its recorded reasoning, marks substituted ones `!! FALLBACK`, banners the per-run rate, and flags affected runs in --list. - docs: the swallow and how to check it, the import gap that triggered this, a fallback-rate reporting requirement for submissions, the artifact field, and a known-limits entry for the residual (substitution is now surfaced, not prevented). Verified: no published number moves. Re-running the broken agent yields byte-identical artifacts for both arms outside the new block, and the same -1.750 [-2.500, -0.875] -- now under an unmissable warning. The fixed agent reports 0/328 substitutions and +0.000 [+0.000, +0.000]. ruff clean; 79 tests pass, including new coverage of the broken-agent case in all three surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing in favour of #6, which fixes the underlying defect this PR set out to surface. The failure this PR found is real and it reproduces: a genuinely broken agent (missing The counter it added cannot see it, though. #6 addresses it from the other end: The Not closed for being wrong about the problem. Reopen the visibility question against #6's ground if the residual gap needs its own instrument. |
The defect
Found by cold-cloning this repo and running the documented loop as an outsider: write a
contributed agent following
docs/adding-an-agent.md(priciest-shop— buys the priciestaffordable Joker instead of the cheapest), then compare it. I made one ordinary mistake — I did
not add the imports its shop policy needs, because
src/bench/agents.pydoes not importFactoredAction/get_fallback_action/ the action constants, and the guide never mentions it.The policy raised
NameErroron all 40 of its shop decisions. Every step of the advertisedloop reported success:
evaluate.pysays16/16 games ... 0 fails16/16 games ... 0 failsgreedy-shopfallback-error:NameError)The broken run publishes a statistically significant finding from code that never executed once.
build_deciderlegality-gates every slot output and wraps every slot call inexcept Exception,substituting a legal fallback. That is correct for battery robustness — one bad state must not kill
240 seeds — and this PR does not change it. The problem is that the substitution is invisible
everywhere a human looks:
evaluate.pyprints0 fails— a "fail" is a crashed game, not a dead policy.summaryhad no field for it (run_depth,blind_stats,win_rate,advance_curveonly).scripts/inspect_run.py— the tool whose entire job is step 5, "inspect failures" — nevermentioned
reasoningorwas_fallback. Its timeline for the broken agent looked completelynormal.
The truth existed only in
events[].reasoningin the raw JSONL, which you have to already know tolook for. A missing import is the mild version; the dangerous version is an
AttributeErrororIndexErroron a rare state, which silently degrades part of a run and shifts a headline number byan unknowable amount that no artifact records.
For a product whose one purpose is trustworthy paired comparison, this is the worst-shaped defect
available — it doesn't break, it lies.
The fix — fallback stays, its silence goes
metrics.fallback_stats(runs)— counts substitutions, runs affected, and a by-reasonbreakdown, isolating
fallback-error:*(the policy raised — always an agent defect) fromfallback-illegaland the benignfallback-phase.summary.fallbackin the result artifact viametrics.summarize. Additive; no existingfield changes and
jackhammer.result/v1consumers are unaffected.evaluate.pyprints each arm's substitution rate on every run, not only bad ones, andwarns explicitly when any arm's policy raised.
inspect_run.pylabels every decision with its recorded reasoning, marks substituted ones!! FALLBACK, banners the per-run rate, and flags affected runs in--list.reporting requirement for submissions (
CONTRIBUTING.md), the artifact field(
docs/result-artifacts.md), and aknown-limits.mdentry for the residual: substitution is nowsurfaced, not prevented, and a partial rate still shifts a number by an amount the artifact
records but does not correct for.
The same broken run now reads:
and
scripts/inspect_run.py --seed PVRQ4K5A:Verification
byte-identical for both arms outside the new
fallbackblock, and the same-1.750 [-2.500, -0.875]— now under the warning above.0 / 328substitutions,+0.000 [+0.000, +0.000].fallback substitutions: 0 / 345 decisions— a clean battery isgenuinely 0.0%, so any nonzero rate is signal.
uv run ruff check src scripts tests— clean.uv run pytest— 79 passed, including new coverage of the broken-agent case in all threesurfaces (
tests/test_playground_metrics.py,tests/test_inspect_run.py, newtests/test_evaluate.py).docs/adding-an-agent.mdwas executed verbatim to confirmevery name resolves.
Open question for the maintainer
I deliberately did not touch
docs/protocol-v1.md's reporting checklist. Adding "fallbackrate" there is arguably a change to the evaluation procedure, which the freeze says mints v2. The
requirement currently lives in
CONTRIBUTING.mdinstead. If you'd rather it be a protocol-levelrequirement, that's a v1.1/v2 call, not mine.
The
priciest-shopagent was only the reproduction vehicle and is not included here.🤖 Generated with Claude Code