Skip to content

Complete the run-artefact logging (close 8 gaps, fix 2 latent bugs) - #40

Merged
ZmeiGorynych merged 17 commits into
mainfrom
logging-completeness
Jun 10, 2026
Merged

Complete the run-artefact logging (close 8 gaps, fix 2 latent bugs)#40
ZmeiGorynych merged 17 commits into
mainfrom
logging-completeness

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

Audit-driven sweep that closes every gap between what the agents collect during a run and what reaches the on-disk artefacts under runs/<benchmark>/<db>/<iid>/<run-id>.{json,trajectory.json} + results.db. Two of the gaps were silent bugs that have been writing `None` since DEV-1515.

Three commits, each independently testable:

  1. `91999f2` — writer-side plumbing + fix two latent cost-key bugs

    • Local writer (`run.py::grade_local_row`) never read `cost_usd*` from `usage`.
    • Cloud writer (`ray_app._grade_one_submission`) read them under the WRONG key names (`cost_usd_agent` instead of `agent_cost_usd` — `TokenUsage.model_dump()` emits the second form). Every cloud annotation since DEV-1515 had `None` costs.
    • New shared helper `extract_usage_costs(usage_blob) -> (agent, sim)` is the single source of truth; both call sites delegate to it.
    • One-shot `claude_sdk_otf*` flavors now set `usage["n_ask_user_calls"]` symmetrically with the a-interact flavors (was missing on one-shot).
    • `finalize_result_row` gains chokepoint backfills for `usage.n_ask_user_calls` and `predicted_row_count` (latter from `predicted_result_json::row_count`).
  2. `bec60a3` — capture `tool_call_stats` + `ResultMessage` metadata on claude_sdk runs

    • The pydantic_ai* family computed `tool_call_stats` during the run via `_run_capture._extract_tool_stats`; claude_sdk* didn't. New `extract_tool_stats_from_claude_sdk_trajectory` produces the SAME output shape so downstream consumers (cascading reports, regrade scripts) don't fork.
    • The SDK's final `ResultMessage` emits `num_turns`, `stop_reason`, `duration_ms`, `duration_api_ms`. Pre-fix the adapters consumed these only for token-accumulation and dropped the lifecycle fields. `stop_reason` is the canonical "why did the agent stop" signal and was previously invisible.
    • Both backfills wired through `finalize_result_row` (shape-discriminator gates which trajectories take the claude_sdk path) so zero per-adapter diff. SDK metadata namespaced under `usage.sdk_*` so `results.db::usage_json` carries it for free.
  3. `3c7c180` — snapshot agent config into every annotation + `run_metadata`

    • New `SubmissionConfig` pydantic class (`framework`, `mode`, `query_mode`, `agent_model`, `user_sim_model`, `slayer_setup`, `reasoning_effort`, `patience`, `max_depth`, `dataset`, `strict`, `use_audited_gold_sql`, `prompt_cache`).
    • Per user choice: duplicated into BOTH `SubmissionMetadata.config` (so each annotation is self-describing for grep / ad-hoc inspection) AND `results.db::run_metadata` (9 new nullable columns via additive ALTER, so SQL joins work). Both populated from the same source — args namespace locally, manifest fields on cloud.
    • After this lands, the cost-by-mode summary is one SQL JOIN on `(run_id, framework, mode)` — no more parsing the cloud run-id substring (`-slayer-` vs `-raw-`).

Test plan

  • Full non-integration test suite (`env -u SSH_AUTH_SOCK uv run --extra all --extra dev --extra pydantic-ai pytest`): currently 2675 pass (up from 2626 pre-PR; +49 new tests).
  • Cloud smoke: `bird-interact-cloud submit --subscription-auth ... --instance-ids `, fetch, confirm:
    • `.json::submission` has populated `cost_usd_agent`, `cost_usd_user_sim`, `n_agent_turns`, `n_ask_user_calls`, `config.*`.
    • `.trajectory.json::tool_call_stats` is populated.
    • `.trajectory.json::usage` carries `sdk_stop_reason`, `sdk_num_turns`, `sdk_duration_ms`, `sdk_duration_api_ms`.
  • DB check: `sqlite3 results.db "SELECT * FROM run_metadata WHERE run_id = ''"` shows the full 14-field row.
  • Re-run the cost-by-mode summary against the new annotations — `submission.cost_usd_*` is no longer None, and the query can JOIN against `run_metadata` for the mode/effort breakdown without parsing run-id substrings.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Per-task wall-clock timeout; richer per-run submission config persisted; autopsy always returns structured error results; improved prompt guidance for opaque grader failures; tool-call/error stats and adapter result-metadata extraction; usage now records ask-user call counts.
  • Bug Fixes

    • Stronger subscription-OAuth validation for Claude SDK flows; CLI requires explicit subscription-auth choice and enforces annotation availability; consistent cost extraction and telemetry backfills; safer, defensive trajectory handling.
  • Tests

    • Extensive new/updated tests for timeouts, annotation guards, metadata/tool-stats extraction, costs, autopsy, DB migration, and submission config.
  • Chores

    • Removed legacy audited-gold submit-time guard and its tests; results DB schema migration support.

ZmeiGorynych and others added 10 commits June 8, 2026 08:30
…annotation

The annotation file (DEV-1515) is the authoritative grading source —
gold_variants, evaluator_prompt, masked_terms, original_gold_is_correct
all live there. Audited-gold is derivative: a task only has an audited
row when its annotation says the original gold is wrong. The old
audited-gold guard therefore mis-fires on the (now common) case where
the annotation says the original IS correct (48/76 of last week's
mini-interact retry batch), forcing operators to pass
--no-require-audited-gold just to proceed.

