Skip to content

DEV-1606: converge in-task + cascade graders on "accept what ex_base accepts" - #63

Merged
ZmeiGorynych merged 2 commits into
mainfrom
egor/dev-1606-in-task-grader-stricter-than-final-cascade-single-gold-non
Jun 26, 2026
Merged

DEV-1606: converge in-task + cascade graders on "accept what ex_base accepts"#63
ZmeiGorynych merged 2 commits into
mainfrom
egor/dev-1606-in-task-grader-stricter-than-final-cascade-single-gold-non

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Three grading-stack defects made in-task / cascade grading stricter than the benchmark's authoritative ex_base grader, causing valid-answer thrash (120+ turns, many resubmits) and zeroing correct answers. Surfaced by the DEV-1589 reverse_logistics run, where both non-passes (_17, _18) were grader artifacts, not reasoning misses.

Defect 1 — in-task submit grades against ONE gold, not best-of audited variants

Under --use-audited-gold-sql the in-task grader scored the agent ONLY against the audited primary variant. On an ambiguous task an agent that commits to a valid NON-primary reading got false-negative feedback and thrashed (reverse_logistics_17: 19 resubmits against the primary while its result exactly matched the audited variant the FINAL grader accepts).

  • apply_audited_gold_overlay now attaches the full variant set as task["audited_variants"] for any guard-passing grouped row — decoupled from the sol_sql-swap status gate, so a primary with audit_status="original" still exposes its edited variants.
  • New harness.evaluate_best_of_audited_variants(...) accepts the first matching variant (swap-and-call, status restored in finally).
  • _dispatch_eval falls back to best-of on a primary miss; the matched variant id is surfaced via a non-agent-visible diagnostic (phase1_matched_audited_variant_id) — never in the agent observation.

Defect 2 — cascade relaxation tiers applied independently, never composed

N6 (epsilon) / N7 (whitespace) / N8 (column-order) / N9 (case-fold) were evaluated independently, so an answer correct only under (column-reorder + 2dp-round) passed no single tier → agent_miss (reverse_logistics_18).

  • New bipartite compare_relaxed (also fixes a pre-existing greedy false-fail in compare_numeric_epsilon) + compare_column_order_relaxed.
  • N8 = strict OR (reorder ∘ epsilon); N9 = terminal full cross-product (reorder ∘ epsilon ∘ trailing-whitespace ∘ case-fold), plus a no-reorder path for mismatched column names. Monotonicity unchanged.

Defect 3 — cascade precision diverges from ex_base

N2/N3 + cell tiers compared raw rows; ex_base rounds floats/Decimals to 2dp (ROUND_HALF_UP) and strips ROUND(...) from SQL. So 94.15248… (full-precision agent) vs 94.15 (2dp gold) FAILED every cascade tier but PASSES ex_base.

  • New upstream_ex_base adapters clean_sqls_like_ex_base (remove_round) + preprocess_rows_like_ex_base (2dp / date / dict canonicalisation), identity fallback when upstream is absent.
  • grade_submission normalizes pred/orig/variant rows + strips ROUND; normalized rows are the single variables used by every tier, the judge, persistence and diagnostics. N1's primary path keeps raw SQL (it cleans internally).
  • The postgres in-task grader (_pg_execute_submit_action) routes through a new _compare_pg_rows_2dp (+ remove_round), with _pg_hashable_row as the hashable-safe fallback.

Tests

Full TDD suite landed first: cascade composition, precision (cascade + adapter), postgres 2dp, and in-task best-of. Edge cases from the plan reviews — overlay-attach when primary is original, same-order epsilon+case with mismatched column names, bipartite-not-greedy, postgres dict-cell fallback — are all exercised. Full non-integration suite green (3426 passed, 94 skipped).

Out of scope

  • The user-sim column-order oracle / in-task self-transpose reflex (tracked separately).
  • DISTINCT / set-vs-bag alignment (precision-only by decision).

Closes DEV-1606.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added smarter grading for SQL submissions, including better handling of numeric precision, column reordering, and text normalization.
    • Expanded support for PostgreSQL result comparison to better match existing grading behavior.
    • Enabled “best-of” evaluation across audited variants when the primary audited result does not pass.
  • Bug Fixes

    • Fixed cases where correct submissions could be mislabeled as misses due to overly strict matching.
    • Improved fallback behavior so grading continues smoothly when upstream normalization is unavailable.

