All posts

When a Tool Call Fails on a Live Phone Call

There is no spinner on a phone call. Why silence generates input, why timeout and empty result are different conversations, and why the model must not guess.

A customer is on the phone. They give an order number. The agent calls your order API. The API takes four seconds, or returns a 500, or returns nothing at all.

In a chat window this is a solved problem. You render a spinner, you show an error state, you offer a retry button. The interface has somewhere to put the situation.

On a phone call there is no interface. There is a person holding silence, and silence means something specific to them.

Silence is not neutral

In text, a pause is the absence of a message. On a call, a pause is a message.

Two seconds of nothing after a customer reads out an order number does not read as "processing". It reads as the other party having stopped listening, or being confused, or the line having dropped. People have spent their whole lives interpreting conversational silence and they do not suspend that skill because they suspect they are talking to software.

The most common thing they do is repeat themselves. Which means your slow tool call has now produced a second copy of the order number arriving mid-flight, and if you handle that naively you get two lookups, or a transcript where the customer appears to have given two different inputs.

So the first rule of tool calls on voice is that the agent must never be silent while one is running. Not because silence is impolite, but because silence generates input.

Say something true before you know the answer

The fix is the same thing a human does. A person looking something up on a call does not go quiet. They say "let me pull that up" and then they are quiet, and the quiet is fine, because it has been labelled.

So the agent speaks a holding line the moment a tool call starts, before any result exists. It has to be true regardless of the outcome, which rules out anything that presumes success. "Let me check that" survives every ending. "Great, I've found your order" does not, and an agent that says it before the lookup returns will eventually say it immediately before announcing that no such order exists.

The holding line buys real time. Speaking it takes about a second, and that second is spent on work rather than on the customer wondering if the call is still alive.

Budgets, and what happens at the edge

Every tool call gets a deadline, and the deadline is set by the conversation rather than by the API.

This is the inversion that voice forces on you. In a web request you give a dependency the time it needs. On a call you give it the time you have, which is a few seconds, after which the honest thing is to stop waiting and say so.

A tool that has not answered in time is treated as failed. Not pending, not retried silently. Failed. The agent then has to say something, and what it says depends on what it can still offer.

The failure modes are not interchangeable and should not collapse into one apology:

Timeout. The system may be fine and slow. Offer to stay on it, or to call back, or to take a message. Do not claim it is broken.

Error. The system answered and the answer was a failure. Say something is wrong on our side and move to the fallback, which is usually a human.

Empty result. The system worked perfectly and there is no such order. This is not an error and must never be reported as one. Telling a customer the system is down when their order number is simply wrong sends them away to wait for a fix that is not coming.

That third one is where most implementations are careless, because at the code level an empty array and a caught exception both take the sad path. On a call they are entirely different conversations.

Retry is mostly wrong here

Retrying with backoff is correct in a background job and usually wrong on a live call. A retry costs the caller more silence in exchange for a possibility, and the budget was already spent.

One immediate retry on a connection-level failure is defensible, because it is fast and the failure is likely transient. Anything with a delay is not: you are asking a person to wait through a strategy designed for a machine that does not mind waiting.

Never invent the answer

The worst outcome is not an error. It is a plausible answer.

A model that has been asked to be helpful, has a failed tool call, and knows the general shape of an order status is fully capable of producing "that's out for delivery today." Fluent, confident, wrong, and spoken aloud with no written record the customer can go back and check.

Which is why a failed call has to be an explicit fact in the context rather than an absence. The difference between the model seeing nothing and the model seeing "lookup failed, no data" is the difference between a guess and an admission. Absence invites the model to fill it. A stated failure gives it something true to report.

The handover

Eventually the agent has to stop. Two consecutive failures on the thing the call is about is a reasonable line: at that point the customer has spent thirty seconds and heard two apologies, and the third will not land better than the second.

The transfer has to carry what happened. The order number they gave, the fact that the lookup failed twice, what has already been apologised for. A handover that arrives as a cold "how can I help" makes the customer repeat everything, which is the exact experience the agent was there to prevent, now with an extra two minutes attached.

Why this is worth designing rather than handling

In text, tool failure is a UI state. On voice it is a conversational event, and it happens in front of the customer with no way to soften it after the fact.

You cannot edit what was said. You cannot show a spinner. All you have is what the agent says next, and how quickly it says it. That is the entire error handling surface, and it has to be designed in sentences rather than in states.