Skip to content

bench: SF10 perf validation, Tier-B token harness, and the finalized token-efficiency doc scope - #291

Merged
devopam merged 18 commits into
mainfrom
bench/sf10-tier-b-run
Jul 30, 2026
Merged

bench: SF10 perf validation, Tier-B token harness, and the finalized token-efficiency doc scope#291
devopam merged 18 commits into
mainfrom
bench/sf10-tier-b-run

Conversation

@devopam

@devopam devopam commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • SF10 performance validation (docs/performance-benchmark.md) — the published perf writeup, generated from a real TPC-H SF10 run.
  • Tier-B agent-loop token harness (benchmarks/tokens/tier_b/) — built out with checkpoint/resume, a real-Claude-Code (claude -p) on/off comparison mode, and real tool-trace + cost instrumentation. Fixed two --allowedTools scoping bugs found while using it (missing Bash, an incomplete curated tool list) that were causing spurious permission-stall "failures." Internal diagnostic tooling — not part of the published claim (see below).
  • translate_nl_to_sql token-usage fix — the tool's internal LLM call was a total blind spot in cost accounting; it now reports tokens_in/tokens_out from the actual provider response and wires them into the existing (previously dead) audit_nl2sql columns.
  • mcpg --demo fixreviews.customer_id was an unplanted, unindexed FK; now indexed as the seed script's own comment already claimed.
  • Finalized the published token-efficiency claim to Tier A only. docs/token-efficiency.md no longer promises a future agent-loop/session-level savings result; roadmap rows 19.4–19.6 and the original benchmark-suite plan updated to match. The agent-loop objective is descoped from the published claim; its harness stays in-tree as internal tooling only.
  • Merged main (this branch was 9 commits behind) — resolved 4 conflicts, all between independently-written fixes for the same underlying issues (Windows event-loop policy on Python 3.14; Windows-only test path bugs); kept the more robust side in each case, verified no functional loss.

Roadmap linkage

Advances roadmap row: 19.1
Advances roadmap row: 19.2
Advances roadmap row: 19.3
Advances roadmap row: 19.4
Advances roadmap row: 19.5
Advances roadmap row: 19.6

Checklist

  • Tests added/updated first (TDD); suite passes locally (2832 passed, 2 skipped — unit; 47 passed — contract)
  • ruff, ruff format, and mypy src/mcpg pass
  • CHANGELOG.md updated under [Unreleased]
  • Roadmap rows cited above; 19.4 and 19.6 marked ✅ shipped, 19.5 marked descoped
  • No hand-edits to src/mcpg/_vendor/

🤖 Generated with Claude Code

https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9

Summary by Sourcery

Add SF10 performance benchmark artifacts, enhance Tier-B token study harness and diagnostics, and finalize token-efficiency documentation scope to deterministic per-call accounting only.

New Features:

  • Introduce SF10 performance benchmark run JSON and HTML dashboard artifacts and document the constrained-environment validation run.
  • Extend Tier-B token benchmark tasks with analytical NL→SQL questions and real-harness diagnostic task sets.
  • Add real-harness comparison and forced-translate_nl_to_sql experiment scripts for Tier-B to study tool choice and token/cost behavior.

Bug Fixes:

  • Ensure translate_nl_to_sql surfaces its internal provider token usage on TranslationResult and into nl2sql audit records, fixing a blind spot in cost accounting.
  • Index reviews.customer_id in the demo dataset to remove an unintended unindexed foreign key while preserving the intended orders.customer_id planted finding.
  • Fix subprocess bin-allowlist tests to use truly absolute paths on Windows and POSIX.
  • Allow performance benchmark runners to configure query timeouts consistently via a new --timeout flag and MCPG_STATEMENT_TIMEOUT_MS, preventing spurious timeouts on heavy SF10 queries.

Enhancements:

  • Add checkpoint/resume support and e2e-path failure resilience to the performance benchmark runner so long runs are safely persisted and interruptions only lose minimal work.
  • Add checkpoint/resume and configuration-matching safeguards to the Tier-B synthetic harness so paid trials are not re-run and incompatible aggregates are avoided.
  • Track tools with hidden internal LLM cost in the Tier-B agent loop and fold their token usage into total_tokens while preserving a separate breakdown.
  • Extend translate_nl_to_sql’s provider implementations to parse and expose vendor-specific usage blocks for Anthropic, OpenAI-compatible, and Gemini responses, with robust defaults when usage is missing.
  • Update contract tests and tool-output schema expectations to include translate_nl_to_sql token fields.
  • Improve benchmark dashboard generation to read/write files with explicit UTF-8 encoding for portability.

