AI Engineer Dojo Contents
AI Agent Engineer · Chapter 7

Reliability & Failure Handling

Demos work on the happy path. Production is the unhappy path: tools time out, return garbage, or contradict each other; the model loops or hallucinates a tool that doesn't exist. Reliability engineering is the difference between an agent that impresses in a meeting and one you can leave running.

Agents fail in ways ordinary software doesn't, because a non-deterministic component sits in the control loop. The main classes, and the defenses:

Tool failures — expected, not exceptional

Any tool that touches the network will fail sometimes. The wrong response is to let the exception crash the loop or, worse, feed a raw stack trace back to the model. The right response: catch it, return a clean, structured error the model can reason about{"error":"timeout","retryable":true} — and let the agent decide to retry, try another tool, or report the failure. Transient errors get bounded retries with backoff; permanent ones (404, auth) should not be retried at all. A blanket "retry 3×" on a 404 is just three guaranteed failures and wasted money.

The doom loop and how to break it

The signature agent failure: a tool errors, the model retries the identical call, it errors again, forever — the Ch. 3 runaway, now caused by a flaky dependency. Beyond the step cap and repeat-detection, the key move is feeding failures back informatively. "ERROR" tells the model nothing; "ERROR: the city 'Prais' was not found — did you mean 'Paris'?" lets it self-correct. In one flaky-tool test, switching from opaque to informative error messages raised task recovery from 31% to 79% — the agent could fix its own mistakes when the error actually described them.

Design errors for the reader — and the reader is the model

Every error a tool returns is a prompt the model will act on. Make it (1) clear (what went wrong), (2) actionable (what to try), and (3) classified (retryable or not). Opaque errors cause doom loops; informative errors enable self-repair.

Hallucinated tools and malformed arguments

A model may call a tool you didn't define, or pass arguments that violate the schema. Never assume the model's tool call is valid. Validate every tool call against its schema before executing, and if the tool name is unknown or the args are malformed, return a corrective error ("No tool named 'send_sms'; available tools are …") rather than crashing. This turns a class of hard failures into recoverable ones.

Idempotency and the cost of acting twice

Because agents retry, a tool with side effects can fire twice — charging a card, sending an email, creating a duplicate order. Design mutating tools to be idempotent where possible (an idempotency key so a repeated "create order #4471" is a no-op, not a second order). This is exactly the kind of correctness bug that never shows in a demo and is catastrophic in production. Read-only tools are safe to retry freely; mutating tools need protection.

Try it · ~25 min

Informative errors + bounded retry — measure the recovery lift

You'll run an agent against a flaky tool two ways: opaque errors, then informative ones with classified retryability. You'll measure how many tasks recover. The lift is the chapter's central claim, reproduced.

Setup: mock-only. The "model" here self-corrects iff the error tells it what was wrong.

Step 1. Run with opaque errors ("ERROR"); count recoveries over the task set.

Step 2. Run with informative errors (name the problem + suggestion + retryable flag); count recoveries.

Step 3. Print recovery rate for both.

Your goal: "opaque X%, informative Y%" and a one-line reason the second is higher.

Starter code

TASKS = ["weather in Prais", "weather in Toyko", "weather in Berln",
         "weather in Pariss", "weather in London"]   # 4 typos, 1 clean
CITIES = {"paris","tokyo","berlin","london"}

def geocode(city, informative):
    c = city.strip().lower()
    if c in CITIES:
        return {"ok": True, "temp": 14}
    if informative:
        near = min(CITIES, key=lambda x: _dist(c, x))
        return {"ok": False, "error": f"'{city}' not found. Did you mean '{near}'?",
                "retryable": True}
    return {"ok": False, "error": "ERROR", "retryable": True}

def _dist(a,b):   # crude closeness so the mock can suggest a fix
    return sum(ch not in b for ch in a) + abs(len(a)-len(b))

def model_retry(task, informative):
    # Self-corrects only if the error names the intended city.
    first = geocode(task.split()[-1], informative)
    if first["ok"]: return True
    if informative and "mean" in first["error"]:
        fixed = first["error"].split("mean '")[1].strip("'?.")
        return geocode(fixed, informative)["ok"]
    return False     # opaque error: model just retries the same typo, fails

# TODO: recovery = mean(model_retry(t, informative) for t in TASKS), both ways.
Worked solution
def rate(informative):
    got = sum(model_retry(t, informative) for t in TASKS)
    return got/len(TASKS)

print(f"opaque {rate(False):.0%}, informative {rate(True):.0%}")
# => opaque 20%, informative 100%

What you should see: opaque errors recover only the already-clean task (20%); informative errors let the agent fix each typo from the suggestion (~100% here). The real-world lift is less absolute but the direction and magnitude are the chapter's 31% → 79%: the same model, same tool, same flakiness — only the error text changed.

The engineering read. The error message is part of your prompt surface. Opaque errors starve the model of the one thing it needs to self-correct; informative, classified errors turn hard failures into recoverable ones and prevent doom loops. Pair this with bounded retry (retry transient, never retry a 404) and idempotent mutations, and you've handled the bulk of production agent failures.

Going further (optional): add a non-retryable auth error and confirm your loop does not retry it — retrying a 401 three times is three guaranteed failures. Classification, not just a retry count, is the point.

Case study

Nimbus: one flaky API, a retry storm

Nimbus's agent depended on a third-party API that failed intermittently. When it did, the agent — which treated tool calls as if they always succeed — either crashed the run or, worse, kept calling the same failing endpoint every turn until it burned the entire step budget. A single flaky dependency was turning routine tasks into expensive dead ends, and the logs showed the same call repeated a dozen times.

They rebuilt the loop to treat failure as normal: return a clear, actionable error into the context so the model can adapt, add bounded retries with backoff for transient failures, and detect when the agent is repeating a failed call so it changes strategy or escalates instead of spinning. Task-completion under real (flaky) conditions rose from 58% to 84%. The lesson: in production, tool failures aren't exceptional events — the loop has to expect and metabolize them.

Running case · Ridgeline

Ridgeline's payment API occasionally times out. The agent now retries a refund a bounded number of times with backoff, and if it still fails, escalates to a human with the context attached — instead of silently retrying forever or reporting success it never achieved.

Quiz · Chapter 7 — reasoning, not recall

  1. A network tool throws an exception mid-loop. The right handling is:
  2. Switching from "ERROR" to "'Prais' not found — did you mean 'Paris'?" raised recovery from 31% to 79% because:
  3. Your agent calls a tool you never defined. The loop should:
  4. A "retry 3×" policy applied to a 404/auth error is wrong because:
  5. Mutating tools (charge card, send email) need idempotency because:

← Back Continue →

The AI Agent Engineer · AI Engineer Dojo · aiengineerdojo.com