Flip the gate to annotation-existence and decouple from
--use-audited-gold-sql — the annotation is needed for grading
regardless of whether the overlay is applied.

Drops the now-obsolete _audited_gold_check module + tests; the
harness-side overlay still pins the primary-first contract for
multi-variant audited rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y fall-back

When the operator passes neither --subscription-auth nor
--no-subscription-auth (previously the default), the submit/annotate
parser now rejects. When --subscription-auth IS chosen but
CLAUDE_CODE_OAUTH_TOKEN is absent or doesn't have the sk-ant-oat01-
prefix, parse-time fail too — the OAuth path no longer silently falls
through to the API-key path.

Failure mode this closes: the OAuth token lives in an env file (.env.ubuntu
here) that interactive login shells source but non-interactive
bird-interact-cloud invocations don't. The pre-fix code saw no token,
silently picked the API-key path, and the 76-task mini-interact retry
batch burned through the API account's credit balance partway, leaving
20 tasks with billing_error: "Credit balance is too low" instead of a
real verdict. Forcing an explicit choice + a token check at parse +
mirror checks in driver/prereqs (defence-in-depth for resubmit, which
bypasses the CLI) all stop that class of failure cold.

Internal `no_subscription_auth` field name preserved for back-compat
with the manifest schema and downstream callers (driver, prereqs,
ray_app); the CLI just mirrors the negation into the same attribute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reading 53 agent_miss trajectories from this week's mini-interact retry
batches revealed 17/53 (32%) match a single signature the existing
classifier didn't have a label for: SLayer's translator always wraps a
dimension-only SELECT in a top-level GROUP BY over every projected
column, with no aggregates. When the gold query expects raw rows (one
per source record) this silently dedups — the agent's predicted row
count comes in well below gold's, and the rowset is a strict subset.

Symptoms in `miss_diagnostics`: `agent_has_group_by=True` while
`best_variant_has_group_by=False`, `rowset_relation_to_best=disjoint`
or `subset_of`. The classifier was previously mis-labelling these as
`user_sim_misleading` / `wrong_join_path` / `exhausted_budget_guessing`
because none of its existing categories fit and the LLM picked the
nearest-neighbour label.

Adds the new pattern to both flavor taxonomies and prompts; the
remediation hint steers the agent toward `mcp__slayer__query_nested`
(or the dedup-off path) when raw rows are wanted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fields

Two follow-ups to the DEV-1535 subscription-auth refactor:

1. The autopsy stage built `anthropic.AsyncAnthropic()` with no args,
   so the SDK walked `ANTHROPIC_API_KEY` → `ANTHROPIC_AUTH_TOKEN`. On
   OAuth workers `ANTHROPIC_API_KEY` is deliberately removed (see
   `ray_app._apply_actor_env_local`), so the autopsy crashed with
   `TypeError("Could not resolve authentication method...")` — 18/28
   missing autopsies in the last mini-interact run. The 3 residual
   `BadRequestError: "Credit balance is too low"` are the same class:
   autopsy fell through to a still-set API key that had no credit.

   `_build_anthropic_client()` resolves auth in order: OAuth subscription
   (`auth_token=...` → Bearer), API-key fallback, else a clear
   `RuntimeError` so the outer `except Exception` records a meaningful
   FQN instead of the SDK's cryptic `TypeError`.

2. The a-interact tool schema marks `key_asks`, `disclosed_resolutions`,
   `undisclosed_resolutions` as required, BUT the prompt was silent on
   them — the LLM omitted them, pydantic rejected the call, and 7/28
   autopsies died with `Field required`. Align UP (not relax DOWN, per
   PR feedback): add an explicit instructional block to the a-interact
   prompt naming the three fields with `[]`-when-empty guidance.
   One-shot stays unchanged (the four ask-shaped fields are not in its
   schema; naming them even to say "skip" would invite latching).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5/53 misses in the latest mini-interact retry batches were the
`output_schema_misread` pattern — agent computed the right values but
the column ORDER / column COUNT / value TYPE didn't match gold. The
`_COLUMN_NAMES_DONT_AFFECT_GRADING` block already told the agent that
names don't matter; it was silent on what TO do when stuck.

Adds two paragraphs (shared by all 4 OTF flavors, since the pattern
hits both one-shot livesqlbench and a-interact mini-interact):
  1. Re-read the question before submit for explicit ordering language
     ("list X, then Y"), bare-quoted IDs (string vs int), and boolean
     predicates (raw BOOLEAN vs CAST INT).
  2. When the grader returns an opaque "ex_base returned 0 but
     expected 1" and the values look right: try shape permutations
     FIRST (column order, bare types, column-count drop/add) BEFORE
     adding logic, changing formulas, or asking the user.

Snapshot hashes for SLAYER_OTF_ONE_SHOT / SLAYER_OTF_AINTERACT
rebaselined; no prompt-content tests added (per
feedback_no_prompt_content_tests in memory).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… cap

Sweep of 76 mini-interact retries showed every `correct` verdict landed
under 891s / 175 trajectory steps and every `valid_interpretation`
under 659s / 202 steps; 27/53 `agent_miss` runs burned past 900s
thrashing on a wrong baseline they couldn't recover from. The previous
500-step patience was never close to binding (max overall was 295) —
historical paranoia from DEV-1478. Drop to 250 and add a real
wall-clock cap.

Implementation: `asyncio.wait_for` in `run_one_task_with_runner`,
gated by `BIRD_INTERACT_PER_TASK_TIMEOUT_S` (default 900, set 0 to
disable). On timeout, raise `TimeoutError` with a clear message so the
per-task error row records what actually happened rather than the
empty `asyncio.TimeoutError`. Same try/except path → no other
plumbing changes; the cap is a hook the existing error machinery
catches.