Documentation:

  • Document the SF10 performance validation run, its methodology deviations, and findings in docs/performance-benchmark.md.
  • Narrow docs/token-efficiency.md to Tier-A per-call compactness only, remove future Tier-B agent-loop promises, and clarify scope and caveats.
  • Clarify the original benchmark-suite plan’s status and mark the agent-loop objective as descoped from the published claim.
  • Update the feature shortlist roadmap to mark token accounting Tier A and the combined writeup as shipped and Tier-B agent-loop study as descoped.
  • Refine Tier-B task documentation to distinguish planted-flaw audit tasks from analytical NL→SQL tasks and explain grading and correctness margins.

Tests:

  • Add unit tests to verify translate_nl_to_sql propagates provider token usage across all result branches and that providers parse usage from real HTTP response shapes with safe defaults.
  • Add Tier-B aggregation and hidden-token tests to ensure total_tokens includes internal tool LLM usage and that hidden tokens default correctly when absent.
  • Add tests for Tier-B agent hidden-token extraction from tool structuredContent and ensure only approved tools contribute hidden cost.
  • Adjust demo dataset tests to account for the new reviews.customer_id index.
  • Strengthen config tests around subprocess bin allowlists on multi-platform absolute paths.
  • Update dashboard generation tests indirectly via new SF10 artifacts and token-report handling.

devopam and others added 17 commits July 24, 2026 09:37
…y, checkpointing

Running the perf harness at TPC-H SF10 surfaced three real gaps:

- ServerSideRunner had no way to raise the query timeout past MCPg's
  interactive-workload default (30s), and heavy TPC-H queries at SF10
  run close to that on modest hardware. Add a --timeout CLI flag,
  threaded into ServerSideRunner and into the harness's own
  MCPG_STATEMENT_TIMEOUT_MS (the separate Postgres-level GUC) so one
  flag controls both timeout mechanisms consistently.
- The e2e paths call the real, unmodified server, whose 30s timeout has
  no override — a heavy query legitimately exceeding it should not
  discard every other path/query already measured. Catch e2e-prefixed
  path failures per (path, query), log + record them in run metadata,
  and continue; native/server_side failures still raise, since those
  would be a real regression rather than an expected product ceiling.
- The run only wrote results at the very end, so an interruption lost
  everything measured so far. Checkpoint after every query (and after
  the concurrency sweep) to the same output path, with a
  metadata.complete flag distinguishing a finished run from a partial
  one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
…n1 CVEs

asyncio.WindowsSelectorEventLoopPolicy is renamed to the private
_WindowsSelectorEventLoopPolicy under Python 3.14 (per typeshed); a
static attribute reference only resolves under one target Python
version, so mypy --strict (pinned to python_version = "3.14") flagged
it as missing on this Windows dev machine even though the code runs
correctly on the installed 3.13 interpreter. Resolve it via getattr
with a fallback to the private name instead, in both call sites that
set the policy (__main__.py, http_runtime.py) — both need to set it
globally since the actual asyncio.run() call happens inside a
third-party library (FastMCP, uvicorn) they don't control directly.

Also, while getting a clean local pre-commit run on Windows:

- test_run_http_builds_app_and_serves_via_uvicorn's mock uvicorn.run
  had a strict (app, *, host, port) signature that broke once run_http
  legitimately started passing loop="none" on win32 — widen it to
  **kwargs, matching the sibling Windows-specific test already in this
  file.
- test_config.py's subprocess-allowlist tests hardcoded Unix-style
  "/usr/bin" as an example absolute path; os.path.isabs requires a
  drive letter on Windows (ntpath), so it isn't one there. Use
  platform-appropriate absolute paths instead of assuming POSIX.
- pyasn1 0.6.3 (pulled in transitively via the gcp extra's
  google-auth) carries 5 known CVEs; bump to the patched 0.6.4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
Path.write_text/read_text default to the platform locale encoding,
which is cp1252 on Windows — writing the SF10 dashboard crashed with
UnicodeEncodeError on the "Δ" delta character the report renders.
Encode explicitly as UTF-8 on both the read and write side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
A real SF10 run — 60M lineitem rows, all 11 t_db_matches_native
assertions passing, decomposition and concurrency-sweep numbers that
match the committed SF1 run's shape. Added as a distinct section rather
than replacing the SF1 result: this environment (a tuned Docker
Desktop container on Windows, sharing host resources) is explicitly
not the dedicated hardware the "reproduce it yourself" section
describes for the eventual headline SF10 run, so the section states
that plainly and every methodology deviation (raised timeouts, Postgres
tuning, --e2e omitted after repeated unexplained process terminations)
up front rather than presenting the absolute numbers as directly
comparable.

