Agent: I’ve fixed the failing test.
pytest:
FAILED tests/test_invoice.py::test_total — assert 4200 == 4300
Nothing went wrong in any way your monitoring can see. The agent made a plausible change, wrote a plausible summary, and returned success. Anything downstream that trusts the summary — a ticket transition, a CI step, a metric, a human skimming the thread — now believes the work is done.
This is not a hallucination problem you can prompt away. A confident summary of work that did not happen is exactly as plausible as one of work that did, because plausibility is what the model optimises. The only fix is to stop asking it.
TL;DR
the agent says done -> an INDEPENDENT verifier checks -> pass,
or the actual failure goes back as the next instruction
Two properties make a verifier worth having:
- Independence. Running
pytestis independent. Asking the same model “did you do a good job?” is theatre — it shares the failure mode you are trying to catch, and it will confidently agree with itself. You will have built a very expensivereturn True. - Determinism, where you can get it. Tests, compilers, linters, schema validators either pass or fail. Prefer them absolutely. An LLM judge is the fallback for what you cannot automate, and it belongs behind a label so nobody mistakes it for a fact.
OWASP: ASI09 —
the benign, everyday form of human-agent trust exploitation.
Runnable code:
patterns/harness/verification_loops/
The loop, as program output
uv run python -m patterns.harness.verification_loops.demo
=== WITHOUT the pattern ===
agent reports: I've fixed the test.
tests actually pass: False
^ recorded as a success by anything that trusts the agent's summary
=== WITH the pattern ===
attempt 1: REJECTED — I've fixed the test.
completion-claim: FAILED tests/test_invoice.py::test_total
attempt 2: accepted — Fixed — the discount is now applied before tax
verified: True after 2 attempts
tests actually pass: True
what attempt 2 was actually told:
You reported the work as complete, but verification disagrees:
FAILED tests/test_invoice.py::test_total
AssertionError: assert 4200 == 4300
(tax is applied before the discount)
Do not report completion again until the check passes.
=== deterministic verifiers and judges are not the same thing ===
pytest ok=True deterministic
customer-tone ok=False NON-deterministic (evidence)
The pattern
loop = VerificationLoop(
verifiers=[
CommandVerifier("pytest", run_pytest),
CommandVerifier("ruff", run_ruff),
ClaimVerifier(evidence=run_pytest), # catches "it's fixed" when it isn't
],
max_attempts=3,
)
outcome = loop.run(harness_factory, "Fix the failing invoice total test", ctx_factory)
1. This is an outer loop, not a hook. Verification runs after the run completes; failure starts a new run. A hook that rewrote the final answer could hide the failure — re-running makes the retry visible, budgeted, and auditable.
2. The feedback is the failure, not a scolding. Attempt two receives the actual assertion, the file, and the line, plus “do not report completion again until the check passes.” An error message is a prompt; “please try harder” is not one.
3. Attempts are budgeted, and exhaustion is honest. After max_attempts the outcome
is verified=False with every attempt’s failures retained. An unverified result must
never be silently returned as success — that is the original bug with extra steps.
4. All verifiers must agree, and failures are collected together so one attempt can fix everything rather than discovering the linter after the tests.
When to use it
- Coding agents, unconditionally. You have a test suite; use it as the completion signal instead of the model’s opinion.
- Any task with a machine-checkable definition of done: schema conformance, a successful build, a query that parses, numbers that reconcile.
- Before wiring an agent into anything automated. If “done” closes a ticket or advances a pipeline, something must have earned that word.
- Whenever you are about to write an eval. The verifier and the eval assertion are usually the same predicate — build it once.
When NOT to use it
- Genuinely subjective outputs. There is no verifier for “write a nicer apology email.” A judge here adds cost, latency, and false confidence; a human review step is the honest answer.
- When the verifier is weaker than the agent. A shallow check teaches the agent to
satisfy the check. The agent that makes
test_totalpass by editing the test has verified successfully and helped nobody. Verify behaviour, keep the verifier out of the agent’s write scope, and treat a suspiciously fast pass as a signal. - Cheap, reversible, human-reviewed work. If a person reads every output anyway, they are the verification loop.
- Do not retry a deterministic failure indefinitely. If the same input produces the same wrong answer, more attempts burn budget for nothing. Cap attempts low (2–3) and escalate — retrying forever is how a verification loop becomes the runaway from cost budgeting.
Trade-offs and failure modes
- Cost multiplies by attempts. Three attempts is up to 3× tokens and wall clock. Count attempts as a quality metric: a rising retry rate is a prompt or tooling problem, not a verification problem.
- Full re-runs lose intermediate work. This implementation restarts rather than resuming from the failure point. The cheaper version checkpoints and resumes — at the cost of the agent keeping context that already led it astray.
- Verifiers become an attack surface. A verifier the agent can modify is not a verifier, and an agent with filesystem access can modify your test file. Run checks from outside the agent’s sandbox, on a clean checkout.
- Flaky verifiers are worse than none. An intermittently failing suite turns this
into a random retry generator and trains your team to ignore
verified=False. Fix flakes before adopting this. - Judges drift. An LLM judge’s threshold shifts with model versions, so a loop that passed last month may fail today for reasons unconnected to the work. Version-pin judges and evaluate them against labelled data.
- Verification cannot tell you the task was the right one. It confirms the stated goal was met. If the goal was wrong, all three attempts verify perfectly and the outcome is still useless.
Frequently asked questions
Is this just the evaluator-optimizer pattern?
It is that pattern with a specific, opinionated constraint: the evaluator must be independent and preferably deterministic. The generic formulation allows an LLM evaluator, which is where most implementations quietly lose the property that makes the loop worth running.
Can the same model be the verifier if I use a different prompt?
No. A different prompt does not create independence — the same weights, the same training, and often the same context produce correlated errors. If the only available verifier is a model, use a different model, accept it is evidence rather than proof, and label it as such.
How do I verify something that has no test?
Write the smallest deterministic check that would have caught the last failure of this kind. A schema validation, an invariant, a reconciliation, an exit code. If you truly cannot express one, that is a useful finding: it means “done” is undefined for this task, and the agent was never going to be reliably right.
Where does this run — inside CI, or in the agent?
Both, for different jobs. Inside the agent’s loop it converts a failure into a retry. Inside CI it converts a failure into a blocked deploy, which is the eval gate.
References
- OWASP GenAI Security Project, Top 10 for Agentic Applications 2026 — ASI09
- Anthropic, Building Effective Agents — the evaluator–optimizer workflow and verification as a harness responsibility
- OpenAI, Agentic Governance Cookbook — eval-driven validation with precision and recall against labelled data
- Runnable code and tests:
patterns/harness/verification_loops/
Part of the agent harness and governance series. Next: tool design — the mistake that costs 17× more tokens than it needs to.