The Timeout That Looked Like an Empty Result
The health check timed out, the handler returned an empty list, and the agent reported all clear while two services were down. An empty result and a failed check must not share a shape.
An ops agent was asked to check whether any services were unhealthy before a deploy. It reported all clear. Two services had been down for twenty minutes.
The health-check tool had timed out. Its handler caught the exception, logged it, and returned an empty list — because returning an empty list was tidier than letting an error escape. The agent read an empty list the only way an empty list can be read: nothing found.
Two very different things, one shape
There are three answers a lookup tool can give, and they mean completely different things:
- "I checked, and there is nothing."
- "I couldn't check."
- "I checked, and there is something."
An empty array expresses the first and the third-by-absence. It cannot express the second. So any handler that catches an error and returns empty has quietly converted I don't know into there is nothing — and the agent has no way to recover the distinction, because the information was destroyed before it arrived.
⚠️ This is one of the most common wrong-answer bugs in agent tooling, and it's invisible in every log. The tool succeeded. The agent responded sensibly to what it received. Nothing errored anywhere.
Why handlers end up doing this
Nobody writes except: return [] because they think it's correct. It arrives by a reasonable path:
- The tool was originally written for a dashboard, where an empty widget on a blip is better than an error page.
- Someone got tired of the agent flailing on raw stack traces, and smoothing the errors away made the demo look better.
- The API being wrapped returns
200with an empty body on some failures, and the handler passed that through faithfully.
All defensible for a human consumer who would notice the dashboard looked odd. An agent doesn't notice; it acts.
The fix, in the handler
Make the tool say which of the three things happened:
check_service_health(services)
→ { "checked": ["api", "billing", "search"],
"unhealthy": [],
"complete": true }
→ { "checked": ["api"],
"unhealthy": [],
"not_checked": ["billing", "search"],
"reason": "timeout after 5s contacting the health endpoint",
"complete": false }
Three properties do the work:
An explicit complete flag. The agent doesn't have to infer completeness from the shape of the data.
Name what wasn't checked. "Two services unreachable" is actionable; "an error occurred" isn't.
Say what to do next, in the reason. "Timeout after 5s — retry, or report status as unknown for these two" turns a dead end into a next step.
✅ The pattern to apply everywhere
Any tool that can return nothing needs to distinguish these cases, not just health checks:
| Situation | Bad | Good |
|---|---|---|
| Genuinely no matches | [] |
{results: [], reason: "no orders match the filter"} |
| Couldn't check | [] |
{results: [], complete: false, reason: "search index unavailable"} |
| Blocked by permissions | [] |
{results: [], reason: "3 matched but excluded by permissions"} |
| Truncated | [...] |
{results: [...], returned: 20, total: 340, truncated: true} |
Every row on the left is indistinguishable from every other row on the left. That's the whole bug.
🔍 Finding it in your own tools
A ten-minute audit, and it usually finds something:
- Search your tool handlers for
except,catch, andrescue. - For each, ask: can this return a value that looks like a normal empty result?
- Where it can, that tool is currently able to tell your agent that nothing is wrong when it has no idea.
The second pass is the one people skip: check the API you're wrapping. Some services return 200 with an empty body on internal failure, so a handler with no error handling at all can still produce this bug faithfully.
💡 Why this matters more for agents than it did before
A human running the same check sees an empty result and thinks huh, that's suspicious — because they know the system usually has something to say. That instinct is the safety net, and it's built from experience the agent doesn't have.
An agent has only the response. If the response says nothing is wrong, nothing is wrong. Which means every judgment the human used to apply between the tool and the conclusion now has to be expressed in the tool's return value, or it isn't applied at all.
The takeaway
An empty result and a failed check are different facts and they must not share a shape. Give every lookup tool an explicit completeness flag, name what couldn't be checked, and put the next action in the reason string. Then grep your handlers for swallowed exceptions — because a tool that answers "I don't know" with "nothing found" will eventually tell you production is healthy while it's on fire.