Raw run JSON and rendered dashboard committed alongside for
reproducibility.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
Each Tier-B trial is a real, costed model conversation. The perf
harness lost hours of measurement to unexplained process interruptions
in this environment; a Tier-B interruption loses money instead, which
is worse. Write the run to --output after every single trial (not just
at the end), and on startup, load an existing incomplete checkpoint
and skip any (task, arm, trial) already recorded there rather than
paying for it again — but only when the checkpoint's model /
trials-per-arm / max-turns / tool-set match this invocation exactly,
so a differently-configured rerun can't silently mix incompatible
results into one aggregate.

Verified end-to-end against a real (cheap, Haiku, capped-turns) run:
trimming a completed checkpoint to a partial one and rerunning with
identical args skipped exactly the trials already present and made no
new API calls for them; rerunning with a changed --max-turns correctly
refused to resume and started clean instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
…calls

claude-sonnet-5 (the default --model) rejects temperature as a
deprecated parameter — HTTP 400 invalid_request_error,
"\`temperature\` is deprecated for this model." — which made every
trial in the first real Tier-B run fail immediately (0 tokens billed,
since it errors before generating anything; confirmed via the
checkpoint's per-trial error field). Drop the kwarg entirely rather
than special-case it per model. Verified with a cheap 1-trial rerun
against the real target model: all 6 (task, arm) trials now complete
with real token counts and correct grading.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
…already promised

The seed script's own comment says "order_items and reviews get
proper FK indexes — contrast that makes the missing one on
orders.customer_id a *finding*, not a theme," but the DDL only ever
indexed reviews.product_id, leaving reviews.customer_id unindexed too.
That's a second real unindexed FK the dataset never intended to plant,
discovered when the Tier-B benchmark's missing_index task graded a
model's confident, correctly-reasoned identification of
reviews.customer_id as wrong (the grader only accepts orders +
customer_id) — the model wasn't mistaken, the dataset just had an
extra, unplanted gap. Add the index the comment already claimed
existed; the intended orders.customer_id finding is untouched and
still asserted by test_planted_findings_are_present_in_the_ddl.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
translate_nl_to_sql makes its own internal LLM call — a separate HTTP
request to whichever provider is configured, independent of and
invisible to whatever's driving MCPg over MCP. TranslationResult
reported no token usage at all, so that call's real cost was a total
blind spot to any external cost accounting, including an agent's own
token budget and (what surfaced this) the Tier-B benchmark, where
adding this tool without fixing that would have silently made MCPg
look cheaper than it actually is.

Add ProviderCompletion (text + tokens_in/tokens_out) as the Protocol's
return type in place of a bare str; parse each vendor's usage block
instead of discarding it (Anthropic: usage.input_tokens/output_tokens;
OpenAI-compatible: usage.prompt_tokens/completion_tokens; Gemini:
usageMetadata.promptTokenCount/candidatesTokenCount) — missing/
malformed usage defaults to 0 rather than raising, since it's a
nice-to-have, not load-bearing for a translation succeeding. Thread
tokens_in/tokens_out through all five TranslationResult construction
sites (success, refused, preflight-rejected, no-execute, and
exec-error branches all share the one provider call).

Also wires prompt_tokens/completion_tokens into audit_nl2sql.py's
record_nl2sql_event() — it already accepted these params and the audit
table already had the columns, but the call site never had a token
count to pass, so this was dead plumbing until now.

Regenerated the tool-return-shapes contract snapshot
(MCPG_REGENERATE_TOOL_RETURN_SHAPES=1) — a mechanical two-field diff.
tool_surface.snapshot.json is unaffected (only pins name/description/
inputSchema). Updated the test_tool_output_schemas.py manifest to
match (its test only checks a subset, so this wasn't required, just
kept accurate).

Added the first real HTTP-response-shape tests this module has ever
had for the three provider classes (test_nl2sql.py's existing tests
all bypass real HTTP/JSON parsing via a stub) — mocking pattern
borrowed from test_oidc.py's fake-AsyncClient approach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
…tical tasks

Two changes so Tier-B can fairly measure MCPg's actual NL->SQL
workflow rather than only the audit-lookup tools it tested before:

- Add translate_nl_to_sql to the MCPg arm's tool list. Without the
  companion nl2sql.py fix (previous commit), this alone would have
  biased the study toward MCPg — the tool's internal LLM call is
  invisible to the outer agent loop this harness measures.
- TrialResult gains hidden_tokens_in/out (defaulting to 0, so old
  checkpoint JSON without these keys still resumes fine via
  TrialResult(**t)); total_tokens now sums all four fields.
  run_trial's tool-call loop reads translate_nl_to_sql's reported
  usage straight out of the MCP tool result's structuredContent and
  folds it in — baseline arm never sees this tool, so its hidden
  totals stay 0 and no asymmetry is introduced there.

Two new tasks, both graded against ground truth computed directly
against the live demo dataset (not asserted from memory): top revenue
category ("Home", a clear margin over the runner-up) and top
lifetime-spend customer ("Liam Okafor", >2x the runner-up). Unlike the
three existing planted-flaw audit tasks — simple lookups a model
rarely gets wrong — these need a real multi-table join + aggregation,
closer to the plain-English business questions an agent driving MCPg
actually gets asked, and more likely to provoke a wrong first SQL
attempt worth measuring the retry cost of.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
The default Tier-B "mcpg" arm never once chose to call
translate_nl_to_sql on either claude-haiku-4-5 or claude-sonnet-5, even
on the two tasks built for it — it always explored the schema and
wrote its own SQL via run_select instead. That means the normal arm
can't tell us whether MCPg's dedicated NL2SQL tool actually helps.

Added a standalone, reusable diagnostic (never wired into runner.py,
never run in CI) that forces the comparison: for the two analytical
tasks, MCPg's arm gets ONLY translate_nl_to_sql (no run_select, no
schema tools — it cannot answer without going through it), baseline
stays run_select-only. First attempt failed silently in an unhelpful
way — the script only configured MCPG_DATABASE_URL, not the separate
MCPG_NL2SQL_PROVIDER/MCPG_NL2SQL_API_KEY translate_nl_to_sql's internal
LLM call actually needs (distinct from the outer agent's own key) — so
every call errored with "no provider configured" and the resulting
"baseline wins" number was the tool failing outright, not losing a
fair comparison. Fixed, and added a fail-fast check so a missing key
errors immediately instead of spending on trials that can't succeed.

Committed result is a real, verified 1-trial-per-arm run confirming
the fix works end-to-end (non-zero hidden tokens now show up in real
trials, ~7k tokens per translate_nl_to_sql call) and a genuine finding:
even guaranteed to use it, MCPg's NL2SQL tool cost ~4.6x more tokens
and was less reliable (50% vs 100%) than the model just writing SQL
itself. One trial made 17 tool calls before failing — an open question
the current TrialResult schema can't explain (it stores the final
answer, not a turn-by-turn transcript).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
Tier-B's synthetic Python agent loop never actually tested "Claude Code
with MCPg available" - it's a from-scratch harness with a one-line system
prompt calling the raw Anthropic API against MCP tools directly. That gap
explains a real discrepancy: translate_nl_to_sql never fired once across
Tier-B's free-choice runs, but the same model uses it readily in a real
Claude Code project with MCPg configured.

