feat(bench): agent-effectiveness benchmark pilot, A0 vs A2 on S1/S3 with seeded ground truth and audit-derived metrics#950
Merged
Conversation
…ith seeded ground truth (#942) bench/ is a separate Go module (test/load convention) measuring whether an agent connected to the platform answers real data questions more correctly and efficiently than the same agent on bare data tools. Arms are config profiles, not code forks: a0 (raw trino_*/s3_*, no enrichment, no search, gate off) vs a2 (the shipped semantic-first platform). Runs hold the model, prompt scaffold, seed data, and task set constant. A fixed-seed generator (bench/seedgen) emits the Trino warehouse, DataHub metadata, knowledge pages, the 10-task pilot set (5 S1 discovery, 5 S3 knowledge traps across units_cents and net_revenue trap classes), and a scripted smoke; ground truths are computed from the generated rows and a determinism test pins the committed artifacts. The platform audit log is the measurement instrument: the harness mints the dps_ handle via platform_info, hides the session_id plumbing from the model, threads the handle itself, and reads efficiency metrics from the admin audit API (delivery: sync) under a min/max accounting contract that fails loudly on missing rows. Each attempt authenticates from a per-arm identity pool so the user-keyed search-first gate stays independent across attempts, and bench-run resets persisted discovery state between runs. Model adapters are pluggable: anthropic (pinned model, no sampling params; k-repeats and pass^k carry variance) and scripted (plays each task's reference SQL through the live platform, validating seed data, ground truths, handle threading, audit read-back, and both graders with no API key). Deterministic graders score the FINAL ANSWER line only: numeric prefers the first decimal-bearing number; entity requires a correct alias and none of the task's generated wrong_aliases. Harness-level failures are excluded from accuracy and pass^k and reported separately. Lifecycle: make bench-up (stale-instance and port guards), bench-smoke, bench-run, bench-report, bench-down, bench-gen, bench-test; bench.yml runs the module checks in CI. Deliberately not part of make verify, like load and mutation testing. Phase 1 of #930; suites S2/S4/S5, the LLM judge, published numbers, and the regression gate follow in #943-#945.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #950 +/- ##
==========================================
- Coverage 89.04% 89.03% -0.02%
==========================================
Files 424 424
Lines 48581 48581
==========================================
- Hits 43260 43255 -5
- Misses 3547 3550 +3
- Partials 1774 1776 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Closes #942 (phase 1 of #930).
What this adds
bench/is the agent-effectiveness benchmark harness: it measures whether an agent connected to this platform answers real data questions more correctly and more efficiently than the same agent connected to bare data tools. It ablates the PLATFORM, not the model: every run holds the model, prompt scaffold, seed data, and task set constant and varies only the platform configuration. It is the "how well" sibling of thetest/load"how much" harness and follows the same separate-Go-module convention (excluded from the root./...gates, own.golangci.yml, checks viamake bench-test), and like load and mutation testing it is deliberately not part ofmake verify.Arms as config profiles
The ablation mechanism is the platform's own config surface (
bench/config/), not code forks:a0trino_*,s3_*); no semantic provider, no search tool, all cross-enrichment off, search-first gate off (the gate is not persona-aware, so a persona without a discovery tool would deadlock on SEARCH_REQUIRED). Equivalent to wiring the standalone toolkit libraries.a2searchtool, the search-first gate, seeded metadata and knowledge pages.Each profile defines a 32-key identity pool alongside the base admin key: the search-first gate keys discovery on the authenticated USER, so every task attempt authenticates as its own pool identity, and a run refuses to start when tasks x k exceeds the pool.
make bench-runalso resets the persistedsearch_gate_discoverystate so repeated runs start clean. Armsa1(enrichment-only) anda3(memory lifecycle) join in phases 2 and 3.Seeded ground truth
bench/seedgengenerates every artifact from one fixed-seed dataset model (seed 930): Trino DDL/DML (memory catalog), DataHub metadata proposals (descriptions, column units, deprecation), knowledge-page SQL, the task YAML, and the smoke script. Ground truths are computed from the generated rows, never hand-typed, and each numeric task records the reference SQL it mirrors. A determinism test (TestCommittedArtifactsMatch) fails when the committed artifacts drift from regeneration (make bench-genrefreshes them).The pilot task set is 10 tasks: 5 S1 discovery tasks (entity-graded) and 5 S3 knowledge traps across two seeded trap classes:
units_cents: monetary columns are integers in US cents; the fact lives only in the metadata layer (column and dataset descriptions).net_revenue: policy revenue = amount - discount over completed orders only; the fact lives in a knowledge page and dataset description, and the gross-revenue leader and the policy net-revenue leader differ by construction (the generator asserts it).Rubric items (required caveats) are recorded into per-attempt transcripts for manual review; LLM-judged scoring is phase 2 (#943).
Measurement through the platform's audit log
The harness mints the
dps_session handle viaplatform_info, strips the injectedsession_idproperty from every tool schema shown to the model, and threads the handle itself: measurement plumbing invisible to the agent and uniform across arms. Efficiency metrics come fromGET /api/v1/admin/audit/events?session_id=...withaudit.delivery: sync. A run fails loudly when a session's rows fall outside the client-side accounting bounds: confirmed calls must all have rows; platform refusals (authz, the gates, the per-user limiter) short-circuit outer to the audit middleware and have none; transport-level failures count as indeterminate (the platform may have audited before the error surfaced). Harness-level failures are excluded from accuracy and pass^k and reported separately, so an infrastructure hiccup can never masquerade as a platform quality signal.Model adapters
The LLM adapter is provider-pluggable:
anthropic(official Go SDK): real runs, model pinned in the run manifest, no sampling parameters; k-repeats and pass^k carry run-to-run variance.scripted: plays a deterministic script in which SQL-backed tasks run their reference SQL through the livetrino_queryand answer with the live result. One smoke run therefore validates the seed data, stored ground truths, handle threading, audit read-back, and both graders against the real platform, with no API key and no model variance.claude -padapter for subscription-funded runs is filed as bench: claude-cli adapter — run benchmark episodes via claude -p (subscription, no API key) #949.Grading is deterministic over the FINAL ANSWER line only: numeric answers prefer the first decimal-bearing number (a restated year is a bare integer and is skipped when a decimal candidate exists); entity answers must name a correct alias and none of the task's generated
wrong_aliasestrap answers.Lifecycle and CI
make bench-up BENCH_ARM=a0|a2(compose stack + seeded warehouse + release platform build, with stale-instance and port-collision guards),make bench-smoke,make bench-run,make bench-report,make bench-down,make bench-gen,make bench-test. Fora2,make bench-seed-datahubpushes the generated metadata into a DataHub quickstart (same external convention as e2e and load). Abench.ymlworkflow runs the module checks (build/vet/test/lint) whenbench/changes; the benchmark smoke workflow and published results page are phase 4 (#945).Verification
make verifygreen; root module runtime unchanged (bench is a nested module outside the root./...gates by design).go test ./...(unit tests plus an integration test that assembles a real streamable-HTTP MCP server and the admin audit surface in-process and drives the full pipeline through genuine protocol wiring, including identity-pool rotation and the pool-refusal path),golangci-lintwith the same complexity ceilings as the root.a0arm against the compose stack: all 10 tasks pass with the scripted adapter (sessions minted, handles threaded, audit rows converge exactly, both graders validated against live Trino results). This smoke is what pinned the reference SQL to DOUBLE division: Trino evaluatesBIGINT / DECIMAL(4,1)at scale 1, which loses cents.ANTHROPIC_API_KEYand, fora2, a DataHub quickstart; those runs answer bench(phase 1): pilot — A0 vs A2 on S1/S3 with deterministic seed generator and audit-derived metrics #942's exit criterion (a measurable A2-A0 gap, or the explanation for why there is none).