feat(eval): add jobbench harness and correct token accounting - #125
Merged
David Koleczek (DavidKoleczek) merged 1 commit intoAug 17, 2026
Merged
Conversation
Two related changes to the evaluation harnesses.
## Token accounting (deep-swe)
total_tokens was computed as input + output, which dropped cache reads
entirely. Because the four agent arms have very different cache behavior,
this did not just understate a number, it inverted the ordering: on one
real run an opencode trial reported 95,147 tokens against an amplifier
trial's 1,218,757, an apparent 12x gap, when opencode had actually
processed ~19.6M tokens against amplifier's ~10.2M. The old figure
discarded opencode's entire 19.3M cache-read volume.
total_tokens is now the sum of four disjoint fields:
input + cache_read + cache_write + output
For that sum to be valid, input_tokens is normalized to fresh-only on
both sources. opencode's tokens_input column is already fresh-only and is
accumulated as-is; the amplifier stacks fold cache_read into their
reported input_tokens, so parse_events subtracts it back out.
When input < cache_read the convention has broken. Rather than emit a
silently wrong number, fresh input is clamped to 0, the occurrence is
counted, and the run notes record that input_tokens is a FLOOR and
total_tokens may undercount.
cost_usd is unaffected. It is priced from raw per-source counts against
MODEL_RATES_PER_M and was never derived from total_tokens.
Five tests cover the new behavior: opencode fresh-only accumulation, cost
invariance under the normalization, events-branch cache_read subtraction,
cache_write > input not going negative, and the clamp-and-flag path.
The OpenCode install is also unpinned. The benchmark measures opencode as
it ships today; pinning a version freezes the control arm against a
release that is no longer what users get. The retry loop around the
install endpoint is retained, since that flakiness is unrelated.
## jobbench harness
A new harness running the same four agent arms against JobBench, a
multi-source knowledge-work benchmark: reconciling contradictory records,
cross-referencing, and citation tracing across 35 white-collar
occupations. This is not a coding benchmark, which is what makes it worth
running alongside deep-swe rather than instead of it.
Each task hands the agent a folder of source files and expects
deliverables (xlsx, docx, pdf, ipynb, sqlite, pptx). An LLM judge scores
them against a weighted, criterion-level rubric, all-or-nothing per
rubric. Two splits: main (65 tasks, 569 rubrics) withholds reference
material the agent must find via live web search; easy (63 tasks) is
self-contained.
Structure:
run.py six subcommands: fetch, list-tasks, dtu-check,
bake, run, grade
src/jobbench/ dataset fetch, DTU lifecycle, golden image
bake, matrix, scheduler, trial, grading, judge
profiles/ a base image plus one layer per agent arm
tests/ 127 tests, all synthetic fixtures
Golden Incus images are baked per arm because cold-provisioning
LibreOffice and the analysis stack for every trial is untenable at 260
trials; a warm launch is ~15s. No provider, model, or API key is baked
into any image layer. Credentials are injected at trial launch through
DTU passthrough.
grading.py invokes the judge as a module subprocess rather than through
upstream's run_judge.sh, because that script silently skips an empty
output directory and writes no result file, making a crash
indistinguishable from a task that never ran.
metrics.py is vendored byte-identical from deep-swe. The token
normalization above was developed here and ported back.
Known issues that bound what the numbers mean are documented in the
README: tool results are lost on the opencode-amplifier arm (root cause
outside this harness, surfaced as a warning in trial.json), that same arm
reports token and cost fields as not_available by construction, judge
token usage is captured but never priced, and scores are not comparable
to the published leaderboard because the rubrics were validated by the
authors against a different judge model.
judge.py and prompt.py are derived from the upstream benchmark under
Apache-2.0, with modifications enumerated in their module docstrings and
full attribution in THIRD-PARTY-NOTICES.md. They are kept in-tree rather
than under vendor/ so local adaptations stay diffable against upstream,
and are excluded from lint and format for the same reason. The dataset
itself is fetched at runtime, not redistributed.
Tests: deep-swe 27 passed, jobbench 127 passed.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two related changes to the evaluation harnesses. Scope of impact: evaluation-only — nothing under
src/,wrappers/, orprotocol/is touched, so no engine, wrapper, or protocol release is implied.Token accounting (deep-swe)
total_tokenswas computed asinput + output, which dropped cache reads entirely. Because the four agent arms have very different cache behavior, this did not just understate a number — it inverted the ordering. On one real run an opencode trial reported 95,147 tokens against an amplifier trial's 1,218,757, an apparent 12x gap, when opencode had actually processed ~19.6M tokens against amplifier's ~10.2M. The old figure discarded opencode's entire 19.3M cache-read volume.total_tokensis now the sum of four disjoint fields:For that sum to be valid,
input_tokensis normalized to fresh-only on both sources. opencode'stokens_inputcolumn is already fresh-only and is accumulated as-is; the amplifier stacks foldcache_readinto their reportedinput_tokens, soparse_eventssubtracts it back out.When
input < cache_readthe convention has broken. Rather than emit a silently wrong number, fresh input is clamped to 0, the occurrence is counted, and the run notes record thatinput_tokensis a FLOOR andtotal_tokensmay undercount.cost_usdis unaffected — it is priced from raw per-source counts againstMODEL_RATES_PER_Mand was never derived fromtotal_tokens.Any prior cross-arm token comparison from this harness should be treated as invalid.
The OpenCode install is also unpinned in the same change. The benchmark measures opencode as it ships today; pinning a version freezes the control arm against a release that is no longer what users get. The retry loop around the install endpoint is retained, since that flakiness is unrelated.
jobbench harness
A new harness running the same four agent arms against JobBench, a multi-source knowledge-work benchmark: reconciling contradictory records, cross-referencing, and citation tracing across 35 white-collar occupations. It is not a coding benchmark, which is what makes it worth running alongside deep-swe rather than instead of it.
Each task hands the agent a folder of source files and expects deliverables (xlsx, docx, pdf, ipynb, sqlite, pptx). An LLM judge scores them against a weighted, criterion-level rubric, all-or-nothing per rubric. Two splits:
main(65 tasks, 569 rubrics) withholds reference material the agent must find via live web search;easy(63 tasks) is self-contained.Golden Incus images are baked per arm because cold-provisioning LibreOffice and the analysis stack for every trial is untenable at 260 trials; a warm launch is ~15s. No provider, model, or API key is baked into any image layer — credentials are injected at trial launch through DTU passthrough.
grading.pyinvokes the judge as a module subprocess rather than through upstream'srun_judge.sh, because that script silently skips an empty output directory and writes no result file, making a crash indistinguishable from a task that never ran.metrics.pyis vendored byte-identical from deep-swe. The token normalization above was developed here and ported back.Known issues that bound what the numbers mean
Documented in
jobbench/README.md, surfaced here because they limit how the output should be read:opencode-amplifierarm (22–27 occurrences/run, ~2x wall clock, trial still exits 0). Root cause is outside this harness; flagged viawarningsintrial.json.not_available, by construction.opencode-vanilla'scost_usdis recomputed from this harness's rate card, not opencode's self-reported figure.Licensing
judge.pyandprompt.pyare derived from the upstream benchmark under Apache-2.0, with modifications enumerated in their module docstrings and full attribution inTHIRD-PARTY-NOTICES.md. They are kept in-tree rather than undervendor/so local adaptations stay diffable against upstream, and are excluded from lint and format for that reason. The dataset itself is fetched at runtime, not redistributed.Verification
make verify-paritycould not be run to completion locally — the TypeScript conformance runner crashes on a missingesbuildinnode_modules, a local environment gap unrelated to this change. CI runs that gate, and nothing here touchessrc/,wrappers/, orprotocol/.No CHANGELOG entry: this is harness-only and not user-visible in any shipped artifact.