Adds a new diagnostic that drives real `claude -p` headless invocations
twice per task - once with MCPg's MCP server available, once without -
and compares Claude Code's own reported token/cost numbers directly,
instead of extending the synthetic harness further. Two new analytical
tasks with verified ground truth against the live demo dataset, designed
to need genuine multi-table joins/aggregation rather than lookups.

Never wired into runner.py or CI - costed, spawns real subprocesses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
…ison

Switches real_harness_comparison.py from --output-format json to
stream-json so each trial's actual tool-call sequence is captured (which
tools fired, in order) instead of only aggregate token/turn counts - the
thing needed to actually answer "did translate_nl_to_sql fire," which
token/turn counts alone can't. Adds cost_usd and tool_names to the shared
TrialResult schema (both default to their zero-value for the synthetic
harness, which has neither).

Verified with a live dry run before wiring in: confirmed the event shape,
confirmed --bare mode isn't usable here (needs a plain ANTHROPIC_API_KEY
for the top-level session's own auth; this project authenticates via
OAuth), and confirmed the model calling a tool name (list_tables) that
doesn't exist in MCPg, recovering from the error - real signal the
instrumentation is now able to surface.

Re-ran the smoke test end to end through the fixed script (4 real
trials, all correct): translate_nl_to_sql fired 0/2 times in the mcpg
arm, same as the first smoke test - 0/4 total across both runs now, with
real tool-call evidence rather than inference from token counts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
…gers translate_nl_to_sql

Adds --mcpg-system-prompt-hint, applied only to the mcpg arm via
--append-system-prompt, to test a concrete hypothesis for why
translate_nl_to_sql fires in a real project but not in this comparison:
that project injects a standing instruction every session via a
SessionStart hook ("Prefer mcpg for DB inspection, schema/design review,
query plans, and audits over ad-hoc psql.").

Result: appending that exact text did not change the outcome -
translate_nl_to_sql still fired 0/2 times in the mcpg arm, same as
without it (0/6 total across all three real-harness runs now). The
model still reached for run_select/get_compact_schema/list_schemas
instead. Consistent with the hint text itself, which names inspection/
audit/plan work but never mentions NL-to-SQL translation - so this
specific standing instruction isn't sufficient on its own to explain the
other project's behavior; something else (task phrasing, conversation
history, or run-to-run variability) is still unaccounted for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CgKWtcUQYcfuQv3mpyGQ9
The token-efficiency writeup and its cross-references (performance
writeup, roadmap rows 19.4-19.6, the original benchmark-suite plan)
promised a future agent-loop (Tier B) round-trip savings result. That
objective is descoped from the published claim; the writeup now states
only the deterministic per-call compactness result (Tier A) and its
break-even accounting, paired with the performance result.
…t task-set