Expected impact on the studied retry batch: 0% false-negative on
correct/valid, kills 27 (51%) of agent_miss runs early, ~30% sum
wallclock reduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The most-recent-attempt sweep over runs/mini-interact/ (671 attempts)
showed n_agent_turns=None on every claude_sdk* row — only the
pydantic_ai* adapters set the field explicitly. The CascadingReport
column, the post-hoc budget-sizing analyses, and the cloud cascading
metrics all had to fall back on len(trajectory) as a turn-count proxy.

Backfill in `finalize_result_row` (the single point every adapter funnels
through) by counting AssistantMessage entries in the trajectory when the
adapter didn't set the field explicitly. Pre-set values are preserved so
the pydantic_ai* path that computes its own count from
`run.all_messages()` still wins.

Defensive against the claude_sdk fallback that stuffs `str(msg)` into
the trajectory when dataclasses.asdict raises — non-dict entries are
ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two latent bugs since DEV-1515 left `submission.cost_usd_agent` and
`submission.cost_usd_user_sim` at None on every cloud and local
annotation:

  - The cloud actor (`ray_app._grade_one_submission`) read the costs
    under the WRONG key names — `cost_usd_agent` / `cost_usd_user_sim`
    — while `TokenUsage.model_dump()` emits `agent_cost_usd` /
    `user_sim_cost_usd`. The keys never resolved, so the field passed
    to the annotation writer was always None.
  - The local writer (`run._grade_local_row`) never extracted the cost
    fields at all — only `n_agent_turns` / `n_ask_user_calls`.

Adds the shared `extract_usage_costs(usage_blob) -> (agent, sim)`
helper in `eval/grade_in_place.py` next to `decode_result_json`. Both
the cloud and local writer route through it so the wrong-key drift
can't recur. The cloud fall-through path (write_failed_submission_annotation
on grader-exception) also receives the costs now.

Also surface `usage.n_ask_user_calls` symmetrically across all four
`claude_sdk_otf*` flavors. Pre-fix only the a-interact variants
copied `ctx_dict.asks_used` into the result row's usage dict; one-shot
flavors silently dropped it. Writing 0 rather than absent keeps the
usage shape consistent (one-shot SLAYER excludes `ask_user` so the
value is always 0, but field-presence consistency lets downstream
joins / cost queries work uniformly).

`finalize_result_row` gains two more chokepoint backfills:

  - `usage.n_ask_user_calls` defaults to 0 if absent (defensive
    backstop for adapters that forget the convention).
  - `predicted_row_count` is backfilled from `predicted_result_json`'s
    `row_count` field when the adapter hasn't set it (handles both raw
    dict and JSON-encoded shapes; no-op on malformed snapshots or
    error-snapshots without `row_count`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…runs

Two adapter-side signals the claude_sdk family was throwing away:

1. tool_call_stats — per-tool n_calls / n_errors / error_samples in
   the SAME shape as `_run_capture._extract_tool_stats` (the
   pydantic_ai sibling). The new
   `extract_tool_stats_from_claude_sdk_trajectory` walker reads
   AssistantMessage.tool_use blocks (counts calls + builds the
   tool_use_id → name map) then UserMessage.tool_result blocks
   (counts errors; resolves names via the map). Cap at 10 samples /
   400 chars each, matching the sibling.

2. ResultMessage metadata — num_turns, stop_reason, duration_ms,
   duration_api_ms from the SDK's final stream message. Pre-fix
   `accumulate_assistant_usage` consumed these messages for token
   counting and dropped the lifecycle fields. `stop_reason` is the
   canonical "why did the agent stop" signal (turn budget vs
   end_turn vs error) and was previously invisible. Folded under
   `usage.sdk_*` rather than a new top-level dict so results.db's
   `usage_json` carries them for free — no _persist or downstream
   parser changes.

Both backfills land in `finalize_result_row` so zero per-adapter
diff is needed; the shape discriminator
(`_looks_like_claude_sdk_trajectory`) keeps pydantic_ai trajectories
on their existing path. Adapter-explicit values (rare —
multi-phase runs that pre-compute these) are preserved via
`setdefault` / `is None` guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…adata

Cost-by-mode / failure-mode-by-effort analyses previously had to parse
the cloud run-id substring (`-slayer-` vs `-raw-`) to recover the
framework / mode / query_mode of a row. The config was in the manifest
but never landed on the per-task artefact or the results.db sink.

Closes that gap two ways at once (per design choice — user explicitly
asked for both):

  - `SubmissionMetadata.config: Optional[SubmissionConfig]` — a new
    pydantic class carrying framework, mode, query_mode, agent_model,
    user_sim_model, slayer_setup, reasoning_effort, patience,
    max_depth, dataset, strict, use_audited_gold_sql, prompt_cache.
    Threaded through `_build_submission_annotation`,
    `_write_harness_confirmed_annotation`,
    `write_failed_submission_annotation`, `grade_and_write`, and
    `grade_one_submission` (all Optional with None defaults — older
    callers / pre-DEV-1535 annotations parse cleanly).

  - `results_db.run_metadata` table extended with 9 new nullable
    columns (additive ALTER via the same `_DIAGNOSTIC_COLUMNS`
    pattern used for task_results). Existing result DBs gain the
    columns on next `open_db`. `insert_run_metadata` accepts the new
    fields keyword-only with None defaults so legacy callers
    (existing tests, older invocations) work unchanged. Bool fields
    coerced via int() — SQLite has no native bool — with None
    preserved so "never set" stays distinguishable from "explicitly
    False".

