Ground Truth

Why Your AI Agent Reports Success But Nothing Ran: Five Ways a Tool Call Dies

Mostafa DhouibMostafa Dhouib··12 min read
The short answer

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.

The waiter, your model
takes the order and writes it correctly
The slip, the tool call
pure text, a JSON object, nothing more
The hand-off
the slip falls on the floor here
The cook, your code
ready and willing, never saw an order
The customer
told the food is on its way
Nobody made a mistake. The waiter did its job, the cook was ready, and from each component's own perspective everything went perfectly. That is why it is so hard to catch.
FigureA model cannot run anything. It writes the order down and hands it off, and the failure is that the slip never reached the kitchen while everyone reported success.

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.

Model writes the call
1 Read
streaming, arguments still an empty string
2 Assemble
index
3 Carry
proxy translation
4 Signal finished
finish flag
5 Check
intended versus executed, the reason the other four stay hidden
Tool runs
The model wrote the correct order. It got dropped in the plumbing.
FigureA tool call passes from the model to the tool through five stages, and it can be silently dropped at any of them while every signal still reports success.

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."

  1. Chunk 1 arrives: a tool is coming, and here is its name
  2. The harness parses now
    arguments are still an empty string at this instant
  3. Empty string parses to an empty object, which is perfectly valid
    an empty field read as nothing here, rather than as not ready yet
  4. The tool fires with no input, does nothing, and returns success
    doing nothing correctly is a success
  5. Chunks 2 and 3 arrive with the actual arguments
    too late, it already ran
A classic race condition: the ready flag went high before the data register latched the byte. Same failure, one layer down.
FigureThe model streams a tool call one piece at a time. Reading it the instant the name arrives finds the arguments still empty, and an empty parse looks exactly like a valid one.

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.

  1. Check zero: count intended against executed
    one emitted, zero executed. The whole failure as a pair of numbers
  2. Check one: turn streaming off, rerun the identical request
    splits the five suspects in half
  3. It works now: the problem was reassembly
    read the raw stream. Empty arguments is cause one, wrong buckets is cause two
  4. Still broken: streaming was never the issue
    call the model directly to catch a middleman, or inspect the finish signal
  5. Five checks, worst case
    at no point do you touch the model or the agent's reasoning
Build check zero first. It confirms the bug is real, and permanently it catches the other four.
FigureAn ordered diagnosis for a dropped tool call, cheapest and most informative first. The first check does most of the work, and left in place it is also the fix for the fifth cause.

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.

Carrying a program like this one?

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