…accepts"

Three grading-stack defects made in-task / cascade grading STRICTER than
the benchmark's authoritative ex_base grader, causing valid-answer thrash
and zeroing correct answers.

Defect 1 — in-task best-of-variant. Under --use-audited-gold-sql the
in-task grader scored the agent ONLY against the audited PRIMARY variant.
apply_audited_gold_overlay now attaches the FULL variant set as
task["audited_variants"] for any guard-passing grouped row (decoupled
from the sol_sql-swap status gate, so a primary with audit_status=
"original" still exposes its edited variants); new
evaluate_best_of_audited_variants accepts the first matching variant;
_dispatch_eval falls back to best-of on a primary miss and surfaces the
matched variant id via a non-agent-visible diagnostic
(phase1_matched_audited_variant_id) — never in the agent observation.

Defect 2 — cascade tier composition. N6/N7/N8/N9 were evaluated
independently, so an answer correct only under (column-reorder + epsilon)
passed no single tier. New bipartite compare_relaxed (fixes a pre-existing
greedy false-fail in compare_numeric_epsilon too) + compare_column_order_
relaxed; N8 = strict OR reorder∘epsilon; N9 = terminal full cross-product
(reorder ∘ epsilon ∘ trailing-whitespace ∘ case-fold, plus a no-reorder
path for mismatched column names). Monotonicity unchanged.

Defect 3 — precision parity with ex_base. N2/N3 + cell tiers compared raw
rows; ex_base rounds to 2dp and strips ROUND from SQL. New
upstream_ex_base adapters clean_sqls_like_ex_base (remove_round) +
preprocess_rows_like_ex_base (2dp / date / dict canonicalisation);
grade_submission normalizes pred/orig/variant rows (the single normalized
variables used by every tier, the judge, persistence and diagnostics);
the postgres in-task grader (_pg_execute_submit_action) routes through a
new _compare_pg_rows_2dp (+ remove_round), with _pg_hashable_row as the
hashable-safe fallback.

Tests: full TDD suite landed first (composition, precision cascade +
adapter, postgres 2dp, in-task best-of). Full non-integration suite green.

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

linear Bot commented Jun 26, 2026

Copy link
Copy Markdown
DEV-1606 In-task grader stricter than final cascade: single-gold + non-composed tolerance tiers cause valid-answer thrash

In-task grader is harsher than the final cascade

Two related grader defects make the agent thrash (120+ turns, many resubmits)
on answers that are actually correct or valid. Both are pre-existing and
benchmark-side (NOT pre-encoding-specific) — surfaced by the DEV-1589
reverse_logistics run (claude_sdk / opus-4-7 / a-interact / pre-encoded otf),
where the only two non-passes (reverse_logistics_17, _18) were BOTH grader
artifacts, not reasoning misses.

Defect 1 — in-task submit grades against ONE gold, not best-of audited variants

harness.py::_pg_execute_submit_action (and the SQLite execute_submit_action
path) compares the agent's result against a single sol_sql
(sample_status.original_data["sol_sql"] — original gold, or the audited
PRIMARY variant under --use-audited-gold-sql). It does NOT pick the best
score across audited variants. The harness comment is explicit: the sol_sql
list is "a sequence of dependent steps, not a set of independent alternatives."

Best-of-audited-variant matching exists ONLY at final offline cascade grading
(eval/tolerant_grader.py::grade_submission, tiers N2/N3 over the
audited_sol_sql variants in audited_gold/<bench>/<bench>_audited.jsonl).

Consequence: on an ambiguous task (original_gold_is_correct=False,
multiple valid readings) the agent gets FALSE-NEGATIVE in-task feedback the
moment it commits to a valid NON-primary reading — "ex_base returned 0… try
again" — so it resubmits endlessly. reverse_logistics_17 submitted 19**
**times / 125 turns, every attempt rejected against the primary
v1_gold_broad_costs (9 cost components), while its result EXACTLY matched the
audited variant v2_kb_strict_trc — which the FINAL grader accepts
(valid_interpretation). It was right the whole time. A task that exhausts
patience before stumbling onto the primary reading would score 0 on a valid
answer.