Both the local runner (`run.py`) and the cloud actor / collator
(`ray_app.py`, `collation.py`) build the same SubmissionConfig +
extended run_metadata block from the same source — args namespace
for local, manifest fields for cloud. Same shape, same downstream
joins.

After this lands the cost-by-mode summary becomes a single SQL JOIN
on (run_id, framework, mode) — no more substring parsing — and
annotations are individually self-describing for grep / ad-hoc
inspection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces audited-gold submit checks with annotation-file guards, enforces Claude OAuth prereqs, extracts tool/result telemetry from Claude SDK trajectories into finalize_result_row, threads SubmissionConfig and canonical usage-costs through grading and persistence, adds per-task timeouts, updates autopsy, and expands tests.

Changes

Core Infrastructure: Trajectory extraction and harness backfills

Layer / File(s) Summary
Claude SDK trajectory helpers
src/bird_interact_agents/agents/_run_capture.py
Adds _looks_like_claude_sdk_trajectory, extract_tool_stats_from_claude_sdk_trajectory, and extract_claude_sdk_result_metadata returning None for non-matching shapes.
Finalize result-row backfills
src/bird_interact_agents/harness.py, tests/test_finalize_result_row.py
finalize_result_row ensures usage is a dict, backfills n_agent_turns from AssistantMessage counts, initializes usage["n_ask_user_calls"], backfills predicted_row_count, derives tool_call_stats from Claude trajectories, and folds sdk_* metadata via setdefault.

Agents & prompts

Layer / File(s) Summary
Agent usage includes ask-user counter
src/bird_interact_agents/agents/claude_sdk_otf/agent.py, src/bird_interact_agents/agents/claude_sdk_otf_raw/agent.py
Agents merge n_ask_user_calls (from context asks_used) into the usage object passed to finalize_result_row.
Prompt guidance update
src/bird_interact_agents/agents/_shared_otf_prompts.py, tests/test_shared_otf_prompts.py
Expanded _COLUMN_NAMES_DONT_AFFECT_GRADING with opaque-failure troubleshooting guidance and updated snapshot digests.

Submission validation, CLI, driver, and prereqs

Layer / File(s) Summary
Annotation-file submit-time guard
src/bird_interact_agents/cloud/_annotation_check.py, tests/cloud/test_annotation_check.py
Adds missing_annotation_ids() checking <annotations_root>/<benchmark>/<db>/<iid>.task.json presence and annotations_requiring_audited_gold_without_rows() reporting iids whose annotations mark gold incorrect but lack audited-gold rows.
CLI: explicit subscription-auth and annotation guard
src/bird_interact_agents/cloud/cli.py, tests/cloud/test_cli.py
Requires --subscription-auth/--no-subscription-auth, validates OAuth token when chosen, lowers default patience, and replaces audited-gold guard with --require-annotation (default ON) enforced at parse-time.
Driver & prereqs: fail-fast OAuth for Claude SDK
src/bird_interact_agents/cloud/driver.py, src/bird_interact_agents/cloud/prereqs.py, tests/cloud/test_driver.py, tests/cloud/test_prereqs.py
read_api_keys_from_local_env and check_api_keys now hard-fail when Claude SDK frameworks require subscription OAuth and CLAUDE_CODE_OAUTH_TOKEN is missing; test wiring adjusted for predicate imports.
Cleanup: audited-gold guard removed
removed: src/bird_interact_agents/cloud/_audited_gold_check.py, removed tests tests/cloud/test_audited_gold_check.py
Removed the prior audited-gold guard module and its tests; layered audited-gold-row sync check implemented in _annotation_check.

Configuration snapshots, costs, and persistence

Layer / File(s) Summary
SubmissionConfig model and threading
src/bird_interact_agents/eval/annotation_schema.py, src/bird_interact_agents/eval/grade_in_place.py, tests/test_submission_config.py
Adds SubmissionConfig model and threads optional config through grade_and_write, write_failed_submission_annotation, and grade_one_submission so per-task config snapshot is persisted in submission annotations.
Usage-cost extraction
src/bird_interact_agents/eval/grade_in_place.py, tests/eval/test_extract_usage_costs.py
Adds extract_usage_costs to read canonical agent_cost_usd and user_sim_cost_usd keys; run/ray paths use it to populate annotation cost fields.
Results DB run_metadata expansion & migration
src/bird_interact_agents/results_db.py, src/bird_interact_agents/cloud/collation.py, tests/test_results_db.py
Adds extra nullable run_metadata columns, open_db adds missing columns via ALTER, and insert_run_metadata accepts and persists the extended config snapshot (boolean→nullable-int coercion preserved).
Ray and run wiring
src/bird_interact_agents/cloud/ray_app.py, src/bird_interact_agents/run.py
Per-task _submission_config snapshot built from runtime cfg; extracted costs and turn counts wired into grading and failed-annotation paths; run_evaluation and collate() persist extended run metadata.

Advanced features and autopsy

Layer / File(s) Summary
Per-task wall-clock timeout
src/bird_interact_agents/run.py, tests/cloud/test_run_one_task.py
Adds _per_task_timeout_s() reading BIRD_INTERACT_PER_TASK_TIMEOUT_S (default 900s; non-positive disables) and wraps runner with asyncio.wait_for, raising a clearer TimeoutError.
Autopsy enhancements
src/bird_interact_agents/eval/autopsy.py, tests/test_autopsy.py
Adds _build_anthropic_client() preferring OAuth then API key, one-shot vs interactive prompt/tool schema branching, new slayer_overaggregation pattern, and structured _autopsy_error_result() so run_autopsy never returns None.
Large test updates Extensive test additions/updates across modules to cover new guards, auth behavior, run-metadata migration, trajectory extractors, and finalize_result_row backfills.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"🧡🐇 I hopped through code with careful cheer,
I counted turns and trailed the tool-use gear,
Wrote snapshots, guards, and timeouts clear and neat,
A carrot for tests — the pipeline stays complete! 🥕"

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/test_autopsy.py (1)

