An agent that can call tools will, sooner or later, call the same tool twice. A network timeout triggers a retry. A run resumes from a checkpoint that predates its last action. The model, reading its own transcript, decides the email "probably didn't send" and sends it again. None of this is exotic. It's normal behavior for a loop built on a nondeterministic model and an unreliable network.

Read-only tools shrug that off. Tools with side effects, such as creating a ticket, charging a card or posting a message, do not. The classic answer from distributed systems applies directly: make the operation idempotent, so doing it twice has the same effect as doing it once.

The model to copy: payment APIs

Stripe's API documentation is the clearest public description of idempotency keys, and its rules are a good template. The key facts from the docs:

  • You send a unique key with the request, in an Idempotency-Key header. Stripe suggests V4 UUIDs or another random string with enough entropy to avoid collisions, up to 255 characters, and says to avoid sensitive data such as email addresses in the key.
  • The server remembers the outcome. For the v1 API, Stripe saves the status code and body of the first request made with a given key once execution begins, and retries with the same key return that saved response, even if it was a 500 error.
  • Parameters are checked. The idempotency layer compares incoming parameters with the original request and returns an error if they differ, to prevent accidental misuse.
  • Keys expire. For v1, keys can be removed after they are at least 24 hours old; reuse after that creates a new request.
  • Some failures aren't saved. If parameters fail validation, or the request conflicts with one already running, no result is saved, and you can retry.
  • Reads don't need keys. Stripe notes that GET and DELETE requests are idempotent by definition, so a key has no effect on them.

Applying it to an agent

For a tool that changes something, add a key, and have the harness (your code) generate it, not the model.

  1. Derive the key from stable inputs. For example: the run ID, the step number, and a hash of the tool name plus normalized arguments. A retry of the same step produces the same key, and a genuinely new action produces a different one.
  2. Pass it to the tool or a wrapper in front of it, which records the key together with the result.
  3. On a repeat, return the stored result instead of acting again.

A random UUID per call, as Stripe suggests for ordinary clients, is fine when your code generates it once per logical operation and reuses it on retries. For an agent that can resume from a checkpoint, deriving the key from run and step is more robust, because the key is reproduced without having to be stored first. Never let the model invent the key: it will produce a different one on each attempt, which defeats the purpose.

Details that decide whether it works

  • Same intent, different arguments. If the model retries with reworded arguments, a hash of the arguments differs. For high-stakes tools, key on the business identity ("refund for order 1234") instead of the raw argument text.
  • Store the outcome, not only the key, including permanent errors. Otherwise the agent sees a different answer on replay and may take a different path. This mirrors Stripe returning the saved response, errors included.
  • Handle in-flight duplicates. Record the key as pending before acting. A second call that arrives while the first runs should wait or return "in progress," not start a parallel copy.
  • Give keys a lifetime at least as long as your longest plausible resume window.
  • Tell the model what happened. When a call is deduplicated, say so in the result ("already completed at step 4; no new action taken"). The model reasons better when it knows the action happened once.

Idempotency isn't approval

Confirmation gates decide whether an action should happen; idempotency decides how many times it can. They solve different problems, and a safe agent needs both. Human approval for risky actions is also a core defence against prompt injection.

If you can add only one thing to a tool that moves money or sends messages today, add the key. It turns "retry and hope" into a property you can test.

Sources