Two permission-scoping bugs in real_harness_comparison.py's mcpg arm
caused spurious tool-approval stalls in headless mode: Bash wasn't in
--allowedTools at all, and the curated 9-tool mcpg allowlist missed
real tools the model reached for (e.g. lint_naming_conventions).
Replaced the curated list with a wildcard (mcp__mcpg__*) plus Bash, so
the mcpg arm is genuine free choice rather than an artificial subset.

Also adds a --task-set flag (analytical | audit) so the harness can
run against tasks.py's planted-flaw DBA tasks, not just the two
analytical ones — internal diagnostic tooling, not part of the
published token-efficiency claim.
# Conflicts:
#	src/mcpg/__main__.py
#	src/mcpg/http_runtime.py
#	tests/unit/test_config.py
#	tests/unit/test_http_runtime.py
Both were already shipped on this branch (b10602c, 44f82d8) but never
got an Unreleased entry.

@gemini-code-assist-2 gemini-code-assist-2 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several enhancements to the benchmark suite and token accounting, most notably capturing and reporting the internal LLM token usage for the translate_nl_to_sql tool across Anthropic, OpenAI, and Gemini providers. It also adds checkpointing and resumability to the performance and Tier-B token runners to prevent data loss during long runs, introduces a --timeout option for the performance runner, and indexes reviews.customer_id in the demo seed script. Documentation and feature plans have been updated accordingly. Feedback on the changes points out a potential UnicodeEncodeError on Windows when writing the performance checkpoint file without an explicit UTF-8 encoding, and a code suggestion has been provided to resolve this.

Comment thread benchmarks/perf/runner.py Outdated
Comment on lines +253 to +254
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(run.to_dict(), indent=2) + "\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Writing the JSON checkpoint file without specifying an explicit encoding (e.g., encoding="utf-8") defaults to the platform-dependent locale encoding. On Windows environments, this often defaults to CP1252, which can cause a UnicodeEncodeError if any non-ASCII characters are present in the metadata (such as the PostgreSQL version string or platform information). Specifying encoding="utf-8" ensures consistent and robust behavior across all platforms, matching the fixes applied to other benchmark scripts in this PR.

Suggested change
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(run.to_dict(), indent=2) + "\n")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(run.to_dict(), indent=2) + "\n", encoding="utf-8")

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues, and left some high level feedback:

  • Consider keeping the generated benchmark artifacts (perf-sf10.html/json, real-harness comparison JSONs, forced-nl2sql-comparison.json) out of the tracked tree or moving them under a dedicated artifacts/ or samples/ area so they don’t grow the repo and get confused with source-of-truth inputs.
  • The checkpoint/resume guards for Tier-B and the real-harness comparison only key off model/trials/config flags; it would be safer to include database_url (and for real_harness, worktree_dir) in _RESUME_KEYS so runs against different databases or code trees can’t be silently merged into one aggregate.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider keeping the generated benchmark artifacts (perf-sf10.html/json, real-harness comparison JSONs, forced-nl2sql-comparison.json) out of the tracked tree or moving them under a dedicated `artifacts/` or `samples/` area so they don’t grow the repo and get confused with source-of-truth inputs.
- The checkpoint/resume guards for Tier-B and the real-harness comparison only key off model/trials/config flags; it would be safer to include `database_url` (and for real_harness, `worktree_dir`) in `_RESUME_KEYS` so runs against different databases or code trees can’t be silently merged into one aggregate.

## Individual Comments

### Comment 1
<location path="benchmarks/tokens/tier_b/tasks.py" line_range="123-132" />
<code_context>
+    ]
+
+
+def audit_tasks() -> list[Task]:
+    """The three planted-flaw DBA tasks from ``default_tasks()``, isolated for the real-harness diagnostic.
+
+    The synthetic Tier-B harness measured these three costing MCPg's
+    advisor arm 2.66x more tokens than bare SQL — the opposite of what the
+    tool descriptions promise (one advisor call vs. many exploratory
+    queries) — and that number was never explained, only recorded. This
+    subset lets ``real_harness_comparison`` re-run just these three through
+    real Claude Code with tool-trace capture, to see *why*: whether the
+    advisor tool gets called and then the agent explores anyway, whether
+    its output isn't trusted, or something else.
+    """
+    return default_tasks()[:3]
+
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Selecting audit tasks by list slice is brittle and will silently change if default_tasks() ordering ever changes.

