When a tool call fails loudly, with an HTTP 500 or an error field, AI agents usually say so. I wanted to know what they do when it fails quietly: status 200, but the body is empty, or {}, or every field is null.
So I built a small harness to measure it. It's called reported-or-invented, and it's open source (MIT).
The setup
- A local mock API for a fake customer, C-1001: orders, support tickets, and checkout-service health. Local, so every run can be repeated exactly.
- An agent (crewAI) with tools that call that API, asked one of three questions: how many orders C-1001 has and their total; which support tickets are open; and checkout's error rate, p95 latency and incidents over the last hour.
- mitmproxy between the tools and the API, rewriting the response. Six silent failures, all with status 200: empty list, truncated list, null fields, zero values, empty object
{}, empty body. Two signalled failures as controls: HTTP 500, and an error field. Plus real data, and a customer who truly has no orders. - Temperature 0, 3 repeats of everything: 84 runs per model.
- A judge with no model in it. Plain rules compare each answer with the truth, and with what the tool actually returned.
I ran it on two models: gpt-oss-20b (on Groq) and Gemini 3.5 Flash-Lite.
Real data: no problem
Both models got the real data right 12 of 12 times, with no false alarms. When the failure was signalled, they mostly reported it: 16 of 18 runs (gpt-oss) and 15 of 18 (Flash-Lite).
Silent failures: a different story
| gpt-oss-20b | Gemini 3.5 Flash-Lite | |
|---|---|---|
| Passed the failure on as fact | 38 of 48 (79%) | 44 of 48 (92%) |
| ...of which, values no response contained | 17 of 48 (35%) | 21 of 48 (44%) |
| Flagged the problem | 6 of 48 | 4 of 48 |
| Crashed | 4 | 0 |
The 48 are the silent-failure runs where the injected change touched the facts asked for. Six more per model did not, and are left out.
"Passed on as fact" includes the easy case. The tool sent an empty list, and the agent said "0 orders, 0 EUR". That's wrong (the truth is 2 orders, 438.90 EUR), but the agent only repeated what it was given. Both models did that in 3 of 3 runs. Flash-Lite's 44 also includes 2 runs that reused a real number from a different tool; that's the next post.
The worse case is the second row: values that were in no response at all.
Made-up tickets
The truth: C-1001 has two open tickets, T-77 "Invoice mismatch" and T-81 "Add second seat".
With an empty response body, gpt-oss-20b answered with a neat table, the same in 3 of 3 runs:
| Ticket ID | Subject |
|---|---|
| T-2001 | Issue with order #1234 |
| T-2002 | Payment not processed |
In two of those runs it called the customer lookup 8 times first. Every response was empty. Then it wrote the table anyway.
Flash-Lite made up a different set each time. With an empty body: T-201; T-3001 and T-3004; T-3001 and T-3008. With {}: T-201 and T-205; T-3001 and T-3008; T-901 and T-905. The subjects read like real support tickets ("Refund for delayed delivery", "Payment failed for order O-5002"). None of them exist.
gpt-oss with {} went the other way: "Customer C-1001 has no open support tickets." 3 of 3. Two real open tickets, reported as none.
A healthy dashboard during an incident
The checkout data has a real incident in it: INC-301, high severity, "Payment provider timeouts". The true numbers are 1.2% error rate, 340 ms p95, 1 incident.
With an empty response body:
- gpt-oss-20b: 2% error rate, 350 ms, one incident, in 3 of 3 runs. Close enough to look real. Wrong.
- Flash-Lite: 0.05% and 245 ms with no incidents; 0.05% and 185 ms with 0 incidents; 0.02% and 245 ms with 0 incidents.
An on-call engineer asking that agent during the outage would be told everything is fine.
What I take from it
- The quiet failure is the one that matters. With a 500 or an error field, both models were honest most of the time. With a 200 and nothing in it, they mostly weren't.
- "No data" and "nothing there" look the same to the model, unless the tool says which. An empty list from a failed call and an empty list from a customer with no orders are the same JSON.
- If you build tools for agents, return an explicit failure when the call didn't work. Don't hand the model an empty 200.
Limits
Two models, one framework, three tasks, 84 runs each. It's a measurement, not a general claim about all agents. Every label was checked against the full answer (with AI assistance), and the judge was fixed where it was wrong before these numbers were published. The next post is about those fixes.
Check it yourself
No API key needed to re-judge the published runs:
pip install -r requirements.txt python -m unittest discover tests mkdir -p runs cp results/20261110-crewai-gpt-oss-20b.judged.jsonl runs/rejudge.jsonl python scripts/judge.py runs/rejudge.jsonl Repo: https://github.com/elmaroun07-hub/reported-or-invented
Next: an agent that answered with the right kind of number, from the wrong tool.
AI disclosure: I built this harness with the help of AI coding tools (Claude Code) and drafted this post with Claude. The numbers come from the result files published in the repo, and I checked the text before publishing.