The original transcript had a specific shape: the agent asked for a missing year, the user answered on the next turn, and nothing in the system knew a create was pending. The obvious fix, giving the model a tool to record what it's collecting, has the weakness of every “the model must remember to” rule.
So the draft is derived. A mutating call that fails on missing input (a validation error such as “Field required”) automatically becomes a pending entry in the conversation's state, and a later success on the same section clears it. The model never maintains it; it is shown a <pending> block at the top of every call. That state drives the one guard that runs when the model tries to finish:
async def check(self, ctx, draft):
if draft.had_successful_mutation:
return ACCEPT
pending = (ctx.state or {}).get("pending_entry")
if not pending:
return ACCEPT # nothing started, nothing to misreport
if any(c.mutating for c in draft.calls):
return ACCEPT # it tried, failed, and has the error
if pending.get("corrected_once"):
return ACCEPT # one confrontation per draft
await self._mark_corrected(ctx, pending)
return Correct(
StateReport.render(draft.calls)
+ "<unfinished>" + json.dumps(pending)
+ "This entry is still not in the CV. Nothing was written this "
"turn. Either call the tool that writes it now, ask the user "
"for what is missing, or — if the user cancelled this entry — "
"call `discard_pending_entry`. Do not describe it as saved."
+ "</unfinished>")
Each condition was earned. The guard never fires on a turn that attempted a write: that turn already has the error and the report in front of it, and in practice answers honestly. It fires at most once per draft because, measured live, a pending draft plus a chatty user meant a correction round-trip on every turn, about 12,500 extra input tokens each time. When the user cancels, the model calls discard_pending_entry, which has a read-after-write verifier like every other mutating tool. And when the guard does fire, the text the user already saw stays on screen; the correction arrives as a visible continuation, so the stored reply equals what was streamed, character for character.