1304-1310: ⚡ Quick win

Unusual default parameter pattern may cause confusion.

The function signature captures _ONE_SHOT_PATTERNS as a default parameter value, shadowing the module-level variable. This freezes the list at function definition time rather than reading it at test execution time.

If this is intentional (e.g., to guard against runtime mutations of the module variable), consider adding a comment explaining why. Otherwise, remove the parameter and reference the module variable directly in the test body.

♻️ Proposed simplification
-def test_tool_schema_one_shot_pattern_enum_matches(_ONE_SHOT_PATTERNS=_ONE_SHOT_PATTERNS):
+def test_tool_schema_one_shot_pattern_enum_matches():
     from bird_interact_agents.eval.autopsy import _AUTOPSY_TOOL_SCHEMA_ONE_SHOT

     pattern_enum = set(
         _AUTOPSY_TOOL_SCHEMA_ONE_SHOT["input_schema"]["properties"]["pattern"]["enum"]
     )
     assert pattern_enum == set(_ONE_SHOT_PATTERNS)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_autopsy.py` around lines 1304 - 1310, The test function
test_tool_schema_one_shot_pattern_enum_matches currently uses a default
parameter `_ONE_SHOT_PATTERNS=_ONE_SHOT_PATTERNS` which shadows and freezes the
module-level `_ONE_SHOT_PATTERNS`; remove the default parameter so the test body
references the module-level `_ONE_SHOT_PATTERNS` directly (or if freezing was
intentional, add an explicit comment above the function explaining why the
default is required to guard against runtime mutations). Ensure the assertion
still builds `pattern_enum` from
`_AUTOPSY_TOOL_SCHEMA_ONE_SHOT["input_schema"]["properties"]["pattern"]["enum"]`
and compares it to the module `_ONE_SHOT_PATTERNS`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bird_interact_agents/cloud/_annotation_check.py`:
- Around line 90-91: The current check uses path.exists() which returns true for
directories and can let malformed checkouts pass; change the guard to require a
real file by replacing the exists() check with path.is_file() for the
constructed path variable (the one built from ann_root / bench.name / db /
f"{iid}.task.json") so the code enforces the docstring/CLI contract and fails
early if the annotation file is missing or not a regular file.

In `@src/bird_interact_agents/cloud/driver.py`:
- Around line 166-167: The branch incorrectly calls
prereqs._is_claude_sdk_framework(...) which can be fooled if the prereqs module
is monkeypatched; instead import the predicate function by name (e.g.
_is_claude_sdk_framework) and call it directly, then keep the existing behavior
of checking not no_subscription_auth and reading CLAUDE_CODE_OAUTH_TOKEN into
token; update the top-level imports to import _is_claude_sdk_framework from the
prereqs module and replace prereqs._is_claude_sdk_framework(framework) with a
direct call to _is_claude_sdk_framework(framework).

In `@src/bird_interact_agents/harness.py`:
- Around line 1103-1105: The telemetry backfill only runs when row.get("usage")
is a dict, so create/initialize row["usage"] to an empty dict before applying
defaults: ensure the code in harness.py that sets _usage = row.get("usage")
instead sets row["usage"] = row.get("usage") or {} (and then uses _usage =
row["usage"]) so n_ask_user_calls is added for rows that previously had no
usage; apply the same initialization for the other backfill block referenced
(the one around lines 1147-1155) so sdk_* fields and other defaults are always
populated even when usage is missing.

---

Nitpick comments:
In `@tests/test_autopsy.py`:
- Around line 1304-1310: The test function
test_tool_schema_one_shot_pattern_enum_matches currently uses a default
parameter `_ONE_SHOT_PATTERNS=_ONE_SHOT_PATTERNS` which shadows and freezes the
module-level `_ONE_SHOT_PATTERNS`; remove the default parameter so the test body
references the module-level `_ONE_SHOT_PATTERNS` directly (or if freezing was
intentional, add an explicit comment above the function explaining why the
default is required to guard against runtime mutations). Ensure the assertion
still builds `pattern_enum` from
`_AUTOPSY_TOOL_SCHEMA_ONE_SHOT["input_schema"]["properties"]["pattern"]["enum"]`
and compares it to the module `_ONE_SHOT_PATTERNS`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9fecc227-552e-4ddc-8506-90b7890af599

📥 Commits

Reviewing files that changed from the base of the PR and between 78b3d92 and 3c7c180.

📒 Files selected for processing (32)
  • src/bird_interact_agents/agents/_run_capture.py
  • src/bird_interact_agents/agents/_shared_otf_prompts.py
  • src/bird_interact_agents/agents/claude_sdk_otf/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_raw/agent.py
  • src/bird_interact_agents/cloud/_annotation_check.py
  • src/bird_interact_agents/cloud/_audited_gold_check.py
  • src/bird_interact_agents/cloud/cli.py
  • src/bird_interact_agents/cloud/collation.py
  • src/bird_interact_agents/cloud/driver.py
  • src/bird_interact_agents/cloud/prereqs.py
  • src/bird_interact_agents/cloud/ray_app.py
  • src/bird_interact_agents/eval/annotation_schema.py
  • src/bird_interact_agents/eval/autopsy.py
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/harness.py
  • src/bird_interact_agents/results_db.py
  • src/bird_interact_agents/run.py
  • tests/cloud/test_annotation_check.py
  • tests/cloud/test_audited_gold_check.py
  • tests/cloud/test_cli.py
  • tests/cloud/test_driver.py
  • tests/cloud/test_prereqs.py
  • tests/cloud/test_run_one_task.py
  • tests/eval/test_extract_usage_costs.py
  • tests/test_autopsy.py
  • tests/test_dual_eval.py
  • tests/test_extract_claude_sdk_result_metadata.py
  • tests/test_extract_tool_stats_claude_sdk.py
  • tests/test_finalize_result_row.py
  • tests/test_results_db.py
  • tests/test_shared_otf_prompts.py
  • tests/test_submission_config.py