audit_tasks() assumes the first three entries in default_tasks() are always the planted-flaw audit tasks. Any future reordering or prepending in default_tasks() will change this subset without obvious breakage. Please select these tasks by explicit identifiers (e.g., missing_index, pii_columns, naming_violation) instead of positional slicing to keep the behavior stable as default_tasks() evolves.

Suggested implementation:

```python
    ]


 def audit_tasks() -> list[Task]:
     """The three planted-flaw DBA tasks from ``default_tasks()``, isolated for the real-harness diagnostic.

     The synthetic Tier-B harness measured these three costing MCPg's
     advisor arm 2.66x more tokens than bare SQL — the opposite of what the
     tool descriptions promise (one advisor call vs. many exploratory
     queries) — and that number was never explained, only recorded. This
     subset lets ``real_harness_comparison`` re-run just these three through
     real Claude Code with tool-trace capture, to see *why*: whether the
     advisor tool gets called and then the agent explores anyway, whether
     its output isn't trusted, or something else.

     Tasks are selected by explicit identifiers (``missing_index``,
     ``pii_columns``, ``naming_violation``) rather than positional slicing,
     so changes to ``default_tasks()`` ordering do not silently alter this
     subset.
     """
     audit_ids = ("missing_index", "pii_columns", "naming_violation")

     # Build an index of tasks by identifier to avoid relying on list order.
     tasks_by_id: dict[str, Task] = {task.id: task for task in default_tasks()}

     try:
         return [tasks_by_id[audit_id] for audit_id in audit_ids]
     except KeyError as exc:
         # Fail loudly if any expected planted-flaw task is missing, rather
         # than silently changing the audit subset.
         missing = ", ".join(audit_id for audit_id in audit_ids if audit_id not in tasks_by_id)
         raise RuntimeError(
             f"Expected planted-flaw audit tasks missing from default_tasks(): {missing}"
         ) from exc


```

I assumed the task identifier attribute is named `id`. If your `Task` model uses a different field (e.g. `slug`, `name`, or `key`), update `task.id` and the `audit_ids` values to match the actual identifiers for the planted-flaw tasks (`missing_index`, `pii_columns`, `naming_violation`). Also ensure `Task` is hashable and that `default_tasks()` returns a list containing these three tasks with those identifiers.
</issue_to_address>

### Comment 2
<location path="benchmarks/tokens/tier_b/agent.py" line_range="70-74" />
<code_context>
+_TOOLS_WITH_HIDDEN_LLM_COST = {"translate_nl_to_sql"}
+
+
+def _hidden_tokens(tool_name: str, result: Any) -> tuple[int, int]:
+    """The tokens ``tool_name``'s own internal LLM call spent, or (0, 0)."""
+    if tool_name not in _TOOLS_WITH_HIDDEN_LLM_COST:
+        return 0, 0
+    structured = getattr(result, "structuredContent", None) or {}
+    return int(structured.get("tokens_in") or 0), int(structured.get("tokens_out") or 0)
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Assuming structuredContent is always a dict can raise at runtime if the SDK payload shape changes.

`structured = getattr(result, "structuredContent", None) or {}` assumes `structuredContent` is a mapping. If a future SDK/tool returns a list or other non-mapping type, `structured.get(...)` will raise `AttributeError` and break trial accounting. Please guard with `isinstance(structured, dict)` (or `Mapping`) before using `.get`, and return `(0, 0)` for unsupported types.
</issue_to_address>

