Why Your AI Agent Reports Success But Nothing Ran: Five Ways a Tool Call Dies
When an agent says it called a tool, the run comes back green, and nothing happened, the model almost never lied. A tool call is just text the model hands off, and it gets dropped somewhere in the plumbing between the model and the tool, in one of five places: streaming, index collision, proxy translation, the wrong finish flag, or no check at the end. A bigger model fixes none of them.
Why Your AI Agent Reports Success But Nothing Ran: Five Ways a Tool Call Dies
The short answer. Your agent says it called a tool, the run is green, and nothing happened, no API call, no database write, no email. The model almost never lied. A tool call is just text the model writes and hands off, and it got dropped in the plumbing between the model and the tool, in one of five places: streaming, index collision, proxy translation, the wrong finish flag, or the absence of a final check. A bigger model fixes none of them.
If you have built anything with agents, you have seen this.
The agent reports that it called the tool. The run comes back green. Status: success. And the thing you actually asked for never happened.
What makes it maddening is that there is nothing to grab onto. No error. No stack trace. Nothing in the log that looks even slightly wrong. The run planned the call, emitted the call, closed the stream, finished the turn, and reported success. Every instrument you have says it worked.
Meanwhile, in the real world: zero API calls, zero database writes, zero emails sent. Nothing moved.
That is the trap. When something is obviously broken and every signal says it is fine, you start hunting everywhere except the one place that is actually broken.
The model almost certainly did its job
Start where everyone starts, at the model's output, because the instinct is that the model lied.
Look at what it produced. Tool name, correct. Arguments, complete and valid. The model did exactly what it was asked to do.
So every failure worth investigating happens after this point. Everything that happens to that output once the model has handed it off.
And this is the part almost nobody looks at, because it is not the clever part. It is the boring part, the plumbing, and that is where it is broken essentially every time.
What a tool call actually is
The confusion starts with the phrase itself. A model cannot run anything. It has no hands. When a model "calls a tool," it is not calling anything at all.
Think of the model as a waiter. It takes your order and writes it down on a slip. It does not cook. That slip is the tool call: pure text, a JSON object, nothing more.
The slip then gets handed to the cook, which is your code, and the cook reads it and makes the dish.
Two jobs. Two different places. The model writes the order, your code executes it, and between the two there is a hand-off.
So the real question is: what happens when the order gets written correctly and never reaches the cook?
The customer gets told the food is on its way. The cook never knew there was an order. And here is the part that makes this so hard to catch: nobody made a mistake. The waiter did its job and wrote the order correctly. The cook was ready and willing. From each component's own perspective, everything went perfectly.
That is why this is a silent killer. There is no faulty part to find.
Cause one: the streaming race
This is the most common by a wide margin, and it comes from how the model sends the call.
The model does not hand you the whole order in one go. It streams it, one piece at a time. The first chunk says a tool is coming and gives its name. A moment later the arguments start arriving, piece by piece.
The bug is that the harness reads the arguments the instant the first chunk lands, having seen the tool name and concluded the call is ready.
At that exact moment, the arguments are an empty string. Parse an empty string and you get an empty object. An empty object is perfectly valid. So the tool fires with no arguments, does nothing, because with no input there is nothing to do, and returns successfully, because doing nothing correctly is a success.
From the outside, the tool ran and produced no output. Both of those statements are true, and together they are a disaster.
Here is the whole bug in one line: an empty field did not get read as "not ready yet," it got read as "there is nothing here."
- Chunk 1 arrives: a tool is coming, and here is its name
- The harness parses nowarguments are still an empty string at this instant
- Empty string parses to an empty object, which is perfectly validan empty field read as nothing here, rather than as not ready yet
- The tool fires with no input, does nothing, and returns successdoing nothing correctly is a success
- Chunks 2 and 3 arrive with the actual argumentstoo late, it already ran
If you have worked near hardware, this will feel extremely familiar, because it is a classic race condition. The status register says the byte is ready, the ready flag is high, but the data register has not latched the byte yet. Read one cycle too early and you get stale data or nothing at all. The flag went up before the data was really there.
Same failure, one layer down.
Cause two: index collision
This one shows up when the model requests two tool calls in a single turn.
Look at the index on each streamed chunk. Every one of them says index zero. So the first call and the second call are tagged identically, and when the assembly code groups the chunks by index, both calls land in the same slot and one overwrites the other.
You asked for two things to happen. One of them quietly disappeared before anything tried to run it.
If you have done any protocol work, you know this one too. TCP reassembles a stream of packets using their sequence numbers. Give every segment the same sequence number and the stack either orders them wrongly or collapses them into one. Grouping by the wrong key produces the wrong result.
Same failure, one layer down.
Cause three: proxy translation
This one is not about your code and not about the model. It is about something sitting between them.
The model speaks one format. There is a proxy, a gateway, or a compatibility shim in the middle translating it into a different format. By the time it comes out the other side, the tool call is gone, because the field it lived in had nowhere to map to in the target format, so the translation quietly dropped it.
The tell for this one is specific and very useful: it works when you call the model directly, and breaks the moment you run it through your own stack.
Every protocol engineer has hit this. A gateway sits between two wire formats, say one with a header, body, and trailer, and the other has no equivalent of the trailer. The gateway drops it. Direct communication works, the middleman breaks it.
Same failure, one layer down.
Cause four: the wrong finish flag
This one is sneaky, because the tool call completed fine.
The response comes back with a valid tool call sitting right inside it. But the finish reason says "stop" when it should say that tool calls are pending. And your loop is checking that flag to decide what to do next.
It sees stop, exits, and walks right past the call sitting there waiting to be run.
The work was done. The model produced exactly the right output. Your loop trusted the wrong signal and quit.
Driver engineers deal with this constantly. A peripheral raises a status bit and sets the done bit while the transfer is still in flight. The driver trusts the bit, reads early, and gets garbage. Wrong flag, trusted.
Same failure, one layer down.
Cause five: nothing checks at the end
The fifth cause is the reason the other four can stay hidden.
Look at a trace of a failed run. The agent kicked off, reported success. The model span shows it emitted one tool call. Go looking for the execute step and there are zero of them.
Nothing anywhere in the system compares those two numbers.
Intended: one. Executed: zero. There is no check, so the drop sails straight through the gate and gets reported as success.
And the flip side of this same gap is just as dangerous. The exact same missing check is what lets a tool run twice, which in a system that processes payments or sends messages is considerably worse than not running at all.
Anyone who has worked on distributed systems has names for both halves. Checking something and then acting on it, with a gap in between where it quietly changed, is a time-of-check to time-of-use bug. And without an exactly-once guarantee, a message gets delivered twice as readily as it gets dropped.
No check at the boundary means both the drop and the double slip right through.
Same failure, one layer down.
How to find which one is yours
You do not have to guess. There is an ordered diagnosis, cheapest and most informative first, and the first check does most of the work.
- Check zero: count intended against executedone emitted, zero executed. The whole failure as a pair of numbers
- Check one: turn streaming off, rerun the identical requestsplits the five suspects in half
- It works now: the problem was reassemblyread the raw stream. Empty arguments is cause one, wrong buckets is cause two
- Still broken: streaming was never the issuecall the model directly to catch a middleman, or inspect the finish signal
- Five checks, worst caseat no point do you touch the model or the agent's reasoning
Check zero: count intended against executed. Instrument the run to count the tool calls the model emitted and the tool calls that actually executed. One and zero. That mismatch is the entire failure expressed as a pair of numbers.
Build this first, always. It does two jobs at once: it confirms the bug is real rather than a misreading, and if you leave it in permanently, it is the fix for cause five.
Check one: turn streaming off and rerun the identical request. That is the whole test, and it splits the five suspects in half.
If it suddenly works, your problem was in how the streamed pieces were reassembled, which means cause one or cause two.
If it is still broken with streaming off, streaming was never the issue, and you are looking at a middleman, a bad signal, or a missing check, which means cause three, four, or five.
This is not a fix, it is a diagnosis, and it is the most useful single move available.
From there it narrows fast. If it worked with streaming off, read the raw stream directly. Empty arguments at the moment of parsing is cause one. Pieces landing in the wrong buckets is cause two. If it broke both ways, call the model directly to catch a middleman, which is cause three, or inspect the finish signal, which is cause four.
Worst case that is five checks, and notice that at no point do you touch the model or the agent's reasoning.
The fixes, in the same order
Cause one. Do not parse and do not run until you are sure the call is complete: the name is there and the arguments are closed off. Treat an empty parse as "wait," never as "done." Some stacks handle this correctly now and at least one very common framework has had it open for a long time, and the practical workaround is exactly the diagnostic: turn streaming off on the paths that use tools.
Cause two. Group the streamed pieces by the call's own identifier, not by arrival order and not by the index, which requires trusting the provider to number them correctly. If the index arrives broken or all zeros, repair it in a small shim before your assembly code ever sees it.
Cause three. Find the middleman by pulling layers out one at a time until it works. Then either pin that layer to a known-good version or route around it. Format bridges are the prime suspect, and pinning matters because a silent version bump can bring the whole problem back months later. Real breakages from version bumps exist across multiple proxies.
Cause four. Trust the call, not the flag. If there is a tool call in the response, run it, whatever the finish reason says. If you control the gateway, fix the marker at the source. For providers you already know misbehave, stop consulting the flag entirely.
Cause five. Take check zero and make it permanent. Compare intended against executed on every single run, and refuse to report success when the two do not match. While you are in there, make each call safe to run exactly once, which closes the double-execution version of the same gap.
Cause five is the one that quietly catches the other four before you even know which one you have. Build it first.
The lesson underneath
Notice what none of this was. Not a prompt change. Not a model change. Not anything to do with reasoning.
All five problems lived in the plumbing between the model and the tool. Not inside the model, not inside the tool. In the channel between them.
The smartest model available fixes none of them, which means the thing you were sure about, that the agent lied about calling the tool, was wrong. It almost always told the truth.
That is the bigger lesson. The failures worth understanding are almost never in the smart part of the system. They live in the connections, and finding them means being willing to look at every layer: the model, the stream, the protocol, the driver, all the way down. It is the same discipline whether there is a model in the system or not.
FAQ
Why does my AI agent say it called a tool when nothing happened? Because a tool call is just text the model writes and hands off, and it was dropped in the plumbing between the model and your code. The model almost certainly emitted a correct call. Look at streaming reassembly, index collisions, proxy translation, the finish flag, and whether anything compares intended calls against executed ones.
Why does my tool fire with empty arguments? Because the harness parsed the arguments the instant the tool name arrived, while the arguments were still an empty string. An empty string parses to an empty object, which looks valid, so the tool runs with no input and returns success. Wait until the call is complete, and treat an empty parse as not ready rather than as nothing.
Why does my agent only run one of two tool calls? Most likely an index collision. Both streamed calls are tagged with the same index, so grouping the chunks by index puts them in the same slot and one overwrites the other. Group by each call's own identifier instead, and repair broken indices in a shim before assembly.
My agent works calling the provider directly but breaks through my stack. Why? That is the signature of a translation layer dropping the tool call field, because it has no equivalent in the target format. Pull layers out one at a time until it works, then pin that layer to a known-good version or route around it.
How do I detect a dropped tool call in production? Count the tool calls the model intended against the tool calls that actually executed, on every run, and refuse to report success when they do not match. That single check catches drops, catches double executions, and is the reason the other four causes can hide when it is missing.
Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.
Bring us the program