💤 Files with no reviewable changes (2)
  • src/bird_interact_agents/cloud/_audited_gold_check.py
  • tests/cloud/test_audited_gold_check.py

Comment thread src/bird_interact_agents/cloud/_annotation_check.py Outdated
Comment thread src/bird_interact_agents/cloud/driver.py Outdated
Comment thread src/bird_interact_agents/harness.py Outdated
ZmeiGorynych and others added 3 commits June 9, 2026 21:23
…dk paths

Three review findings, all in the same area, all small but compound:

1. (Codex) `harness.finalize_result_row` backfilled `n_agent_turns`
   only to the top-level `row["n_agent_turns"]`. Every annotation
   writer (local `run.py::_grade_local_row`, cloud
   `ray_app._grade_one_submission`) reads
   `usage_blob.get("n_agent_turns")`, so the backfill was dead for
   the exact runs that needed it. Mirror the backfilled value into
   `usage["n_agent_turns"]` so writers actually see it.

2. (CodeRabbit, major) The `usage`-side backfills (`n_ask_user_calls`,
   `sdk_*`) only fired when `row["usage"]` was already a dict. The
   early-error claude_sdk paths
   (`agents/claude_sdk_otf/agent.py:393-406`, the analogous early
   returns in claude_sdk_otf_raw) build rows with NO `usage` field
   at all. Hoist `row["usage"] = {}` initialisation to the top of
   `finalize_result_row` so every backfill below has a target.

3. (Codex) `_run_capture._looks_like_claude_sdk_trajectory` only
   checked `"data" in item`. The `claude_sdk_*_raw` adapters
   serialize each trajectory item as
   `{"type": ..., "data": str(msg)[:500]}` — `data` is a string,
   not a dict. Pre-fix the discriminator accepted these and the
   walker returned an empty stats dict, falsely indicating
   "0 tool calls / 0 errors" instead of "not extractable". Tighten
   to require `isinstance(item["data"], dict)`.

Tests extended to cover all three:
  - `test_initialises_usage_when_row_has_no_usage_field` +
    `test_replaces_non_dict_usage_with_initialised_dict` (replace
    the prior tests that asserted skip-when-missing — opposite of
    the new intent).
  - `test_sdk_metadata_lands_even_when_starting_usage_was_not_a_dict`
    (the early-error claude_sdk paths are now analyzable).
  - `test_n_agent_turns_backfill_mirrors_into_usage` +
    `test_n_agent_turns_mirror_does_not_clobber_explicit_usage_value`.
  - `test_returns_none_on_claude_sdk_raw_string_data_shape` +
    `test_returns_none_when_any_item_has_non_dict_data` for both
    walkers (tool stats + result metadata).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-arg trick)

Three review nits, no behaviour change in production code paths but
each shores up an edge case:

1. (CodeRabbit) `cloud/_annotation_check.py` used `path.exists()`,
   which returns True for directories too. A malformed checkout
   where the annotation slot was accidentally created as a directory
   would silently pass the submit-time guard and then fail later
   when the harness tried to read the file. `is_file()` matches the
   docstring / CLI help "file" contract.

2. (CodeRabbit) `cloud/driver.py` called the predicate via
   `prereqs._is_claude_sdk_framework(framework)` (module-attribute
   access). Some tests monkeypatch `driver.prereqs` to a generic
   MagicMock; the predicate then becomes a truthy mock and the
   OAuth path fires for every framework, masking real test
   failures. The current driver test fixture works around this with
   a `side_effect` wire-through, but a future test could trip on
   it. Importing `_is_claude_sdk_framework` by name removes the
   indirection.

3. (CodeRabbit nit) `tests/test_autopsy.py::test_tool_schema_one_shot_pattern_enum_matches`
   had an unusual `_ONE_SHOT_PATTERNS=_ONE_SHOT_PATTERNS` default
   parameter trick. Dropped — the module-level variable is
   referenced directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…predicted_row_count

Two Codex findings on the post-Group-1/2 push:

1. (Codex, minor) Cloud actor at `ray_app.py:822` hardcoded
   `predicted_row_count=None`, ignoring the new backfill in
   `finalize_result_row`. Cloud-side annotations lost the row-count
   evidence the new `slayer_overaggregation` autopsy pattern relies
   on. Forward `row.get("predicted_row_count")` instead.

2. (Codex, major) The `--require-annotation` swap (commit `da857dd`,
   per user request) dropped the audited-gold presence check
   entirely. When an annotation has `original_gold_is_correct=False`
   AND `--use-audited-gold-sql` is on AND the audited_gold sidecar
   is missing a row for that iid, `load_audited_gold_rows_for`
   returns [] and the tolerant grader silently falls back to the
   (annotated-as-wrong) original gold. The primary annotation guard
   doesn't catch this — both annotation AND audited_gold files
   exist, but they're out of sync.

   Layered guard `annotations_requiring_audited_gold_without_rows`:
   only fires when both `--require-annotation` and
   `--use-audited-gold-sql` are on (matches the intent of the user's
   original gate-swap — annotation is still the primary gate; this
   plugs the sync gap without reintroducing the audited-gold
   requirement for the 48 `original_gold_is_correct=True` instances
   that legitimately have no audited row). Bypassable via the same
   `--no-require-annotation` opt-out.

   Includes 4 new tests covering: original-correct (no check), wrong
   gold + missing audited row (caught), wrong gold + matching row
   (passes), id-without-annotation (skipped — primary guard's job).

Also tightens the existing annotation guard's `path.exists()` →
`path.is_file()` regression test added so directories at the
annotation path can't bypass either check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bird_interact_agents/cloud/_annotation_check.py`:
- Around line 154-156: Currently load_audited_gold_rows_for(...) is being called
inside the instance_ids loop which reparses the audited-gold file for every iid;
instead, call load_audited_gold_rows_for once before iterating instance_ids to
build a presence index (e.g., a set of instance ids or a dict keyed by instance
id) and then consult that in the loop; update the code in
src/bird_interact_agents/cloud/_annotation_check.py to call
load_audited_gold_rows_for(benchmark=bench.name) once, create a fast lookup like
audited_ids = {row['instance_id'] for row in rows} (or keep rows indexed by
instance_id), and replace the per-iteration load_audited_gold_rows_for(...) call
with a membership/lookup check against audited_ids using the iid variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b8172d7-e4bb-4032-8587-aeffe3956b26

📥 Commits

Reviewing files that changed from the base of the PR and between 663e9ef and 53fcc29.

📒 Files selected for processing (4)
  • src/bird_interact_agents/cloud/_annotation_check.py
  • src/bird_interact_agents/cloud/cli.py
  • src/bird_interact_agents/cloud/ray_app.py
  • tests/cloud/test_annotation_check.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/bird_interact_agents/cloud/cli.py

Comment thread src/bird_interact_agents/cloud/_annotation_check.py Outdated
Four findings from Codex + CodeRabbit on the round-2 push:

1. (Codex, major) The DEV-1535 per-task wall-clock cap was wrapped
   ONLY around `run_one_task_with_runner` (the `cached_runner` path
   used for raw query_mode). SLayer / non-raw runs fall through to
   `run_one_task` — pre-fix that path ran uncapped, defeating the
   cap on exactly the runs most likely to thrash. Mirror the
   `asyncio.wait_for` wrapper into `run_one_task` so both entry
   points respect `BIRD_INTERACT_PER_TASK_TIMEOUT_S`.

2. (Codex, minor) `eval/annotate.py` and `eval/regrade.py` still
   read costs under the WRONG key names (`cost_usd_agent` /
   `cost_usd_user_sim`) — the same bug we fixed for the cloud +
   local grader-write paths, repeated in the regenerate-annotation
   and regrade CLIs. Route both through `extract_usage_costs` so
   the canonical TokenUsage keys (`agent_cost_usd` /
   `user_sim_cost_usd`) resolve. Fixture in
   `tests/test_eval_annotate_cli.py` updated to the canonical
   shape; the prior test was asserting the (broken) old behaviour.

3. (Codex, major) The layered audited-gold guard silently swallowed
   parse / schema errors on annotation files. The submit guard
   exists to surface these BEFORE cluster startup; swallowing them
   recreates the exact delayed-failure mode the guard prevents.
   Malformed annotations are now REPORTED in the missing-list.

4. (CodeRabbit, major perf) The layered guard re-scanned the full
   audited-gold consolidated JSONL once per iid — O(N_ids ×
   file_size). For a 76-iid retry batch that's 76 full passes.
   New `_build_audited_gold_presence_index` reads it once into a
   set; two-pass loop avoids building the index at all when no
   annotation actually requires it (`original_gold_is_correct=True`
   everywhere). Spy-based tests pin the call counts at 1 (with
   requirement) and 0 (without).

Existing CLI test
(`test_require_annotation_passes_for_livesqlbench_when_file_present`)
updated to pass `--no-use-audited-gold-sql` so it tests the primary
guard in isolation — the layered guard now correctly rejects its
bare `{}` stub as malformed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_eval_annotate_cli.py (1)

173-174: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Update test fixture to use canonical usage-cost key names.

The inline fixture still uses the old wrong key names (cost_usd_agent, cost_usd_user_sim), while line 51's _write_attempt_json was correctly updated to the canonical names (agent_cost_usd, user_sim_cost_usd). When generate_submission_annotation calls extract_usage_costs(usage), it will not find these old keys and will return (None, None), so this test is inadvertently validating the None-cost fallback path instead of actual cost extraction.

🔧 Proposed fix to align with canonical key names
-        "usage": {"cost_usd_agent": 0.01, "cost_usd_user_sim": 0.0,
+        "usage": {"agent_cost_usd": 0.01, "user_sim_cost_usd": 0.0,
                   "n_agent_turns": 1, "n_ask_user_calls": 0},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_eval_annotate_cli.py` around lines 173 - 174, The test fixture in
tests/test_eval_annotate_cli.py is using old key names ("cost_usd_agent",
"cost_usd_user_sim") so extract_usage_costs returns (None, None); update the
inline usage dict to use the canonical keys ("agent_cost_usd" and
"user_sim_cost_usd") to match the behavior of _write_attempt_json and ensure
generate_submission_annotation / extract_usage_costs read real values; locate
the usage dict in the failing test and rename those two keys accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/test_eval_annotate_cli.py`:
- Around line 173-174: The test fixture in tests/test_eval_annotate_cli.py is
using old key names ("cost_usd_agent", "cost_usd_user_sim") so
extract_usage_costs returns (None, None); update the inline usage dict to use
the canonical keys ("agent_cost_usd" and "user_sim_cost_usd") to match the
behavior of _write_attempt_json and ensure generate_submission_annotation /
extract_usage_costs read real values; locate the usage dict in the failing test
and rename those two keys accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b625031f-bc31-420d-ad26-126fccc03695

📥 Commits

Reviewing files that changed from the base of the PR and between 53fcc29 and b695a0a.

📒 Files selected for processing (8)
  • src/bird_interact_agents/cloud/_annotation_check.py
  • src/bird_interact_agents/eval/annotate.py
  • src/bird_interact_agents/eval/regrade.py
  • src/bird_interact_agents/run.py
  • tests/cloud/test_annotation_check.py
  • tests/cloud/test_cli.py
  • tests/cloud/test_run_one_task.py
  • tests/test_eval_annotate_cli.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/cloud/test_cli.py

ZmeiGorynych and others added 3 commits June 9, 2026 22:10
…submit auth

Two Codex findings + one missed CodeRabbit fixture from round 3:

1. (Codex, major) Resubmitting pre-DEV-1535 cloud runs hard-failed
   when CLAUDE_CODE_OAUTH_TOKEN wasn't set in the env. Legacy
   manifests don't carry the `no_subscription_auth` field at all,
   so `manifest.get("no_subscription_auth", False)` defaulted to
   "subscription required" and the strict guard in
   `read_api_keys_from_local_env` raised. Pre-DEV-1535 those runs
   silently used the API-key path. Flip the resubmit default to
   `True` (legacy API key) when the field is ABSENT, and log a note
   explaining why. New manifests carry the field explicitly so the
   strict-submit contract for fresh runs is unchanged.

2. (Codex, major) The primary submit guard
   (`missing_annotation_ids`) only checked `path.is_file()` —
   schema validation only fired inside the layered audited-gold
   check, which is gated on `--use-audited-gold-sql`. With
   `--no-use-audited-gold-sql` and default `--require-annotation`,
   a malformed `.task.json` passed the submit and failed mid-run
   when `read_task_annotation` raised. Schema-validate in the
   primary guard too — a present-but-malformed file is just as
   bad as a missing one for the submit-time-fail-fast contract.

3. (CodeRabbit, missed in round 3) `tests/test_eval_annotate_cli.py`
   had a SECOND inline `usage` dict with the legacy wrong-key
   names (`cost_usd_agent` / `cost_usd_user_sim`). My round-3
   string-replace only caught the first one because the values
   differed. Updated to the canonical TokenUsage keys.

Internal test helper `_write_annotation` and the
`test_require_annotation_passes_for_livesqlbench_when_file_present`
fixture write proper TaskAnnotation pydantic models now — the
bare `{}` stub was exploiting the gap Codex flagged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…loop

Two valid Codex findings (the third — `annotate --subscription-auth`
not using OAuth — is incorrect: `_is_claude_sdk_framework("annotator")`
returns True, so the OAuth path does apply to annotator):

1. (major) The DEV-1535-r3 audited-gold presence index only checked
   `instance_id in set` — way too lax. A sidecar row with just
   `{"instance_id": "X"}` and no `audit_status` or `audited_sol_sql`
   would have passed the layered guard but failed the runtime
   overlay (which the original `cloud._audited_gold_check` module —
   replaced in this PR — used to validate via the same rules).

   Harden the index to enforce the same contract
   `apply_audited_gold_overlay` does at runtime:
     - `audit_status` ∈ {clean, original} passes regardless of
       `audited_sol_sql` (original IS the audited gold).
     - `audit_status` ∈ {edited, unrecoverable} requires
       non-empty `audited_sol_sql` list (the overlay's
       `isinstance(audited, list) and bool(audited)` check).
     - Cross-benchmark collision guard: row's `benchmark` field,
       if present, must match the benchmark we're checking (DB
       names overlap across benchmarks by design).
     - Cross-database collision guard: row's `selected_database`,
       if present, must match the dataset map's mapping for the
       id (defence-in-depth).
     - Grouped format: validate the primary variant; fall back
       to the first variant if no primary is marked.

   4 new tests pin the validation rules (missing-sql, empty-sql,
   cross-benchmark, clean-row-passes-empty-sql).

2. (major) The per-task wall-clock cap (`BIRD_INTERACT_PER_TASK_TIMEOUT_S`)
   was wrapped in `run_one_task_with_runner` (cloud actor) and
   `run_one_task` (round-3 fix), but the LOCAL `run_evaluation`
   loop awaited `runner(...)` directly — so the `bird-interact` CLI
   path ran uncapped. Mirror the `asyncio.wait_for` wrapper into
   `run_evaluation._run_with_sem` so the cap binds at every entry
   point now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…base fields

Round 5 hardened the audited-gold presence index to reject malformed
rows (missing audit_status, empty audited_sol_sql), but the
cross-benchmark / cross-database checks were "reject when present and
wrong" — absent/null fields passed through.

The runtime overlay in `harness.apply_audited_gold_overlay` treats
absent `benchmark`/`selected_database` as missing-row (silent
fallback to original gold). The submit-time index must mirror that
contract or the layered guard reopens the silent-fallback gap.

Switch to "require present and matching": reject rows whose
`benchmark` is not a string equal to the benchmark we're checking,
and reject rows whose `selected_database` is not a non-empty string.

2 new regression tests (missing `benchmark` field, missing
`selected_database` field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ZmeiGorynych
ZmeiGorynych merged commit 815c67d into main Jun 10, 2026
1 check passed
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