Agentic AI
How do you stop an AI agent from doing the same thing twice?
Three layers of an agent retry on their own and only the model call is safe to repeat. The fix is a read before every write, not a better retry policy.
Unity Horizon6 min read
You read the target's own record before every write, and you treat that read as the receipt. Three layers of an agent retry on their own, and only the top one is safe to repeat. The other two touch the world.
The mechanism is the gap between a side effect landing and its confirmation coming back. Ordinary software has that gap. An agent widens it, because the confirmation the model reads is a sentence you wrote, and a rendered page is not a status code.
Which layer of an agent actually retries?
| Layer | What it repeats | Who decides | Documented default |
|---|---|---|---|
| HTTP client in the SDK | the model API call | your SDK | Twice, with exponential backoff, on connection errors, rate limits and 5xx |
| The model | the tool call | the model, reading your error text | Two to three attempts with corrections before it gives up |
| The orchestrator | the run, from a checkpoint | your code | Whatever you wrote |
Row one is documented by Anthropic, whose SDKs "automatically retry transient failures ... twice by default", and by the OpenAI Python library, which retries connection errors, 408, 409, 429 and 5xx the same number of times. Row two comes from Anthropic's own tool use guide: after an error result, "Claude will retry 2-3 times with corrections before apologizing to the user."
Do the arithmetic on the defaults alone. Three attempts from the model, and one orchestrator retry on top of them, is six touches of the same intent before anyone in your company has written a line of retry logic. Row one is harmless. Rows two and three spend money.
Why is repeating an API call safe and repeating a tool call dangerous?
RFC 9110 section 9.2.2 draws the line and has drawn it since 2022. A method is idempotent when the intended effect of several identical requests is the same as the effect of one, and a client "MAY automatically repeat a request with an idempotent method if a communication failure occurs before the client is able to read the server's response." GET, HEAD, PUT, DELETE and OPTIONS qualify. POST does not, and every write your agent performs is a POST or a button.
So the automatic retry in your SDK is safe for a reason that has nothing to do with agents. Calling the Messages API books nothing and files nothing. It charges you and stops there, while the tool call reaches into somebody else's database. And the tool_use block carries a fresh id on every attempt, so the identifier names the request and never the intent, and nothing in the loop can recognise the second attempt as the same piece of work.
The error message says the request failed. It does not say the work did not happen.
What does a payments API do that your tool does not?
It issues the receipt itself. Stripe's idempotency layer saves the status code and body of the first request made under a given Idempotency-Key, including 500s, and returns that same stored result to every later request carrying it. Keys are pruned after 24 hours. If the parameters differ from the original, the request errors rather than quietly doing something new.
Read that as a design brief instead of a feature. The party that owns the state is the party that can tell two attempts apart, so it does the deduplication and the caller is allowed to be dumb about retries. Stripe made the retry someone else's problem. A tool you write has to make it yours.
What do you do when the target system has no idempotency keys?
Read before you write, every time, against the target's own record of what exists. That read is the receipt the system refuses to issue. It is also the only thing in the loop your agent did not produce itself.
Derive the identifier from stable business facts, the property and the arrival date, rather than from a UUID minted inside the run. A random identifier survives a retry inside one run and tells you nothing on the next one. That is the case that files the same declaration twice, a week apart, with a shift change in between.
Write the intent to your own log before acting and reconcile afterwards. A log written after the click records only the attempts that came back.
We built OFFSET, which files declarations directly to the ΑΑΔΕ portal, and the portal issues nothing an agent can quote back. The same pattern under a browser is what that build runs on: record the intent, log back in, verify against the portal's own submission listing, continue only then.
Does MCP settle which tools are safe to retry?
It names the property and refuses to stand behind it. The MCP schema defines idempotentHint, "if true, calling the tool repeatedly with the same arguments will have no additional effect on its environment", defaulting to false, alongside destructiveHint defaulting to true. Then the same file says all properties in ToolAnnotations are hints, "not guaranteed to provide a faithful description of tool behavior", and that clients "should never make tool use decisions based on ToolAnnotations received from untrusted servers."
The tools specification points the other way at the same time. Tool execution errors "contain actionable feedback that language models can use to self-correct and retry with adjusted parameters", and clients SHOULD hand them to the model. Retry after failure is the designed behaviour of the protocol, and the flag that says whether retrying is safe is advisory.
The field exists. Nothing enforces it.
What does a read before every write cost?
Less than the duplicate. A 1920x1080 screenshot costs 2,691 visual tokens on the high-resolution tier that covers Claude 4.7 and later models, because Claude bills images in 28 by 28 pixel patches. At Claude Opus 5's list price of $5 per million input tokens, that screenshot is $0.013. Three of them around one write is four cents. Prices and token counts checked by Unity Horizon on 8 September 2026 against Anthropic's pricing page and its vision documentation.
Set that against the work. Estimated, not measured: one short-term-rental declaration takes 4 to 7 minutes by hand, which at a EUR 14 per hour loaded cost is EUR 0.93 to 1.63 to do once. Unwinding a duplicate costs at least that again, plus the correction the target system demands, plus the phone call. Your numbers will differ.
Who wins here is decided by the systems you write into, not by the model you picked. A team whose targets hand back a booking reference or an invoice number gets deduplication almost free. A team whose only confirmation is a page rendered in a browser pays for a second session on every uncertain write, in every run, for as long as the integration lives. Vendors demonstrate the first attempt. Production is mostly the second one.
We have not measured how often a run lands in the ambiguous state per thousand writes, and the figures above are arithmetic over published list prices rather than a failure rate. If you have counted your own ambiguous writes, we would rather quote your number than estimate around it.
Which write in your agent has no receipt you can read back, and what query would tell you whether it already happened? If the honest answer is that nothing would, that is the kind of work we scope.
Common questions
- Why does an AI agent submit the same form twice?
- Because the tool result that reports a failure cannot tell the model whether the write landed. A timeout after the submit button reads exactly like a timeout before it, so the model tries again. Nothing between the model and your tool removes the duplicate.
- Are AI agent retries idempotent by default?
- No. The SDK retry is safe because a model API call creates nothing outside your own bill. The retry that matters is the model calling your tool a second time after an error, and that one is safe only if you made the tool safe.
- Does the MCP idempotentHint make a tool safe to retry?
- No. The MCP schema states that all ToolAnnotations properties are hints and are not guaranteed to describe tool behaviour faithfully, and that clients should never make tool use decisions based on annotations from untrusted servers. idempotentHint defaults to false.
- How do you make an agent action idempotent when the target system has no idempotency keys?
- Read the target's own record of what exists before every write, derive the identifier from stable business facts rather than a fresh UUID, and log the intent before acting rather than after. The read is the receipt the target refuses to issue.
[ NEXT ]
Want this built for you?
Thirty minutes, no prep, no pitch. Tell us what slows your business down and we'll show you what an agent can do about it.
Book a call