← ragul.sh

Field notes · Production AI agents

The agent didn’t hallucinate. We moved a field.

An agent in production failed in a way no eval suite would have caught — because the thing that changed wasn’t the model, and the thing that broke never threw an error.

16 Aug 2026 8 min read Agent reliability

The short version

Most writing about agents in production is about the model. Prompt design, context windows, which model to pick, how to stop it hallucinating.

The worst thing that happened to our agent had nothing to do with the model. We renamed a field in one of our own services, the tool wrapper around it didn’t get updated, and the agent kept working. That’s the part worth writing down: it kept working. It produced output that looked right, that an operator would reasonably act on, and nothing in the system raised its hand.

I’m co-founder and CTO at Rehouzd, where the agent runs against real off-market single-family deals — property analysis, buyer matching, and the prep work ahead of disposition. Real money moves at the end of that pipeline, which is why this incident mattered more than the runtime would suggest.

01 · The design

What the agent is allowed to decide — and why deal size isn’t the gate

Before the failure, the design, because it’s what determined the blast radius.

The agent reads a deal, pulls the data it needs through a set of typed tools, and produces structured output an operator works from: a summary, a set of candidate buyers, an explicit list of what’s missing. It does not message buyers. It does not silently mutate deal state.

The interesting decision isn’t that there’s a gate — everyone builds a gate. It’s what the gate is measured on. The obvious choice is deal value: above some dollar line, a human signs off. We don’t do that, and I’d argue against it.

Deal size doesn’t correlate with the agent’s uncertainty. A large deal with complete, fresh inputs is a job the agent does well. A small deal missing condition data and working from stale comps is where it produces something confident and wrong. Gating on dollars gets that exactly backwards: it applies scrutiny where the system is most reliable, and waves through the cases most likely to be garbage.

So the gate is on two things:

  • Input completeness. Required fields missing or stale means the agent stops and says so rather than inferring around the hole.
  • Scoring confidence. When the match score or the agent’s own confidence lands below the cutoff, it escalates instead of ranking.

Uncertainty about the inputs, and uncertainty about the output. Neither one is a dollar amount.

02 · The incident

How a renamed field became a silent AI agent failure

Contract drift

Contract drift is what happens when an upstream service changes shape — a field renamed, removed, or retyped — but the tool wrapper an agent depends on isn’t updated to match. The service is correct. The agent’s understanding of the service is not.

Ours drifted during normal development. A field moved — the kind of change that is entirely unremarkable inside a service boundary, gets reviewed in twenty seconds, and ships.

The tool wrapper the agent calls that service through didn’t change with it. From the agent’s side, a field it expected simply wasn’t there anymore.

Here’s the thing a normal service would have done: thrown. A null-pointer, a deserialization failure, a 500 with a stack trace, a red line on a dashboard. Loud, immediate, and traceable to the commit that caused it.

The agent did what agents are built to do. It reasoned over what it got. Missing input is not an exception to a language model, it’s just a slightly different prompt. It filled the gap with inference, produced well-formed structured output about a live deal, and the UI rendered it happily because the output shape was still valid. Every layer downstream was satisfied.

Service tests Passing
Output schema Valid
Operator UI Rendered
The answer Wrong

Every layer with an opinion reported healthy. The only thing that knew was the output itself, and nothing was checking that.

That’s the trap, and I don’t think it’s specific to us: the tolerance for messy input that makes an agent useful is the same property that makes contract drift invisible. You built a thing whose entire value proposition is “handle whatever you get.” Then you’re surprised when it handles a bug.

We caught it within hours, from an anomaly in production traces — output patterns that didn’t look like the day before. Not from a test. Not from an alert. From someone reading logs and noticing the shape of the traffic had shifted.

The cost was engineering time: tracing back from an odd output pattern to the actual source is meaningfully slower when nothing threw, because there’s no stack trace to follow and no failing test pointing at a commit. You’re reasoning backwards from plausible-looking output to a schema change three services away.

03 · The remediation

The fix: schema validation, contract tests, and confidence gating

Three things, in the order they matter.

1. Validate at the tool boundary, and fail closed

Every tool response is parsed against an explicit schema before the agent ever sees it. If it doesn’t conform, the agent receives a typed error — not partial data, not a best-effort object with holes in it.

