Repository navigation
feat(agent): read this turn's edits back before the answer is sealed - #4866
kovtcharov-amd wants to merge 2 commits into
Conversation
A tool result saying `"status": "success"` is not the same as a file that is still on disk, still has content, and still parses. Today the only thing that closes that gap is the model choosing to check — and whether it does is a property of the model, not of the loop: given the same tools and the same prompt, some read their work back every time and some never do. The loop is better placed to do it. It already records which files this turn wrote, so just before the answer is sealed it now opens each of them and asks three questions the tool result cannot answer: * is it still there, * does it still have content, * and, for Python and JSON, does it still parse. If anything comes back wrong, the findings go into the context and the turn gets two steps to fix them. Those two steps are granted on top of `max_steps`, not taken out of it, and that is the part that matters. Verifying currently costs steps from the same budget as working, so a model under budget pressure is right to skip it — and the pressure peaks at the end of a turn, which is exactly where the edits are. Making the repair free removes the incentive to look away. "Which files did this turn write" is read from the completion-evidence ledger, which every write tool feeds. `_turn_file_edits` looks like the obvious source and is not: it is appended only for a tool result carrying `operation == "edit_file"`, and nothing in `file_io_tools` sets that field, so reading it would make the check silently do nothing for every shipped file tool. There is a test driving the real tools through the real loop specifically to pin that. Deliberately narrow: read-only, in-process, no shell, no subprocess, no repository walk. It cannot be slow in proportion to the repository and cannot touch anything the turn had not already touched, so it is safe to run every time. A file type it cannot parse is checked for existence and content and otherwise left alone, rather than having a syntax guessed at for it. Two rounds per turn, after which the turn is looping and the findings go into the answer instead. `GAIA_AGENT_VERIFY_EDITS=0` turns it off. This does not tell you whether a change is *correct* — nothing that needs no configuration could. It tells you the change is not obviously broken, which is the failure the loop can see by itself. Two existing loop tests opt out of the check rather than absorb it. Both register write tools that report success without touching the disk, so the read-back correctly finds the file they named missing and spends a repair step saying so; one pins the loop's exact model-call count and the other scripts a fixed list of replies that the extra call exhausts. Neither is about writing files, so the honest fix is to turn the check off for them and say why, not to weaken it. - [x] `pytest tests/unit/test_post_edit_verification.py` - 28 passed; covers a missing file, an emptied file, broken Python and broken JSON, the free repair steps not counting against `max_steps`, the two-round ceiling, an unparseable file type being left alone, and the env knob - [x] `pytest tests/unit/agents` - 2312 passed, 53 skipped - [x] `python util/lint.py --black --isort` - [ ] **Eval (LLM-affecting):** the findings enter the conversation and can add turns, so this wants an eval run before merge. Compare against the same run with `GAIA_AGENT_VERIFY_EDITS=0`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…orts
The post-edit read-back took its file list from the completion-evidence
ledger, and that ledger records more than edits: it also holds the outputs
of tools that generate files (`generate_image`, `text_to_speech`,
`take_screenshot`, ...) and paths an executor reported. So a turn that
generated an image had the image path read back as if the model had edited
it; when the path was not a readable text file the check reported a
problem and spent a repair round, and the model was asked to "fix" a file
it never edited.
The ledger now marks an entry `edited` when its last writer was one of the
file-editing tools in `WRITE_TOOLS`, and the read-back only considers those.
A later change by anything else replaces the entry, so a file that a script
rewrote or deleted on purpose after its edit is not reported as broken.
The read-back itself is tightened along the same lines:
* binary files are skipped, by extension or by a NUL byte in the content -
"is it empty" and "does it parse" are text questions;
* reads go through the agent's read boundary without prompting, so a path
the model could not read is not read here either;
* a file over 2 MiB is checked for presence only, so the check cannot get
slow in proportion to what was written.
- [x] `pytest tests/unit/test_post_edit_verification.py` - 36 passed; the
new tests cover a generated image (unit and through the real loop with
the check on), a read-only tool, an executor output, a file deleted on
purpose, binary content and a path outside the read boundary. Five of
them fail without this change.
- [x] `pytest tests/unit/agents/test_image_outcome_guard.py` - 8 passed
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Verdict: Request changes Before the agent gives its final answer, it now reads back the files it edited during the turn. If a file is missing, empty, or no longer parses as Python or JSON, the agent gets two extra steps to fix it, and those steps don't count against its step budget. The design is sound and well tested. Three things need fixing before merge:
Real-world evidenceN/A: this changes the agent loop internally and adds no CLI, API, or MCP surface, so the evidence bundle has nothing to capture. The verdict rests on the code review and the PR's unit-test results. How the change affects model behaviour is still unmeasured until the eval runs. 🔍 Technical details🟡 Findings that outlive the round cap are dropped (
|
|
Holding until v0.25.0-rc1 is tagged: this changes agent behavior, and the release eval now running (https://github.com/amd/gaia/actions/runs/37436249503) measures main at 9ce6598 without it. It merges right after the tag, and the next eval will measure it. |
Branch:
feat/verify-edits-before-the-answer-is-sealedBase:
e0fd6717f609a5be7ca8676a6be2c869fd4fe4b2(e0fd6717f)Commits:
bdfc3402885aedfb8fc3488a3aaa85da3ae2c3ab836b57016695d59db4362ade466d60eb032d376fRelationship to #4719 and #4745
#4719 ("keep the read-back check out of the user's answer") touches a
model-facing prompt string inside the completion check, and only fires when
there is an artifact gap — the model claimed a save the evidence ledger cannot
corroborate. It tells the model not to narrate that read-back in its reply.
This change is a different mechanism answering a different question: an
in-process, read-only check the loop runs over the files the turn actually
wrote, whenever it wrote any. It does not touch #4719's prompt string, does not
re-instruct the model to read anything, and does not reintroduce the narration
#4719 removed — its findings only enter the conversation when something is
wrong. The two compose; neither is redundant.
#4745 ("keep the answer a model sends alongside its last tool call") requires
the promoted answer to be in place before the seam's checks run. It is: the
promotion happens earlier in
_process_query_implthan this check, so theread-back still runs against exactly what the user will see.
The problem
A tool result saying
"status": "success"is not the same as a file that isstill on disk, still has content, and still parses.
Today the only thing that closes that gap is the model choosing to check — and
whether it does is a property of the model, not of the loop. Given the same
tools and the same prompt, some models read their work back every time and some
never do. That means the behaviour disappears the moment the model changes,
which is not a property anyone can build on.
The change
The loop is better placed to do it. It already records which files this turn
wrote, so just before the answer is sealed it opens each of them and asks three
questions the tool result cannot answer:
If anything comes back wrong, the findings go into the context and the turn gets
two steps to fix them.
The free steps are the load-bearing part
Those two steps are granted on top of
max_steps, not taken out of it.Verifying currently costs steps from the same budget as working, so a model
under budget pressure is right to skip it — and the pressure peaks at the end of
a turn, which is exactly where the edits are. Making the repair free removes the
incentive to look away. Charging for it would make this feature self-defeating.
Design notes
Where "which files did this turn write" comes from. The completion-evidence
ledger, which every write tool feeds.
_turn_file_editslooks like the obvioussource and is not: it is appended only for a tool result carrying
operation == "edit_file", and nothing infile_io_toolssets that field, soreading it would make the check silently do nothing for every shipped file tool.
There is a test driving the real tools through the real loop specifically to pin
that.
Deliberately narrow. Read-only, in-process, no shell, no subprocess, no
repository walk. It cannot be slow in proportion to the repository and cannot
touch anything the turn had not already touched, so it is safe to run every
time. A file type it cannot parse is checked for existence and content and
otherwise left alone, rather than having a syntax guessed at for it.
Bounded. Two rounds per turn, after which the turn is looping and the
findings go into the answer instead of driving another repair.
GAIA_AGENT_VERIFY_EDITS=0turns it off.What this is not
This does not tell you whether a change is correct — nothing that needs no
configuration could. It tells you the change is not obviously broken, which
is the failure the loop can see by itself. Judging correctness needs the
repository's own tests, which is a different and much larger piece of work.
Two existing tests opt out
Both register write tools that report success without touching the disk, so the
read-back correctly finds the file they named missing and spends a repair step
saying so. One pins the loop's exact model-call count; the other scripts a fixed
list of replies that the extra call exhausts. Neither is about writing files, so
the honest fix is to turn the check off for them and say why in the fixture, not
to weaken the check.
Fix: only text-file edits are read back
The first version of this branch took its file list straight from the
completion-evidence ledger. That ledger records more than edits: it also holds
the outputs of tools that generate files (
generate_image,text_to_speech,take_screenshot, ...) and paths an executor reported. So aturn that generated an image had the image path read back as if the model had
edited it, found a "problem", and spent a repair round asking the model to fix
a file it never edited.
tests/unit/agents/test_image_outcome_guard.pycaughtit: two of its tests failed with an unexpected extra model call.
The second commit fixes the cause rather than the tests (which are unchanged):
editedwhen its last writer was one of thefile-editing tools in
WRITE_TOOLS, and only those are read back. Any laterchange by something else replaces the entry, so a file a script rewrote or
deleted on purpose after its edit is not reported as broken;
model could not read is not read here either;
New regression tests cover a generated image (unit, and through the real loop
with the check on), a read-only tool, an executor output, a file deleted on
purpose, binary content and a path outside the read boundary; five of them fail
without the fix.
Risk
The findings enter the conversation and can add turns, so this changes model
behaviour and wants an eval run before merge.
Test plan
pytest tests/unit/test_post_edit_verification.py tests/unit/agents/test_cut_off_reply_guard.py tests/unit/agents/test_parse_error_recovery.py tests/unit/agents/test_completion_evidence.py tests/unit/agents/test_read_file_line_ranges.py tests/unit/agents/test_image_outcome_guard.py tests/unit/eval/test_flagship_tasks.py::test_an_adversarial_task_rejects_the_shortcuton this branch alone — 251 passed, 2 skipped (
test_post_edit_verification.pyis 36 tests: a missing file, an emptied file, broken Python and broken JSON,
the free repair steps not counting against
max_steps, the two-roundceiling, an unparseable file type being left alone, the env knob, and the
over-reach cases above)
tests/unit/agents/test_image_outcome_guard.py— 8 passed, unmodifiedtests/unit— see belowpython util/lint.py --black --isortGAIA_AGENT_VERIFY_EDITS=0Full unit suite
tests/unit, Windows,--timeout=300, run on unmodifiedmain(e0fd6717f)and on a disposable build with all four open branches stacked (line endings in file tools, step-budget notices, post-edit
read-back, model-endpoint retry):
maine0fd6717fThe same five tests fail on both, and none of them is touched by this branch:
three installer TUI-detection tests,
test_file_access_scope.py::test_no_allowlist_still_defaults_to_cwd,and
test_lemonade_asr.py::TestRealServer::test_real_transcription_round_trip,which needs a live server. No test fails on the stacked build that passes on
main.Stacking with other open branches
Applies cleanly to
mainon its own. It conflicts textually with the step-budget notices branch (feat/tell-the-model-how-much-step-budget-is-left), whichever lands second: both add an env-knob function after_TOOL_USER_WAITand per-turn state in__init__and at the top of_process_query_implinsrc/gaia/agents/base/agent.py. The resolution is to keep both sides — two separate functions and both sets of fields.🤖 Generated with Claude Code