What it does
A researcher agent gathers sourced facts about a company. A drafter turns those facts into a short outreach email and an account brief. A reviewer then checks the draft against the researcher's claims and scores it out of ten. Nothing below an 8 reaches a human.
The interesting constraint is the last one. If a draft fails, it goes back for revision. If it fails again, the retry budget is spent and the work escalates to a human queue rather than being released at a lower standard. The system is built so that "not good enough" is a permitted outcome — most agent chains I've seen will always hand you something.
How it's wired
- ContractsEach agent is a skill definition with a strict JSON schema on its output. A stage that returns malformed data fails loudly instead of passing garbage downstream.
- ResearcherRead-only. Fetches and returns claims with real source URLs attached — never prose the next stage has to trust.
- DrafterWrites the outreach email and account brief from the researcher's claims only.
- ReviewerCompares the draft line by line against those claims, flags anything unsupported or generic, and scores it.
- GateScore of 8 or higher releases. Below that, revise. Two consecutive blocks, or an exhausted retry budget, escalates.
- RecordEvery attempt — brief, drafts, verdicts, outcome — is written to disk as an audit trail.
Incident log
Four of these were found in a single live debugging session, and the fifth in an instrumentation audit afterwards. The fifth is the one I'd want to talk about.
The researcher was inventing its sources
Runs completed and produced claims with plausible-looking source URLs attached. The URLs did not go anywhere.
The researcher's skill definition required a web search tool. No such tool had ever been wired into the runtime. Asked for sourced claims and given no way to fetch anything, the model produced sourced-looking claims instead.
This is the failure that undermines the entire premise. The reviewer's whole job is checking the draft against the researcher's facts — if those facts are fabricated, the quality gate is validating one invention against another and confidently passing it.
Wired the search tool in properly. Later runs produce claims with source URLs that resolve, which is what makes the reviewer's verdict mean anything.
Schema validation would have failed every live call
Live runs failed with a 401 that I initially chased as an authentication problem.
Two separate faults stacked. The API key had only ever been set in a single terminal session and was never persisted, so any new process had no credentials. Underneath that, required fields in the run metadata were never being populated — meaning that even with a valid key, every live call would have failed schema validation anyway.
The visible error pointed at the wrong layer. Fixing the key alone would have produced a new failure and the appearance of a second unrelated bug. Worth remembering that the first error you see is often not the first error that happened.
Key persisted at user scope; metadata populated at run construction. Also raised the token ceiling for the drafter and reviewer, which extended reasoning had been quietly consuming, and removed a print helper that was dead code.
The best run I ever had was not saved
A run escalated exactly as designed — the reviewer flagged real drift from the sourced claims, the revision hit the same flag, the retry budget ran out, and the work went to a human queue instead of shipping. It was the single best demonstration the system had produced. The claim text, draft bodies, and reviewer flag detail were all gone.
An audit found three independent causes, any one of which alone would have lost the data. The audit function only printed to the console and never wrote to disk. The serializer deliberately stripped the brief, draft, and verdict to keep the run log small. The draft and verdict variables were overwritten on each retry, so earlier attempts were already gone from memory. And the disk write itself sat inside a single except branch — so when the process died from credit exhaustion rather than a contract error, persistence was skipped entirely.
Every one of those decisions was locally reasonable. Keeping the log lean is sensible. Reusing a variable is normal. Handling the error you expect is what you're taught. Together they meant the system had no durable memory of its own best behaviour, and I didn't find out until the moment I needed it.
The run record now accumulates the brief once plus per-attempt drafts and verdicts. The entry point wraps in try / except ContractError / except BaseException / finally, so persistence happens on any exit path. A new interrupted outcome distinguishes the process dying from the four real gate decisions. Verified against a fully mocked agent call, no network required.
The lost run is unrecoverable without paying to re-run it, and I decided that wasn't worth it. The dashboard shows that run with an explicit "not captured" panel rather than backfilling it or quietly hiding the gap.
Known limits
- No live run has ever reached the released or halted outcome — those paths exist only under test fixtures. The escalation path has no fixture coverage at all, because none of the four fixtures reaches two consecutive blocks or an exhausted retry budget. I know this is the weakest part of the test suite.
- The researcher still occasionally overruns its own field-length limits on live calls. It's documented rather than fixed.
- Ten of the thirteen live runs are real but terse — they predate the instrumentation work and don't carry full detail.
- Further live runs are paused. The pipeline costs real money per execution and I stopped when I had what I needed rather than spending to pad the numbers.
What I'd do next
- Build a fixture that actually exercises the escalation path, since it's the behaviour I most want to claim and currently the least tested.
- Publish the dashboard and open the repo.
- Add a second vertical to prove the contracts hold when the domain changes.