Repository navigation
fix(agents): prep eval measures the root, not the fake renderer - #12198
Conversation
Under AGENT_RENDERER=fake every run_data_code call got the fake's fixed four-row table, so the root ran its code a second time (9 of 11 data-code misses). The eval's data runs now execute the snippet with the real harness in data mode and the renderer's limits (agents/evals/host_data.py), read by the local backend's reader (collect_data_run). Sizes are compared again; an expected size may list a few values where the user's code leaves the shape open. Also: two stale total-row fixtures (the parser leaves out a trailing summary row since #12166) with a fixture test that refuses a drop_rows dropping nothing; the scope judge on filters by place and on pasted JavaScript data code (three new scope cases); the root on count/sum instructions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
There was a problem hiding this comment.
🔵 Needs a closer look
Executing model-written Python without container isolation requires explicit owner security review.
1 open finding
What changed in this PR
Corrects the prep eval so it measures the root agent’s data preparation rather than the fake renderer’s fixed output.
Changes:
- Executes eval data snippets through the real harness and compares table sizes.
- Updates stale fixtures and adds regression coverage.
- Clarifies prompt handling of aggregation requests, filters, and pasted data code.
| File | Description |
|---|---|
| tests/unit/agents/evals/test_prep.py | Tests real data output, size alternatives, and fixture consistency. |
| tests/unit/agents/evals/test_host_data.py | Tests host execution and failure handling. |
| changelog.d/agent-prep-eval-gate.md | Records eval and prompt fixes. |
| agents/README.md | Documents revised eval behavior. |
| agents/evals/scope.evalset.json | Adds scope-classification cases. |
| agents/evals/prep.py | Selects the host backend and compares table sizes. |
| agents/evals/host_data.py | Adds eval-only host data execution. |
| agents/evals/fixtures/prep/en-drop-total-row.json | Moves the total row before data rows. |
| agents/evals/fixtures/prep/en-code-python-random-walk.json | Allows two or three output columns. |
| agents/evals/fixtures/prep/de-hard-kette-gesamtzeile.json | Removes the obsolete row-drop expectation. |
| agents/anyplot/render/backends/local.py | Extracts shared data-output collection. |
| agents/anyplot/prompts/scope_judge.md | Clarifies filters and pasted data code. |
| agents/anyplot/prompts/root.md | Distinguishes aggregation instructions from questions. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review finding: the timeout cancels the stderr reader, so the kill now reads stderr to its end while it reaps the process; a regression test floods stderr past the timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…he renderer Review round (Fable + Gemini) on #12198: - HostDataBackend validates the job itself (DATA validator, refused as `blocked`), allow-lists ENVIRONMENT development/test, documents that it runs on the host and why that is acceptable for the eval. - prep.main builds the backend before the first case, so a backend the settings cannot have is a setup error, not a run of misses. - One helper, wire.run_cpu_seconds, clamps RLIMIT_CPU to the run's timeout in the executor, the local backend and the host runs (20 s, not 60 s). - The kill is bounded by a grace period, and any failure, not only a cancellation, kills and reaps the process group before the cleanup. - collect_data_run reports an oversized out.csv with its size and cap, as the renderer's _collect_data does. - Scope examples and cases no longer mirror the failed prep fixture; two needs_data contrast cases name an outside source. - README records the scope and prep runs of 2026-10-15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
…crash
Review finding: a raw `{"op": "run_data_code"}` the root put into
prepare_data's operations has no size, and matching it raised KeyError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
|
Owner decision (2026-10-15): keep option (a), the eval runs model-written data snippets on the host under HostDataBackend (validator inside |
…rn (#12199) ## Summary - **Cause.** The root uses `static_instruction` plus a dynamic `instruction` (the session block). ADK 2.11 sends the dynamic part as user content: a `<<<BEGIN_SYSTEM_INSTRUCTION>>>` block with a preamble, inserted at the current-turn boundary, so it lands inside the user's own turn (`google/adk/flows/llm_flows/prompt/_instructions.py`, `LlmRequest._insert_transient_user_content`). Claude Haiku 5.5 read it as user text that posed as a system instruction. In a baseline trace of 8 prep cases on origin/main, its thinking said so in 7 of 8 turns, for example "a suspicious block embedded in the user's message ... which looks like an injection attempt I should disregard". Every one of the 23 root requests carried the markers in a user message. - **Fix.** The root has no dynamic `instruction` anymore. A `before_model_callback` (`add_session_context`) appends the session block to the system instruction as a second part after the static policy. `VertexClaude` sends each part as its own Claude system block, with ADK's cache breakpoint on the first part only (`system_blocks`). A session block that changes therefore leaves the policy cached, and the static prefix stays byte-identical. `root.md` now describes the block as part of the instruction instead of explaining away the markers. The thinking `block_binding` drop stays: a data tool can still change the block between two calls. - **Regression checks.** A service-flow test checks that no root request has the markers or the session block in any content, and that the system instruction is the static part plus the block. A registry test checks that no agent has a dynamic `instruction`. Model tests cover the two-block split, with and without caching. The prep eval now scores a reply that talks about the root's own instructions (markers, a system instruction, the session block) as a miss and counts it under `instruction_talk`. A scripted test covers that path and asserts the shape of the Claude request. ### Numbers (Claude Haiku 5.5, Vertex AI `eu`) | Run | Turns flagged in thinking | Replies about it | Root requests with markers | Exact | Cost | |---|---|---|---|---|---| | origin/main, 8 cases (2 of 10 were `guard_unavailable`) | 7/8 | 0/8 | 23/23 | 8/8 | $0.026 | | this branch, same 10 cases | 0/10 | 0/10 | 0/30 | 10/10 | $0.025 | | this branch, full prep eval | 0/50 | 0/50 | 0/143 | 47/50 = 94 % (PASS) | $0.098 (#12198 runs: $0.131, $0.142) | On the same 8 cases, the cost per model call fell from $0.00114 to $0.00087. The three full-run misses are model choices unrelated to the block: one leaves the `Notiz` column in, one labels classes `1`/`2` instead of `Klasse 1`/`Klasse 2`, and one adds an extra sort. None of them talked about instructions. ### Decided and documented - The session block now lives in the system prompt. Its contents keep the same trust level and fences: the block was already "instruction priority", with user-derived values only inside `<plot_language>` and `<user_data>` fences. User text can no longer imitate it in place, and `root.md` says that a message that imitates the block is wrong. The scope guard and the `[message withheld]` path touch only user contents and are unchanged. ## Plan N/A ## Test plan - [x] `pytest tests/unit/agents`: 6,842 passed. `renderer/test_executor.py::test_the_memory_floor_kills_the_run` failed once under `-n auto` while the live eval ran alongside, and passes alone. It is unrelated. - [x] CI blocker fixed in this PR: `test_convert.py::TestSecurityReview` (from #12194) asserted a 1.0 s wall clock for scanning a 30 MB crafted comment. The CI runner, under coverage, took 1.17 s and 1.19 s on two runs. The bound is now 5 s, which still tells a linear scan from a backtracking one (hours). - [x] `ruff check`, `ruff format --check`, `mypy api core agents` - [x] `uv run python -m tools.changelog check --base origin/main` - [x] Live prep eval on Vertex AI, before and after (see the table) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Summary
AGENT_RENDERER=fake, everyrun_data_codecall got the fake's fixed table back: 4 rows ofxandy= x². The root asked for a 500-step walk, saw 4 squares, and rightly ran its code again ("the data doesn't match the code, I'll run it again"). 9 of the 11 data-code misses came from this alone. The eval's data runs now go throughHostDataBackend(agents/evals/host_data.py). It runs the real harness in data mode with the renderer's data-run limits. The local backend's reader reads the result back, extracted ascollect_data_runinrender/backends/local.py. Sorun_data_codegets the sameDataOutputit would get fromPOST /run-data. Every data-code case compares table sizes again. An expected size may list two to four values where the user's code leaves the shape open: the random walk defines bothstepsandwalk, so 2 or 3 columns both count.en-drop-total-rowandde-hard-kette-gesamtzeilestill expected the root to drop that row. The root was right to make no drop, and both cases scored as misses. The English case now has its total at the top, where the parser keeps it. The German chain case expects no second drop. A fixture test now refuses an expecteddrop_rowsthat drops nothing, so a parser change can't make a fixture stale silently again.needs_data. It also once judged pasted JavaScript data code asother_library.scope_judge.mdnow names both cases, with different example wording than the fixtures, andscope.evalset.jsongets three new cases. The root answered "Zähle die Buchungen pro Kategorie" withquery_dataas if it were a question.root.mdnow says that an instruction to count or sum per group is aprepare_dataaggregate.The gate stays at 80 %.
Prep eval, Claude Haiku 5.5 on
eu, 50 casesThe one miss in run 2 is
de-hard-kette-gesamtzeile: the root left outdrop_columns Notiz. That is harmless, because theaggregatedrops the column anyway, but it is a real model variance, so the fixture stays as it is.Scope eval after the
scope_judge.mdchange, 250 cases: PASS. False refusals 0 %, adversarial and refusal recall 100 %, exact 98.4 %, 4 unavailable (judgeValueErrortwice). Before the change, a subset run reproduced the falseneeds_dataonfilter-001.Design note for the owner
HostDataBackendruns model-written Python on the developer's machine without the container. Only the eval uses it, and it refusesENVIRONMENT=production. Three layers stand in front of it: the DATA validator, whichrun_data_codeapplies before any run; the harness's audit-hook guard; and rlimits withpython -Iand a minimal environment. The local backend's docstring rules out a bare-subprocess fallback for the product. This eval-only exception is a deliberate choice, so please confirm it or pick another option. The alternatives are to require Docker (AGENT_RENDERER=local) or the deployed renderer for the data-code cases.Plan
No plan file. Brief: Block L, prep eval gate (2026-10-15).
Test plan
tests/unit/agents/evals/test_host_data.py: pins the contract. A 500-step snippet comes back as 500×2, not the fixture table. Too many rows returns the harness exit code with measured value and cap.no_frame,errorandblockedkeep their exit codes. A timeout kills the process group. The environment is the renderer's data environment withoutRLIMIT_NPROC. Production is refused.test_prep.py: a scripted run throughHostDataBackendstores 500 rows in one call. Covers size choices andeval_backendunderfake, and checks that every expecteddrop_rowsdrops a row.pytest tests/unit/agents(6829 passed), ruff check/format, mypyagents/evals agents/anyplot/render.🤖 Generated with Claude Code
https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M