Fix: teach the in-task submit grader to accept a best-of match against ANY
audited variant for the task (mirror the final cascade's N2/N3). Pull the
variant set the same way grade_submission does.

Defect 2 — cascade relaxation tiers are applied independently, never composed

eval/tolerant_grader.py evaluates N6 (numeric-epsilon), N8 (column-order),
N9 (case-fold), etc. INDEPENDENTLY: N8 reorders columns but compares values
EXACTLY (no rounding); N6 rounds/tolerates but does NOT reorder. An answer
that is correct only under BOTH (e.g. column-reorder AND 2dp-round) passes no
single tier and is mislabeled agent_miss.

Consequence: reverse_logistics_18 computed the exactly-correct
per-condition avg disposal-cost + avg carbon, but (a) emitted the two metric
columns in the opposite order from gold (cost-first, matching the question
wording "How pricey… what about carbon?"; gold is carbon-first for no semantic
reason) and (b) didn't round to 2dp. It is correct under (reorder + 2dp-round)
but passed neither N6 nor N8 alone → scored 0 / agent_miss. Verified:
reorder+round makes pred == gold exactly for all 4 rows.

Fix: compose the cell/column relaxation tiers (at minimum N8 ∘ N6 —
reorder-then-epsilon) so an answer correct under a combination of independent
tolerances passes. Keep monotonicity.

Why one issue

Both live in the grading stack (harness.py in-task + tolerant_grader.py
final), both manifest as the same user-visible symptom (opaque "ex_base 0"
feedback → valid-answer thrash → can zero a correct answer), and a fix should
land + be tested together so the in-task and final graders converge on "accept
anything the final cascade would accept."

Tests

  • In-task grader: a submission matching a non-primary audited variant passes
    in-task (new harness test); regression that primary-only still works.
  • Cascade composition: an answer correct only under (column-reorder + epsilon)
    yields a passing tier, not agent_miss (tolerant_grader test).
  • Mechanical contracts only — no prompt-content tests. Run the FULL
    non-integration suite after each step.

Out of scope

  • The user-sim cannot be a column-order oracle (separate prompt-side concern —
    the user-sim role-plays from the question + labeled ambiguities and will
    confirm the question-natural column order, which can disagree with gold; an
    in-task self-transpose reflex that does not defer to the user is tracked
    separately).

Context

Sequel finding from DEV-1589 (claude_sdk OTF encoder) reverse_logistics eval.

Review in Linear

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ZmeiGorynych, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 25 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 21aa8998-c0cc-4956-86bf-36df5a39013e

📥 Commits

Reviewing files that changed from the base of the PR and between 350ed90 and 353166c.

📒 Files selected for processing (5)
  • src/bird_interact_agents/agents/_submit.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • src/bird_interact_agents/harness.py
  • tests/eval/test_tolerant_grader_composition.py
  • tests/test_in_task_best_of_variant.py
📝 Walkthrough

Walkthrough

The pull request adds upstream-compatible SQL and row normalization for grading, revises tolerant comparison tiers with relaxed matching and column-order handling, and adds audited-variant best-of fallback with matched-variant tracking in submission results.

Changes

Grading precision and audited fallback

Layer / File(s) Summary
Postgres 2dp normalization
src/bird_interact_agents/eval/upstream_ex_base.py, src/bird_interact_agents/harness.py, tests/test_pg_submit_precision.py
upstream_ex_base adds fallback SQL and row normalization helpers, and the Postgres submit path strips ROUND(...), normalizes rows, and compares results at 2dp.
Relaxed grading tiers
src/bird_interact_agents/eval/tolerant_grader.py, tests/eval/test_tolerant_grader_composition.py, tests/eval/test_tolerant_grader_precision.py
tolerant_grader adds relaxed multiset matching, relaxed column-order comparison, upstream-aligned normalization, and revised N8/N9 tier checks.
Audited variant overlay and evaluator
src/bird_interact_agents/harness.py, tests/test_in_task_best_of_variant.py
Audited-gold overlay now attaches full audited variant sets per task, and evaluate_best_of_audited_variants scans those variants until one passes.
Best-of submission plumbing
src/bird_interact_agents/agents/_submit.py, tests/test_in_task_best_of_variant.py
_dispatch_eval falls back to best-of audited variants, returns the matched variant id, and submission results store that id in state.result.

Sequence Diagram(s)

Postgres precision path

sequenceDiagram
  participant _pg_execute_submit_action
  participant clean_sqls_like_ex_base
  participant preprocess_rows_like_ex_base
  participant _compare_pg_rows_2dp
  _pg_execute_submit_action->>clean_sqls_like_ex_base: strip ROUND(...) from agent and gold SQL
  _pg_execute_submit_action->>preprocess_rows_like_ex_base: normalize pred_rows and gold_rows
  _pg_execute_submit_action->>_compare_pg_rows_2dp: compute p1 from normalized rows
  _compare_pg_rows_2dp-->>_pg_execute_submit_action: p1 and reward
Loading

Audited best-of fallback

sequenceDiagram
  participant submit_raw_sql
  participant _dispatch_eval
  participant evaluate_dual_gold
  participant evaluate_best_of_audited_variants
  participant execute_submit_action
  participant state.result
  submit_raw_sql->>_dispatch_eval: phase-1 evaluation
  _dispatch_eval->>evaluate_dual_gold: evaluate audited primary and original
  _dispatch_eval->>evaluate_best_of_audited_variants: fallback when audited primary misses
  evaluate_best_of_audited_variants->>execute_submit_action: try each audited variant SQL
  execute_submit_action-->>evaluate_best_of_audited_variants: p1 / observation
  evaluate_best_of_audited_variants-->>_dispatch_eval: matched_variant_id
  _dispatch_eval-->>submit_raw_sql: p1 / reward / observation
  submit_raw_sql->>state.result: phase1_matched_audited_variant_id
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • MotleyAI/bird-interact-agents#22: Modifies _dispatch_eval and phase-1 audited/original scoring plumbing, which this PR extends with a best-of audited-variant fallback.
  • MotleyAI/bird-agents#30: Adds grouped audited-variant parsing and overlay groundwork that feeds the audited-variant data used by this PR.
  • MotleyAI/bird-agents#46: Updates tolerant-grader normalization and upstream ex_base integration that this PR builds on for row and SQL normalization.

Poem

🐇 I hopped through rows by moonlit glow,
and found the 2dp answers below.
Audited variants lined up neat and small,
with matched ids tucked out of sight from all.
Carrot crumbs and SQL dreams—what a fine show!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: aligning in-task and cascade grading with ex_base behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@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 (2)
src/bird_interact_agents/agents/_submit.py (1)

429-429: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update _dispatch_eval’s tuple contract docstring.

Line 429 adds a 10th return value, but the docstring still documents only nine elements. Please include matched_variant_id so future callers don’t unpack the helper incorrectly.

🤖 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 `@src/bird_interact_agents/agents/_submit.py` at line 429, The `_dispatch_eval`
tuple contract docstring is out of sync with the helper’s return values and
still lists only nine items. Update the docstring in `_submit.py` to document
the new `matched_variant_id` element as the 10th return value, using the
`_dispatch_eval` symbol so callers can unpack it correctly.
tests/test_pg_submit_precision.py (1)

123-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the agent-side ROUND(...) stripping too.

This test records rec["pred_sql"], but the submitted SQL already has no ROUND(...), so line 216 in _pg_execute_submit_action can regress without failing. Make the predicted query use ROUND(...) as well and assert both recorded streams were cleaned.

Suggested tweak
-    obs, reward, p1, p2, finished = harness._pg_execute_submit_action(
-        "SELECT x AS m FROM t", status, "/tmp/ignored",
+    _obs, reward, p1, _p2, _finished = harness._pg_execute_submit_action(
+        "SELECT ROUND(x, 2) AS m FROM t", status, "/tmp/ignored",
     )
+    assert all("ROUND" not in s.upper() for s in rec["pred_sql"])
     # remove_round stripped ROUND from the executed gold SQL.
     assert all("ROUND" not in s.upper() for s in rec["gold_sqls"])
🤖 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_pg_submit_precision.py` around lines 123 - 147, The test only
verifies ROUND stripping on the executed gold SQL, so regressions in the
agent-side cleanup inside _pg_execute_submit_action can slip through. Update
test_pg_execute_submit_action_strips_round_and_uses_2dp to submit a predicted
query that also contains ROUND(...), then assert both rec["pred_sql"] and
rec["gold_sqls"] have ROUND removed after _pg_execute_submit_action runs, while
keeping the 2dp normalization assertion intact.
🤖 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/agents/_submit.py`:
- Around line 600-603: The raw dry-run failure path in _submit.py is leaving a
stale phase1_matched_audited_variant_id in prior because it spreads prior
without resetting that diagnostic. Update the dry-run failure branch in the
submission flow so it explicitly clears phase1_matched_audited_variant_id
whenever phase1_passed is set to False, while keeping the evaluated-submission
path that writes matched_variant_id unchanged.

In `@src/bird_interact_agents/eval/tolerant_grader.py`:
- Around line 442-446: The relaxed string comparison in compare_relaxed should
use Unicode-aware case folding instead of lowercasing when casefold is enabled.
Update the string normalization branch in tolerant_grader.compare_relaxed so the
casefold path calls str.casefold() on both inputs, preserving the existing strip
behavior and keeping the rest of the matching logic unchanged.

In `@src/bird_interact_agents/harness.py`:
- Around line 896-900: The legacy single_file path in harness.py is attaching
all rows for an instance_id without checking whether they belong to the same
selected_database or benchmark, which can leak foreign SQL into
task["audited_variants"]. Update the logic around flat_rows_by_iid /
variants_by_iid so the list is filtered to the current primary row’s
database/benchmark before assigning audited_variants, and apply the same
safeguard in the later single_file handling block near the other referenced
section.

---

Nitpick comments:
In `@src/bird_interact_agents/agents/_submit.py`:
- Line 429: The `_dispatch_eval` tuple contract docstring is out of sync with
the helper’s return values and still lists only nine items. Update the docstring
in `_submit.py` to document the new `matched_variant_id` element as the 10th
return value, using the `_dispatch_eval` symbol so callers can unpack it
correctly.

In `@tests/test_pg_submit_precision.py`:
- Around line 123-147: The test only verifies ROUND stripping on the executed
gold SQL, so regressions in the agent-side cleanup inside
_pg_execute_submit_action can slip through. Update
test_pg_execute_submit_action_strips_round_and_uses_2dp to submit a predicted
query that also contains ROUND(...), then assert both rec["pred_sql"] and
rec["gold_sqls"] have ROUND removed after _pg_execute_submit_action runs, while
keeping the 2dp normalization assertion intact.
🪄 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: 45933ada-c73f-44e9-bb5e-a47d6458e114

📥 Commits

Reviewing files that changed from the base of the PR and between c980a73 and 350ed90.

📒 Files selected for processing (8)
  • src/bird_interact_agents/agents/_submit.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • src/bird_interact_agents/eval/upstream_ex_base.py
  • src/bird_interact_agents/harness.py
  • tests/eval/test_tolerant_grader_composition.py
  • tests/eval/test_tolerant_grader_precision.py
  • tests/test_in_task_best_of_variant.py
  • tests/test_pg_submit_precision.py

Comment thread src/bird_interact_agents/agents/_submit.py
Comment thread src/bird_interact_agents/eval/tolerant_grader.py Outdated
Comment thread src/bird_interact_agents/harness.py
- _submit.py: clear stale phase1_matched_audited_variant_id on the raw
  dry-run failure branch (was carried forward via the **prior spread);
  document the new 10th _dispatch_eval tuple element.
- tolerant_grader.py: use str.casefold() (Unicode-aware) instead of
  lower() in the relaxed cell predicate.
- harness.py: filter legacy flat single_file best-of variants by the
  primary row's (selected_database, benchmark) so a same-instance_id row
  from another DB/benchmark can't leak foreign SQL into best-of grading.
- Regression tests for the stale-id clear, the foreign-SQL filter, and
  the Unicode casefold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ZmeiGorynych
ZmeiGorynych merged commit 2cfd5cf into main Jun 26, 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