DEV Community

Chris Sellers
Chris Sellers

Posted on

When an agent's tool call times out, park it instead of retrying

An agent calls a tool that moves money or sends a message. The request goes out. Then nothing comes back: a gateway timeout, a dropped connection, a worker that died mid response.

From the agent's seat, that looks like a failure. Most agent loops treat a failure as a reason to try again. That is the step that charges a customer twice.

This post is about the third state that sits between "it worked" and "it failed", and what an agent should do with it.

A timeout is not a failure

A client timeout tells you one thing: the client stopped waiting. It says nothing about whether the provider did the work.

So every effectful call really ends in one of three states:

  1. Succeeded. You have a response and a receipt.
  2. Failed before send. The request never left your side (validation, auth, a connection that never opened). Safe to retry.
  3. Unknown. The request was sent and the response never arrived. The side effect may or may not have happened.

Most code has branches for the first two and quietly folds the third into "failed". That fold is the bug.

Why "unknown" must not trigger a retry

If you retry an unknown under a new idempotency key, you have asked for a second intent. The provider has no way to know it is the same payment. If the first one landed, you now have two.

If you retry under the same key, you are safer, but only as far as the provider actually de-duplicates on that key. Many do. Some do it only for a limited window. Some tools an agent reaches through MCP never look at the key at all.

Either way, the agent itself is the wrong place to resolve this. It does not have the provider's state, it is under pressure to finish the task, and its default move is to try again.

Park it

The pattern several builders described to me recently is simple: when the outcome is unknown, park the call.

Parking means:

  1. Record the attempt as pending, not failed. Keep the idempotency key, the exact arguments, the tool, the time of send, and whatever partial response you got.
  2. Keep the hold. If you reserved budget or a call slot for this attempt, leave it reserved. Releasing it is how the next attempt gets room to spend again.
  3. Block new keys for the same intent. While an intent is pending, the agent should not be able to issue a fresh key for it. A same key retry can answer "still pending" instead of sending.
  4. Tell the agent something useful. Return a status the loop can act on, not a generic error, so it knows to wait or move on rather than retry.

One reply put it well, paraphrased: treating pending as failed is the expensive move, because failed implies retry or refund, and the charge may well have gone through.

Reconcile outside the agent

A parked call needs something to close it. That is a reconciler, and it should live outside the agent loop.

What it does:

  1. Ask the provider. Many payment and messaging APIs let you look up a request by a client reference you sent with it, and some by the idempotency key itself. That lookup is the real answer.
  2. Back off. Poll on a backoff until the provider gives a final status. Do not hammer it.
  3. Close the record. If the provider says it happened, mark it succeeded and attach what you learned. If the provider says it never arrived, mark it failed, release the hold, and only then let a retry go out.
  4. Escalate when the provider cannot say. Some tools have no lookup at all. Those go to a person with the key, the arguments, and the time of send, so they can check by hand.

The rule underneath: only the reconciler, with evidence from the provider, moves a call from unknown to failed. The agent never does.

What to measure

If you run agents against effectful tools, three numbers tell you whether this is working:

  • How many calls end in unknown per day. If it is zero, check whether unknowns are being folded into failures.
  • How long a call stays parked before something closes it.
  • How many parked calls closed as "it actually happened". That is the number of double charges a naive retry would have caused.

How I handle this in AMW

I'm building Agent Middleware (AMW), a gateway that puts a permit and receipt boundary in front of MCP tool calls. Agents act under a permit; a same key retry returns the original receipt, no second call or charge.

For the unknown case, AMW records a lost response after send as delivery uncertain: the call is charged, the caller gets an error rather than a success, and AMW never re-sends it. When a permit sets a per tool call cap, a delivery uncertain call keeps its slot, so a new key past the cap is still denied.

What it does not do: it does not reconcile with the provider for you, and it does not make the remote effect happen exactly once. Someone, or something you run, still has to check the provider and close the record. I'm biased, since this is the product I'm building, and it is pre-revenue. The parking pattern is older than AMW and works without it.

Disclosure

I'm Chris, the founder and sole owner of AMW. This post was drafted with help from an AI assistant, and the AMW claims were checked against its code. More at thisisatest.tech.

My question for you: when a tool call comes back unknown, who closes it in your setup: the agent, a reconciler job, or a person? I'd like to hear what has held up in production.

Top comments (0)