The call
A design partner pinged us late on a Wednesday. Their customer support agent had just promised a customer a refund the company doesn't issue — full price, on a non-refundable subscription tier, in writing, in the customer's inbox.
The customer was, understandably, very interested in collecting on the refund.
The team needed two things by morning: a clean answer about *what the agent did and why*, and a fix that meant it wouldn't do it again. They had logs. The logs said the agent ran for 1.8 seconds and emitted a polite, well-formed reply. No errors. No retries. No flags.
This is the failure mode every AI engineer ships with: the agent is "fine" by every metric you have, and wrong by every metric that matters.
What the LLM trace told us
We pulled the OpenAI trace first. It was clean. Two model calls — one to classify the intent ("refund request"), one to draft the reply ("Yes, your refund is on its way, you should see it within 5-7 business days"). The token counts looked normal. The latency looked normal. There was no obvious red flag at the model boundary.
That's the limit of LLM observability. It tells you the model did its job. It doesn't tell you the agent did its job.
What the agent trace told us
The agent trace had four steps, not two. In order:
```
agent.run [root] 1820ms
step 1 classify_intent 200 OK 310ms
step 2 tool lookup_account_tier 200 OK 180ms tier='enterprise'
step 3 tool lookup_refund_policy 404 190ms policy='not_found'
step 4 draft_reply 200 OK 420ms
```
Step three is the one that mattered. The agent had asked a downstream tool for the refund policy specific to this customer's tier. The tool returned a 404 — no such policy.
The reply didn't say "I don't know." It said "Yes, your refund is on its way." The agent had inferred, from absence of policy, that refunds were allowed, and confabulated the specifics from a template it had presumably seen in training.
The bug wasn't in the model. It was in the agent's tolerance for missing context.
The fix, and the alert
The fix took twenty minutes. Add a guard: when the policy lookup returns anything other than 200, the agent escalates to a human queue. Stop drafting replies on top of 404s.
The alert took ten minutes. In Trefur we set a rule: any agent run where a tool call returns a 4xx and the agent still produces a customer-facing reply triggers a high-priority page. Three runs matched the rule in the last 30 days of trace history. All three were similar: tools returning 404 or 403, agents replying anyway.
The team is now running that alert in production. It's caught two more before customers noticed.
What I learned
Three things, ordered by how often I'd been making the wrong call before:
1. LLM observability is necessary and not sufficient. Token counts, latency, and model response previews are useful. They are not the agent trace. If the only tool you have stops at the LLM boundary, you can't see the step that mattered for this incident — the silent tool failure two steps upstream of the customer-facing reply.
2. Tool failures are where confabulation lives. When a downstream tool fails and the agent doesn't have a hard rule to stop, the model fills in what's missing. Sometimes correctly. Sometimes wildly wrong, with full confidence and good grammar.
3. The alert needs to fire on the decision, not the symptom. "Customer complained about wrong refund" is a symptom. "Agent drafted a customer-facing reply after a 4xx tool call" is the decision. The decision-shaped alert catches the failure mode before a human gets hurt.
If you ship agents in production, you need observability that covers the agent loop — not just the model call. That's the thing that lets you find the lying step before it ships.
We talk to engineers about this all day. If you have an agent in production and you don't have a trace that shows you every decision it makes, [start free](/pricing). Ten minutes to first trace. Real traces, your real agents.