### Comment 3
<location path="benchmarks/perf/runner.py" line_range="215-224" />
<code_context>
+def _checkpoint(
</code_context>
<issue_to_address>
**nitpick (bug_risk):** Checkpoint writing omits an explicit encoding, unlike other new write_text calls that standardize on UTF-8.

In `_checkpoint()`, `args.output.write_text(...)` still uses the platform default encoding, unlike the other updated paths that explicitly set `encoding="utf-8"`. Please pass `encoding="utf-8"` here as well to keep checkpoint files consistent and avoid cross-platform issues (e.g., non-UTF-8 defaults on Windows).
</issue_to_address>

### Comment 4
<location path="benchmarks/tokens/tier_b/experiments/real_harness_comparison.py" line_range="178" />
<code_context>
+def _invoke_claude(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The claude subprocess invocation does not enforce any overall timeout, so a stuck CLI could hang the experiment.

`_invoke_claude()` starts `claude -p` and calls `proc.communicate()` without a timeout. If the CLI or its network call hangs, the experiment can block indefinitely and never checkpoint the current trial. Please consider wrapping `communicate()` in `asyncio.wait_for` with a reasonable upper bound and, on timeout, recording a `TrialResult` error so one bad invocation cannot stall the entire run.
</issue_to_address>

### Comment 5
<location path="benchmarks/tokens/tier_b/runner.py" line_range="131" />
<code_context>
+    return trials
+
+
+async def _run(args: argparse.Namespace) -> TierBReport:
+    nl2sql_key = os.environ.get("MCPG_NL2SQL_API_KEY") or os.environ.get("ANTHROPIC_API_KEY", "")
+    if not nl2sql_key:
</code_context>
<issue_to_address>
**issue (complexity):** Consider encapsulating the resume/checkpoint behavior into a dedicated helper that owns configuration, state, and persistence so `_run` can focus solely on orchestrating task execution.

The resume/checkpoint logic is useful, but it now cross‑cuts `_run` and introduces several implicit contracts. You can keep all functionality while isolating this concern and making the main loop easier to follow.

### 1. Extract a small helper to own resume/checkpoint state

Right now `_load_resumable`, `_checkpoint`, `_RESUME_KEYS`, and the `done` set are spread across the module. Pull them into a tiny helper that owns:

- loading existing trials
- deciding whether a `(task_id, arm, trial)` is done
- checkpointing after each new trial
- writing the final report

This keeps `_run` focused on task execution.

```python
@dataclass(frozen=True)
class RunConfig:
    model: str
    trials_per_arm: int
    max_turns: int
    mcpg_tools: list[str]

    @classmethod
    def from_args(cls, args: argparse.Namespace, resolved_mcpg_tools: set[str] | None = None) -> "RunConfig":
        tools = sorted(resolved_mcpg_tools or args.mcpg_tools)
        return cls(
            model=args.model,
            trials_per_arm=args.trials,
            max_turns=args.max_turns,
            mcpg_tools=tools,
        )

class ResumableRun:
    def __init__(self, args: argparse.Namespace, resolved_mcpg_tools: set[str]):
        self.args = args
        self.config = RunConfig.from_args(args, resolved_mcpg_tools)
        self.trials: list[TrialResult] = self._load_resumable()
        self._done = {(t.task_id, t.arm, t.trial) for t in self.trials}

    def is_done(self, task_id: str, arm: str, trial: int) -> bool:
        return (task_id, arm, trial) in self._done

    def record(self, result: TrialResult, *, complete: bool = False) -> TierBReport:
        self.trials.append(result)
        self._done.add((result.task_id, result.arm, result.trial))
        return self._checkpoint(complete=complete)

    def finalize(self) -> TierBReport:
        return self._checkpoint(complete=True)

    # existing _load_resumable / _checkpoint logic moved in here, using self.config
    def _load_resumable(self) -> list[TrialResult]:
        ...
    def _checkpoint(self, *, complete: bool) -> TierBReport:
        ...
```

Then `_run` becomes much clearer:

```python
async def _run(args: argparse.Namespace) -> TierBReport:
    settings = load_settings({"MCPG_DATABASE_URL": args.database_url})
    model = AnthropicClient(args.model)
    tasks = default_tasks()

    async with AsyncExitStack() as stack:
        server = create_server(settings)
        session = await stack.enter_async_context(
            create_connected_server_and_client_session(server, read_timeout_seconds=_READ_TIMEOUT)
        )
        listed = await session.list_tools()
        server_names = {t.name for t in listed.tools}
        mcpg_tools = {n for n in args.mcpg_tools if n in server_names}

        missing = set(args.mcpg_tools) - server_names
        if missing:
            print(f"warning: requested mcpg tools not on the server, skipped: {sorted(missing)}")

        runner = ResumableRun(args, resolved_mcpg_tools=mcpg_tools)
        arms = ((ARM_BASELINE, _BASELINE_TOOLS & server_names), (ARM_MCPG, mcpg_tools))

        for task in tasks:
            for trial in range(args.trials):
                for arm, allowed in arms:
                    if runner.is_done(task.id, arm, trial):
                        print(f"  {task.id:16} {arm:9} #{trial}: skipped (already recorded)")
                        continue

                    result = await run_trial(
                        task,
                        session=session,
                        model=model,
                        allowed_tools=allowed,
                        arm=arm,
                        trial=trial,
                        max_turns=args.max_turns,
                    )
                    flag = "PASS" if result.passed else ("ERR " if result.error else "FAIL")
                    print(
                        f"  {task.id:16} {arm:9} #{trial}: "
                        f"tok={result.total_tokens:6} tools={result.tool_calls:2} turns={result.turns:2} {flag}"
                    )
                    runner.record(result, complete=False)

        return runner.finalize()
```

This keeps the nested loops “for task, for trial, for arm, run_trial” visually intact, with resume/checkpoint concerns pushed behind `ResumableRun`.

### 2. Make the metadata/config contract explicit

You currently duplicate the config fields in `_checkpoint` and `_load_resumable` and rely on `_RESUME_KEYS`. The `RunConfig` above can be used to:

- serialize/deserialize the run configuration to/from metadata
- compare the on‑disk config with the current invocation

For example inside `ResumableRun._load_resumable`:

```python
def _load_resumable(self) -> list[TrialResult]:
    if not self.args.output.exists():
        return []
    try:
        data = json.loads(self.args.output.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return []

    meta = data.get("metadata", {})
    if meta.get("complete"):
        print(f"note: {self.args.output} already holds a complete run; overwriting with a fresh one.")
        return []

    disk_config = RunConfig(
        model=meta.get("model"),
        trials_per_arm=meta.get("trials_per_arm"),
        max_turns=meta.get("max_turns"),
        mcpg_tools=meta.get("mcpg_tools") or [],
    )
    if disk_config != self.config:
        print(f"note: {self.args.output} exists but its config differs from this run; starting fresh, not resuming.")
        return []

    trials = [TrialResult(**t) for t in data.get("trials", [])]
    if trials:
        print(f"resuming from {self.args.output}: {len(trials)} trial(s) already recorded, will not be re-run.")
    return trials
```

And `_checkpoint` can build metadata from `self.config`, reducing repetition:

```python
def _checkpoint(self, *, complete: bool) -> TierBReport:
    metadata: dict[str, Any] = {
        "timestamp": self.args.timestamp,
        "git_sha": self.args.git_sha,
        "mcpg_version": __version__,
        "model": self.config.model,
        "trials_per_arm": self.config.trials_per_arm,
        "max_turns": self.config.max_turns,
        "mcpg_tools": self.config.mcpg_tools,
        "host": {"python": platform.python_version(), "os": platform.platform()},
        "complete": complete,
    }
    report = TierBReport(metadata=metadata, trials=self.trials, aggregate=aggregate(self.trials))
    self.args.output.parent.mkdir(parents=True, exist_ok=True)
    self.args.output.write_text(json.dumps(report.to_dict(), indent=2) + "\n", encoding="utf-8")
    return report
```

This makes the matching criteria explicit and central, avoiding implicit contracts between multiple helpers.

### 3. Optional: decouple checkpoint frequency from loop structure

If you later want to reduce I/O without changing semantics, you can let `ResumableRun` control checkpoint cadence (e.g. every N trials, or per task) without touching the loop:

```python
class ResumableRun:
    def __init__(..., checkpoint_interval: int = 1):
        ...
        self._checkpoint_interval = checkpoint_interval

    def record(self, result: TrialResult, *, complete: bool = False) -> TierBReport | None:
        self.trials.append(result)
        self._done.add((result.task_id, result.arm, result.trial))
        if complete or len(self.trials) % self._checkpoint_interval == 0:
            return self._checkpoint(complete=complete)
        return None
```

You can keep the default `checkpoint_interval=1` to preserve the “after every trial” behavior while making it easier to tune later.

These changes should retain all existing behavior (resume safety, per‑trial checkpointing, config checks) but significantly reduce the cognitive load in `_run` and make the resume logic easier to maintain.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread benchmarks/tokens/tier_b/tasks.py
Comment thread benchmarks/tokens/tier_b/agent.py Outdated
Comment thread benchmarks/perf/runner.py
Comment thread benchmarks/tokens/tier_b/experiments/real_harness_comparison.py
Comment thread benchmarks/tokens/tier_b/runner.py
- perf/runner.py: both checkpoint write_text() calls now pass
  encoding="utf-8", matching the other benchmark scripts (Windows
  defaults to a non-UTF-8 locale encoding, risking UnicodeEncodeError).
- tasks.py: audit_tasks() selects by explicit task id instead of a
  positional slice of default_tasks(), so a future reorder can't
  silently change which tasks it returns.
- agent.py: _hidden_tokens() guards structuredContent with an
  isinstance check before calling .get() on it.
- real_harness_comparison.py: _invoke_claude()'s subprocess call now
  has a timeout (default 600s, --invocation-timeout-seconds), so a
  hung claude -p process can't block a run indefinitely.
- runner.py + real_harness_comparison.py: _RESUME_KEYS now includes
  database_url (and worktree_dir for real_harness_comparison), so a
  checkpoint file can't be silently resumed against a different
  database or code tree than it was started with.

Declined: moving committed benchmark result JSONs out of the tracked
tree (matches this repo's existing, repeated convention for benchmark
results — out of scope for this PR to change) and the suggested
ResumableRun/RunConfig extraction in runner.py (working, tested code;
the suggested abstraction isn't earned by a problem this module
actually has).
@devopam
devopam merged commit 4561de8 into main Jul 30, 2026
10 checks passed
@devopam
devopam deleted the bench/sf10-tier-b-run branch July 30, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant