The demo ran 50 times without a failure. Then we shipped it.
Three days into production, the agent started returning confident nonsense. Not errors. Not crashes. It finished its task, wrote a result, logged success. The result was wrong. Nobody caught it for six hours.
The agent's job was to pull structured data from a third-party endpoint, summarize it, and route a decision. In testing, that endpoint always returned a list with at least one item. On day three, it returned an empty list. Valid JSON, zero items. The agent had never seen this input. It did not stop. It summarized the absence of data as if it meant something, filled in plausible context, and routed confidently.
Confident autocomplete on an empty input. That is the production failure mode demos never surface.
Why the demo hid it
Every test I ran before shipping used good inputs. Clean structure, expected ranges, the happy path. I tested for API failures. I tested for malformed JSON. I did not test for technically valid responses that meant "there is nothing here."
Demos optimize for showing what works. Production finds everything you missed. This gap exists in all software, but AI makes it harder to see. A traditional system throws an exception or returns null. An LLM writes something coherent and wrong. You have to read the output, understand the domain, and notice that "summarized one recent transaction" should have been "no transactions found." That requires a human check or an explicit assertion. I had neither.
The deeper problem: I had a handoff between the data layer and the reasoning layer with no contract between them. The data layer said "here are zero items" and the reasoning layer said "let me make sense of whatever I received." It did. Badly. No one told it that zero items was a special case worth stopping for.
Benchmarks do not show you this. A hundred happy-path runs do not show you this. The empty-list case shows up on day three, while you are doing something else.
The fix was not clever
I added a guard. Before the agent reasons over any data, it checks whether the input meets the minimum conditions for a meaningful answer. If not, it returns a structured "no data" signal and stops. The downstream system handles that signal explicitly.
That is the whole fix. No prompt engineering, no fine-tuning, no new model, no clever architecture. A check that runs before the model touches anything.
The unglamorous part is that defining "minimum conditions" for every data type the agent processes took longer than building the agent. I had to think through each input type and ask: what does "valid but meaningless" look like here? You cannot skip this. If you do not define it, the model will always find something to say. It is very good at that.
I run several agents at Agent Enterprise (aienterprise.dk), and this pattern repeats across all of them. The demo uses inputs I chose. Production uses inputs no one chose. The diff between those two populations is where your reliability lives or dies.
A piece on reliable agentic AI from Martin Fowler's platform pulled 171 points on Hacker News in June 2026. Not 45. That is not people reading about exciting new agent capabilities. That is people who have hit production and are looking for help. The practitioner community knows where the gap is.
A demo proves an agent can. Production proves it does, on the bad input, at 3am, when nobody is watching. That is the only gap worth closing.
Reliability is the only feature. Everything else is a demo.
Top comments (8)
"Confident autocomplete on an empty input" is the whole genre of production agent failure in one line. The thing I'd underline is your diagnosis, not the fix: the real bug wasn't the empty list, it was a handoff with no contract between the data layer and the reasoning layer. Once you frame it that way, the guard isn't a hack — it's the missing type. What we've landed on is making the tool return a typed result, an explicit
empty/partial/okdiscriminant, so the model can't receive "zero items" as if it were "items, but few." Forcing the branch beats hoping the model infers it. The case that still gets us is the middle of that spectrum: not empty, but degraded — three of eight fields null, a stale timestamp, a plausible-but-wrong default. A minimum-conditions check passes because there's technically data, and the model happily reasons over the holes. Do you have a notion of "enough" beyond non-empty, or is partial-data still an open failure mode? That's the one I haven't fully closed either. Fully agree that defining "valid but meaningless" per input type is the unglamorous 80% of the work.The part that stands out is that the empty list was valid JSON, so every schema check passed while the agent quietly treated "nothing found" as "here is the answer." I have started asserting on result cardinality before the summarizer runs, since zero rows almost always means retrieval failed upstream, not that the answer is genuinely empty. Did the six-hour blind spot come from missing alerting, or was the "success" log truly indistinguishable from a real one?
The part that generalizes is the missing contract at the data/reasoning handoff, and it is worth pushing one level past a guard function. If "valid but meaningless" is a real state, it belongs in the type the data layer returns, not in a check the reasoning layer has to remember to run. A tagged result (Empty vs a non-empty payload) forces every downstream consumer to handle the empty branch at compile time, so the precondition cannot silently rot as new shapes show up. The other half of the six-hour gap is the output side: a deterministic postcondition, like requiring the routed decision to cite at least one source record, would have tripped before a human ever looked. Both are plain code running around the model, never inside it. I wrote this draft, validate, approve boundary up as a small reference here: github.com/renezander030/agent-app...
This is exactly what happens when agents leave the sandbox. A normal app fails loudly, but an LLM can fail while looking completely fine. Guardrails around data quality are becoming a core part of the build.
The 50 clean demos problem is real because demos usually test the happy path plus one visible edge case. Production tests patience, stale context, partial failure, weird user intent, and retries after the agent has already done half the work. I would trust fewer demos and more logged failure drills.
The guard is right, but 'minimum conditions for a meaningful answer' is itself a spec that rots the same way the demo did. Today it's empty list. Next it's
{items: []}vsitems: nullvs a 200 that's really a stale cache returning yesterday's data vs one row where every field is null. Each is technically valid and semantically empty, and you'll add a clause per incident forever. The failure isn't empty-input specific, it's any input outside the demo distribution, and the LLM's move on all of them is the same: make coherent sense of whatever it got. So I'd split it two ways. Push the meaningfulness signal into the data-layer contract (count, freshness, completeness as explicit fields the reasoning layer must read), not inferred downstream. And gate the OUTPUT on provenance: the agent isn't allowed to route a decision unless the summary cites N>=1 real items it actually saw. Input guards catch the case you named; an output-grounding assertion catches the ones you haven't hit yet. Are you gating on input shape or on output provenance right now?This hits hard. I ran into something similar with a tool I built — it would silently produce plausible-looking output when the input data was sparse or missing. The worst part? The output looked fine to a quick glance.The "no contract between layers" part really resonates. I ended up adding explicit pre-condition checks before the reasoning step. Not elegant, but it catches the empty-list case before it becomes a confident hallucination.Curious if you found a pattern for deciding which edge cases actually need that hard stop vs. which ones are safe to pass through?
This matches what I see reviewing coding agent diffs daily. The scariest failures were never the ones that crashed, they were the ones that came back with a clean, confident answer built on an assumption nobody asked it to check. I had a very similar case, an agent hit an endpoint that returned 200 with an empty array, and instead of stopping it treated the empty array as nothing changed and skipped a write it should have made. No error anywhere, the run just quietly did less than it should have. Your fix, a guard for valid but meaningless input, is the right shape because it moves the decision out of the reasoning step entirely. Once an agent is allowed to interpret ambiguity, it will, and it will do it with total confidence.