response = call_tool("get_deal_snapshot", deal_id)

parsed = DealSnapshot.validate(response)      # explicit schema, not duck-typing
if parsed.failed:
    raise ToolContractError(                  # typed error, never partial data
        tool="get_deal_snapshot",
        missing=parsed.missing_fields,
    )

The agent catches ToolContractError and escalates. It never receives a half-populated object it could reason over.

This is the whole fix in one sentence: an agent should get an error, not a gap. Given an error it can retry, escalate, or tell the operator the tool is broken. Given a gap it will do the single most dangerous thing available to it, which is quietly work around the problem.

2. Contract tests against real pinned responses

Real captured payloads from each upstream, replayed in CI. When a service changes shape, the build breaks — at the boundary the agent actually depends on, which is not the same as the service’s own tests passing.

The service’s tests were green through all of this. They were testing the service, and the service was correct. Nobody was testing the contract, which is a different artifact with a different owner, and in an agent system it’s the one that carries the risk.

3. Completeness and confidence gating

Described above, and it earned its keep here. The gate is what kept the failure from reaching anything irreversible while the output was wrong. Guardrails don’t stop you writing bugs. They decide what a bug is allowed to touch.

04 · The honest part

Why we didn’t build an eval suite (and what caught the failure instead)

The public version of this story usually ends with “and that’s why we built a comprehensive eval suite.”

We didn’t, and I’d be lying if I claimed otherwise. What we have is observability on every run — full traces, flagged low-confidence cases, and enough structure in the logs to see when output patterns drift. That’s what caught this, and for a system whose inputs are real-world messy in ways your fixtures never are, production traffic is a richer signal than a curated test set.

But traces are detection, not prevention. They tell you something broke after it broke. The contract tests are the prevention half, and the honest sequencing is: we had detection first because it was cheap and general, and we added prevention at the specific boundary that hurt us. If I were starting an agent today I’d build both on day one, and I’d build the boundary validation before I wrote a single interesting prompt.

05 · Questions

Questions I get asked about this

What is contract drift in an AI agent system?

It’s when an upstream service changes shape — a field renamed, removed, or retyped — but the tool wrapper the agent depends on isn’t updated to match. The service is correct; the agent’s understanding of it isn’t. In a conventional consumer this crashes. In an agent it produces confident, well-formed, wrong output.

Why don’t eval suites catch this kind of failure?

Evals test the agent against inputs you already thought of. Contract drift changes the inputs themselves, underneath the test set. The agent’s reasoning is unchanged and the output shape is still valid, so a fixture-based suite has nothing to fail on.

Should you gate AI agent autonomy on transaction size?

No. Transaction size doesn’t correlate with the agent’s uncertainty — a large, well-documented job is one it does well, and a small one with missing inputs is where it goes wrong. Gate on input completeness and confidence instead.

How do you contract-test an AI agent’s tool calls?

Capture real upstream payloads, pin them, and replay them in CI against the tool wrapper’s schema. The service’s own tests don’t cover this: they verify the service, not the contract the agent depends on.

If you’re putting an agent in front of anything that matters

  • Schema-validate every tool response and fail closed. An agent given partial data will reason over it, confidently.
  • Contract-test the boundary, not just the service. Green service tests do not mean the agent’s contract held.
  • Gate autonomy on uncertainty — missing inputs, low confidence — not on transaction size. Deal size is not risk.
  • Instrument every run before you need it. The failure you can’t predict is the one you’ll find by noticing traffic looks different today.
  • Assume the quiet failures are the expensive ones. Anything that throws, you’ll fix this afternoon.

The models are getting better at not hallucinating. Nothing is getting better at noticing that your own API changed.


Work with me

This is the kind of failure I get hired to prevent, or to fix after the fact.

Fixed-scope sprint

4–8 weeks

An agent or automated workflow shipped to production, with the tool-boundary validation, contract tests, and approval gates that make it trustworthy.

Seed / Series-A teams with a workflow that should have been automated last quarter.

Fractional technical lead

Ongoing, part-time

Architecture, agent reliability, and technical direction without a full-time hire. Design calls, reviews, and hands-on work where it counts.

Teams shipping AI features who need someone senior on it continuously.

Tell me what’s breaking →

All writing  ·  ragul.sh