An agent decides to issue a refund, calls the payment tool, and the call commits. Then the connection drops before the response arrives. From the agent's perspective, success and failure look identical, so the framework retries, and the refund runs again. That is not a model bug. It is ordinary distributed systems behavior that the agent layer imported without the patterns that usually prevent it, as this analysis of agentic tool calling argues (TianPan.co).
There are three common triggers. A transient execution error where the tool partially ran, a confusing success message that reads like a failure (for example "0 rows updated" when the value was already correct), and a tool-call loop where the model cycles through the same call repeatedly (RunGuard). All three produce the same outcome: two ledger entries for one intent.
The webhook world solved this years ago, and the fix transfers directly. Helpers that make webhook handling safe, whether you send or receive, always come back to the same idea: when repeating an action produces the same outcome, the action is idempotent, and PUT versus POST is the classic illustration because POST implies a new side effect on every call (Hookdeck). A refund tool is a POST in disguise. Your eval suite exists to prove the disguise fails closed.
Durable execution engines help, but do not close the gap. One test rig killed a worker with SIGKILL between a tool's side effect and the moment the step result was journaled, and a mock payment API received two charges in 3 of 3 trials; passing a stable key to the payment call made the second request a no-op in 3 of 3 trials (NerdLevelTech). Journals protect completed steps. Nothing protects a step whose effect landed but whose result was never recorded.



