DEV Community

Chris Sellers
Chris Sellers

Posted on

Which agent tool errors are safe to retry? Only one of four

An agent gets an error back from a tool call. Should it try again?

Most agent loops answer that with one rule: on error, retry a few times with backoff. I think that rule is wrong for any tool that moves money or changes state, because "error" covers at least four very different situations, and only one of them is safe to retry blindly.

This post is the error map I use in Agent Middleware (AMW), the permit and receipt gateway I am building for agent tool calls. The code below is generic, not lifted from my repo.

Ask one question first: did anything happen?

Before deciding whether to retry, the agent needs to know whether the call reached the point where a side effect could exist. I sort every error by that question.

  1. Refused before anything was reserved. The permit does not allow the call: wrong tool, expired permit, or a constraint the backend cannot enforce. Nothing was written, charged or sent.
  2. Lost a race before anything was written. Two calls tried to update the same permit at the same moment and this one lost. Nothing was written, charged or sent.
  3. Refused because a limit was reached. The permit set a per tool call cap and it is used up. Nothing new was charged or sent, but the earlier calls did happen.
  4. Sent, and the answer never came back. The request left the gateway and the response was lost. The side effect may or may not exist.

Only the second one is a "just try again" error.

What each case should do

Refused before reservation: do not retry. Retrying an identical request gets the identical refusal. The fix lives with whoever issues the permit. In AMW this comes back as a signed denied receipt, so the agent has a record of why it stopped.

Lost a race: retry, with the same idempotency key. AMW answers this with HTTP 409 (or JSON RPC error -32005 on the MCP endpoint). The transaction rolled back, the idempotency record for that attempt was released, and no credits moved. Reusing the same key matters here: if the first attempt had somehow finished, the same key returns its stored receipt instead of dispatching again.

Limit reached: do not retry, and definitely not with a fresh key. A fresh key is a new call, not a repeat. When the permit sets a per tool cap and the cap is used up, a new key gets refused with a cap exceeded error, with nothing dispatched or charged. When the permit sets no cap, a new key simply runs again within the permit's budget and expiry. So the cap only protects you if you set it.

Sent with no answer: park it. I wrote a whole post on this one. The short version: AMW records the attempt as delivery uncertain, keeps the charge, and never resends it on its own. A human or a reconciliation job checks the provider before anything else happens.

A sketch of the agent side

def handle_tool_error(err, call):
    if err.kind == "denied":
        # Permit does not allow it. Retrying changes nothing.
        return stop(reason=err.code, receipt=err.receipt)

    if err.kind == "contended":
        # Nothing was written. Safe to retry with the SAME key.
        return retry(call, idempotency_key=call.idempotency_key,
                     backoff=jittered(base_ms=50, max_tries=3))

    if err.kind == "cap_exceeded":
        # Earlier calls happened. A new key would be a new call.
        return stop(reason="cap reached", receipt=err.receipt)

    if err.kind == "delivery_uncertain":
        # Side effect may exist. Do not retry at all.
        return park(call, for_review=True)

    # Unknown error class: treat it as uncertain, not as safe.
    return park(call, for_review=True)
Enter fullscreen mode Exit fullscreen mode

The last line is the part I care most about. When the agent cannot classify an error, the safe default is to stop and ask, not to loop.

What this does not give you

None of this makes a remote side effect happen exactly once. A cap of 1 means the gateway dispatches at most once for that permit and tool. Whether the provider itself applies the effect once still depends on the provider honoring the idempotency key. The error map just keeps the agent from turning one uncertain call into two certain ones.

Where I am

AMW is early and I am building it alone. I am looking for people building agents that touch payments or other irreversible tools who want to break it.

How does your agent loop decide which errors are safe to retry? I would like to hear where my map is wrong.

More at https://www.thisisatest.tech

Top comments (0)