Agentic AI · Module 28 of 41 · 11 concepts · about 19 min

Agent Reliability

Why agents fail and what to do about each failure: hallucination, wrong tools and bad parameters, invented tools, loops and repeated actions, bad plans, lost context, stale memory, cascading failures and partial execution; with the mitigations that work.

At work: The distance between an agent that works in a demo and one that works on the ten-thousandth real request, which is where most agent projects stall.

Why errors compound

What it is

An agent's task succeeds only if every step goes right. If each step succeeds with probability p, a task of n steps succeeds with probability about p to the power n.

  • A step that is right 95 percent of the time sounds reliable.
  • Twenty such steps in a row succeed only about a third of the time.
  • So long tasks need steps that are far more reliable than feels necessary, or ways to catch and correct mistakes along the way.

Why it matters

This arithmetic explains most agent disappointment.

  • A demo is a short task shown once. Production is long tasks, thousands of times.
  • There are three remedies, and every technique in this module is one of them: make each step more reliable, make the task shorter, and add checks that catch an error before the next step builds on it.
  • A check that catches half of all step errors is worth more than a small improvement in the model.

In the real world: A team celebrating '96 percent tool-call accuracy' found its fifteen-step tasks succeeded barely half the time; shortening the task to nine steps did more than any prompt change.

Visual example

Compounding

Each cell is the step rate raised to the number of steps.

Chance a whole task succeeds, by step reliability and task length
steps in the task90% per step95% per step99% per step
559%77%95%
1035%60%90%
2012%36%82%
501%8%61%

Read down a column: the same step reliability gives very different task reliability as tasks get longer.

Judging an agent by how often single steps look right. Measure whole tasks. A 97 percent step rate and a 60 percent task rate are the same system.

Hallucination

What it is

Hallucination is a model stating something false as if it were true. In agents it has four forms:

  • Invented facts: an order status, a policy rule or a price that no tool returned.
  • Invented actions: 'I've issued the refund' when no refund tool was called.
  • Invented identifiers: an order id, file path or URL that looks right and does not exist.
  • Unsupported conclusions: a claim that goes beyond what the evidence shows.

Why it matters

In an agent a hallucination is not only a wrong sentence: it becomes the input to the next step, and may become an action.

  • Ground every claim in a tool result: the prompt says to look up before stating, and tools return explicit 'not found'.
  • Verify the final answer against the run's tool results before sending it.
  • Never trust a claimed action: read the system of record to confirm what happened.
  • Validate identifiers against what tools actually returned in this run.

In the real world: A support agent occasionally told customers a refund had been issued when the tool had failed; a coded check comparing the reply with the action log removed every such case.

def verify_answer(answer: str, state) -> list[str]:
    """Checks in code that a reply matches what actually happened."""
    problems = []
    done = {a.name for a in state.actions if a.succeeded}
    if claims_refund(answer) and "refund_item" not in done and "refund_delivery_fee" not in done:
        problems.append("Reply claims a refund, but no refund tool succeeded.")
    for amount in amounts_in(answer):
        if amount not in state.amounts_seen:                  # every ₹ figure must come from a tool
            problems.append(f"Amount ₹{amount} does not appear in any tool result.")
    for order_id in order_ids_in(answer):
        if order_id not in state.order_ids_seen:
            problems.append(f"Order {order_id} was never returned by a tool.")
    return problems

Fighting hallucination only with 'do not make things up' in the prompt. It helps a little. What works is giving the model the facts through tools and checking its output against them.

Incorrect tool selection, invalid parameters and hallucinated tools

What it is

Three failures at the point of a tool request:

  • Incorrect tool selection: a real tool, but the wrong one. search_orders when the order id was already known.
  • Invalid tool parameters: the right tool with wrong inputs: a wrong type, a missing field, a date in the wrong format, an id from a different object.
  • Hallucinated tools: a request for a tool that does not exist.

Why it matters

Each has a mechanical remedy.

  • Selection: fewer, more distinct tools; descriptions that say when to use each and when not; tool routing so only relevant tools are shown.
  • Parameters: strict schemas, enums, described fields with formats, and validation that returns a precise error the model can correct from.
  • Invented tools: schema-constrained tool calling makes them rare; when one occurs, return an error naming the real tools.
  • All three should be counted as metrics: a rise after a change points straight at the cause.

In the real world: An agent chose the wrong one of two search tools a fifth of the time; merging them into one tool with a scope parameter took the error to near zero.

def check_call(call, state) -> str | None:
    """Return an error message the model can act on, or None if the call may run."""
    tool = REGISTRY.get(call.name)
    if tool is None:
        metrics.count("tool.hallucinated", tool=call.name)
        return f"There is no tool '{call.name}'. Available tools: {', '.join(sorted(REGISTRY))}."
    if problem := tool.validate(call.input):                       # JSON Schema
        metrics.count("tool.invalid_params", tool=call.name)
        return f"Invalid input for {call.name}: {problem}. Correct it and try again."
    if (oid := call.input.get("order_id")) and oid not in state.order_ids_seen:
        return (f"Order {oid} has not been returned by any tool in this conversation. "
                "Use get_recent_orders to find the right id.")
    return None

Treating a wrong tool choice as the model being 'not smart enough'. Read the two competing descriptions side by side. Usually a person could not tell them apart either.

Practise it: Design a Tool-Calling Agent Over 500 APIs

Infinite loops and repeated actions

What it is

Two related failures:

  • Infinite loops: the agent keeps taking steps without getting anywhere: the same call again and again, two calls alternating, or reasoning without acting.
  • Repeated actions: the same write is performed more than once: two refunds, three emails. Caused by loops, by retries after timeouts, or by a resumed run redoing a step.

Why it matters

Loops waste money; repeated writes do real damage.

  • Loops: step, cost and time limits; detection of identical or cycling calls; a corrective nudge; then a hand-over. And the root fix: tool results and errors that tell the agent what to do next.
  • Repeated writes: an idempotency key, a unique label sent with each write so the receiving service performs it once however many times it arrives. Derive it from the run and the step, so a retry carries the same key.
  • Also keep a ledger of completed actions in the run's state and check it before any write.

In the real world: A payment timeout made an agent retry a refund that had in fact succeeded; with idempotency keys the retry returned 'already refunded', and without them forty customers were paid twice.

Visual example

Repeats

The last row is the one that makes all the others harmless.

Why the same action happens twice, and what stops it
causewhat happensdefence
The agent loopsIt decides to refund againRepeat detection; action ledger in state
A timeoutThe call succeeded but the reply was lost; it retriesIdempotency key
A crash and resumeThe step is run again from the checkpointIdempotency key; checkpoint after each step
A duplicate eventThe queue delivers the message twiceEvent id check; per-conversation lock
Any of the aboveThe write arrives twice at the serviceThe service performs it once per key
def idempotency_key(run_id: str, call) -> str:
    """The same logical action always gets the same key, however often it is attempted."""
    return f"{run_id}:{call.name}:{stable_hash(call.input)}"

def refund_item(order_id: int, amount: int, *, idempotency_key: str) -> str:
    existing = payments.find_by_key(idempotency_key)
    if existing:                                        # a retry, a loop or a resumed run
        return f"Already refunded ₹{existing.amount} for order {order_id}. Nothing more was done."
    refund = payments.create_refund(order_id, amount, key=idempotency_key)
    return f"Refunded ₹{refund.amount} for order {order_id}."

def looping(actions: list) -> bool:
    recent = [(a.name, stable_hash(a.input)) for a in actions[-4:]]
    return (len(recent) == 4 and
            (len(set(recent)) == 1 or recent[:2] == recent[2:]))     # AAAA or ABAB

Generating a new random idempotency key on each attempt. The key must be the same on a retry, or it protects nothing. Derive it from stable facts about the action.

Bad planning and context loss

What it is

Two failures of direction:

  • Bad planning: the plan is wrong from the start (missing steps, impossible steps, wrong order), or becomes wrong and is followed anyway.
  • Context loss: the agent loses something it needed: the original goal, a constraint stated early, a result from ten steps ago. Causes: a long context, truncation, or a summary that dropped it.

Why it matters

Both produce an agent that works hard on the wrong thing.

  • Planning: validate plans in code before execution; give each step a completion check; replan on failure with a cap (module 7).
  • Context loss: restate the goal and constraints at the end of the context each turn; keep a to-do list in view; make compaction preserve ids, amounts, decisions and prohibitions (module 12).
  • A final check that the answer addresses every part of the original request catches both.

In the real world: A customer asked for a refund and a rescheduled delivery; the agent did the first and announced success. A completeness check on the final reply now catches the forgotten half.

COMPLETENESS_PROMPT = """The customer asked for the following. List each separate request.
Then, for each, say whether the transcript shows it was DONE, REFUSED WITH A REASON,
or NOT ADDRESSED. Return JSON: {{"requests": [{{"request": "...", "status": "..."}}]}}

Customer's original message: {goal}
What the agent did: {actions}
Final reply: {answer}"""

def unaddressed(goal: str, state, answer: str) -> list[str]:
    result = llm_small_json(COMPLETENESS_PROMPT.format(goal=goal, actions=state.action_log(), answer=answer))
    return [r["request"] for r in result["requests"] if r["status"] == "NOT ADDRESSED"]

Truncating a long context from the top to make it fit. The first things lost are the system prompt and the goal. Summarise the middle; keep the start and the end.

Practise it: Design a Multi-Agent AI System

Stale memory

What it is

Stale memory is acting on something remembered that is no longer true:

  • a long-term memory that has gone out of date: an old address, a cancelled membership;
  • a fact fetched early in a long task that changed while the task ran: stock, a price, an order's status;
  • a cached tool result past its useful life.

Why it matters

Stale facts are dangerous because they look exactly like current ones.

  • Record when and from where each memory came, and show that to the model.
  • Give memories and caches an expiry matched to how fast the fact changes.
  • Re-read before you write: any fact that an action depends on is fetched fresh immediately before the action, never taken from memory.
  • Replace, do not append, facts that have changed (module 5).

In the real world: An agent checked that an order had not been dispatched, spent a minute on other steps, then cancelled it; the rider had already left. The cancel tool now re-reads the status itself.

def cancel_order(order_id: int, user) -> str:
    """Re-read before writing: never act on a status remembered from earlier."""
    order = orders.get(order_id, user=user)             # fresh read, inside the tool
    if order.status != "preparing":
        return f"Cannot cancel: order {order_id} is now '{order.status}'."
    orders.cancel(order_id, expected_version=order.version)    # fails if it changed meanwhile
    return f"Order {order_id} cancelled."

def render_memory(memory) -> str:
    return f"- {memory.text} (noted {memory.created}, from {memory.source})"   # age is visible

Checking a condition at step 2 and acting on it at step 9. The world moved in between. The check belongs inside the tool that acts, at the moment it acts.

Cascading failures

What it is

A cascading failure is one error spreading until it takes down much more than its source:

  • within a run: a wrong fact at step 3 is built on for ten steps;
  • between agents: one agent's mistaken report is treated as fact by three others;
  • between systems: a slow dependency makes every run wait and retry, exhausting workers and rate limits, so everything slows.

Why it matters

The remedy is containment: stop an error where it starts.

  • Checks at boundaries: validate before a result is written to state, memory or another agent.
  • Timeouts on every call, so a slow dependency cannot hold a run for ever.
  • Circuit breakers: after repeated failures of a dependency, stop calling it for a while and fail fast.
  • Bulkheads: separate pools and limits so one tenant or one tool cannot consume everything.
  • Retry budgets: limited retries with backoff, so retries do not become the overload.

In the real world: A slow search service once tied up every agent worker in timeouts; a circuit breaker now turns that into 'search is unavailable, continuing without it' within seconds.

Visual example

Containment

Each box is a place a failure can be caught before it spreads.

Stopping an error where it starts

  1. Tool resultValidated before it enters context
  2. State and memoryChecked against sources
  3. Between agentsUnsupported claims dropped
  4. DependenciesTimeouts, circuit breakers
  5. Final answerVerified against the action log
class CircuitBreaker:
    """After repeated failures, stop calling a dependency for a while."""
    def __init__(self, threshold: int = 5, cool_off_seconds: int = 30):
        self.failures, self.opened_at = 0, None
        self.threshold, self.cool_off = threshold, cool_off_seconds

    def call(self, fn, *args):
        if self.opened_at and now() - self.opened_at < self.cool_off:
            raise DependencyUnavailable("Circuit open: failing fast instead of waiting.")
        try:
            result = fn(*args)
        except (TimeoutError, ConnectionError):
            self.failures += 1
            if self.failures >= self.threshold:
                self.opened_at = now()                 # open the circuit
            raise
        self.failures, self.opened_at = 0, None        # success closes it
        return result

Unlimited retries on a failing dependency. A thousand runs each retrying five times is five thousand extra requests to a service that is already struggling.

Partial execution

What it is

Partial execution is a task that stopped part-way: some actions done, others not. The refund was issued, and the confirmation email failed. Handling it has four parts:

  • Know what was done: an action ledger in the saved state.
  • Resume, do not restart: continue from the last checkpoint with idempotent tools.
  • Compensate where needed: an undo action for a step that must not stand alone, such as releasing a held booking. A sequence of steps each with an undo is called a saga.
  • Tell the truth: report exactly what was and was not done.

Why it matters

A multi-step task touching several systems cannot be made all-or-nothing the way a single database transaction can.

  • So the design goal is that any stopping point is known, safe, and recoverable.
  • Order the steps so the hardest to undo comes last.
  • A half-finished task reported honestly can be completed by a person; one reported as success becomes a complaint.

In the real world: A travel agent that booked the flight and failed on the hotel now releases the flight hold and tells the customer plainly; it used to report the trip as booked.

STEPS = [
    Step("hold_slot",    do=hold_delivery_slot,   undo=release_delivery_slot),
    Step("charge",       do=charge_reschedule_fee, undo=refund_reschedule_fee),
    Step("confirm_slot", do=confirm_delivery_slot, undo=None),      # hardest to undo: last
    Step("notify",       do=send_confirmation,     undo=None),      # failure here is tolerable
]

def run_saga(state) -> str:
    for step in STEPS:
        if step.name in state.completed:
            continue                                   # resuming: skip what is already done
        try:
            step.do(state, idempotency_key=f"{state.run_id}:{step.name}")
            state.completed.append(step.name); save(state)
        except PermanentFailure as error:
            for done in reversed([s for s in STEPS if s.name in state.completed and s.undo]):
                done.undo(state, idempotency_key=f"{state.run_id}:{done.name}:undo")
            return f"Could not reschedule ({error}). Nothing was changed and you were not charged."
    return "Your delivery is rescheduled."

A final message written by the model from its intentions rather than from the ledger. 'I've rescheduled your delivery and emailed you' when the email failed. Build the summary from recorded outcomes.

The reliability checklist

What it is

Every failure in this module, with its cause and its main defence, in one table. Use it to review a design before it ships.

Why it matters

Reliability comes from many small mechanical defences, not from one clever idea.

  • Almost none of them involve the prompt.
  • Each is cheap to add while designing and expensive to retrofit after an incident.
  • When an agent fails, find the row; the defence tells you what to build.

In the real world: A release review walks this table row by row; an agent with an empty cell does not ship until the cell has an answer.

Visual example

Catalogue

The right-hand column is a design checklist.

Agent failure modes and their mitigations
failuretypical causemain mitigation
HallucinationAnswering from memory, not toolsGround in tool results; verify the answer in code
Incorrect tool selectionOverlapping descriptions; too many toolsFewer, distinct tools; tool routing
Invalid tool parametersLoose schemasStrict schemas, enums, precise errors
Hallucinated toolsVague or long tool listsConstrained tool calls; error lists real tools
Infinite loopsUnhelpful tool results; impossible taskLimits, repeat detection, hand-over
Repeated actionsRetries, loops, resumesIdempotency keys; action ledger
Bad planningPlan made before facts were knownPlan validation; replanning with a cap
Context lossLong context; lossy summariesRestate the goal; structured compaction
Stale memoryFacts change over timeTimestamps, expiry, re-read before write
Cascading failuresNo containmentBoundary checks, timeouts, circuit breakers
Partial executionA failure mid-taskCheckpoints, compensation, honest reporting

Interview questions: agent reliability

What it is

The questions interviewers ask about agent reliability, from the first screening call to the senior round. Cover the answer, say yours aloud, then compare.

Why it matters

Reading an idea and explaining it are different skills.

  • Each answer below is the two or three sentences an interviewer is listening for.
  • The reasoning behind each one is in the cards above. If an answer surprises you, go back to that card.

In the real world: A candidate asked what happens when a refund call times out says 'idempotency key' before the interviewer finishes the sentence, and the rest of the interview is about design, not basics.

Q: Why do agents fail on long tasks?

Errors compound: task success is roughly per-step success raised to the number of steps. At 95 percent per step, twenty steps succeed about a third of the time. Fix by more reliable steps, shorter tasks and checks between steps.

Q: How would you prevent infinite agent loops?

Hard limits on steps, cost and time; detection of identical or alternating calls; one corrective nudge; then a hand-over with a summary. And fix the cause: tool results and errors that say what to try next.

Q: How do you prevent the same action happening twice?

Idempotency keys derived from the run and the action, so a retry, a loop or a resumed run performs the write once; plus an action ledger in state checked before any write.

Q: How do you reduce hallucination in an agent?

Give it facts through tools, make empty results explicit, require looking up before stating, and verify the final answer in code against tool results and the action log. Confirm actions in the system of record.

Q: The model calls a tool that does not exist. What happens?

The executor returns an error result listing the real tools; the model corrects itself. It is counted as a metric, and strict tool schemas plus a shorter tool list reduce its frequency.

Q: What is a cascading failure and how do you contain it?

One error spreading through a run, between agents or between systems. Contain it with validation at boundaries, timeouts, circuit breakers, bulkheads between tenants and bounded retries.

Q: A task fails half-way through. What do you do?

Know what was done from the ledger, resume from the checkpoint with idempotent tools, compensate for steps that must not stand alone, order steps so the least reversible is last, and report truthfully.

Q: What is stale memory and how do you avoid acting on it?

A remembered fact that is no longer true. Timestamp and expire memories, and re-read any fact an action depends on inside the tool, immediately before acting.

Proposing to fix reliability by 'improving the prompt'. Of eleven failure modes, the prompt is the main defence for none.

Exercise: break your agent eleven ways

What it is

A short build that uses this module's ideas. Write it yourself before looking anything up; the checks at the end tell you whether it works.

Why it matters

An idea you have only read fades in a week.

  • Building it once, even in fifty lines, is what makes it yours.
  • Interviewers can tell the difference between someone who has read about a loop and someone who has debugged one.

In the real world: A payments company runs this fault-injection suite on every release of its agent; no change ships if any fault produces a duplicate write.

Your task. Inject each failure mode from the catalogue into your agent, watch it fail, add the defence, and confirm the failure is contained.

Steps

  1. Write a fault-injection wrapper for your fake tools that can: time out, return an empty result, return an error, succeed but lose the reply, and return stale data.
  2. Make refund_item succeed but raise a timeout on the first attempt. Confirm a double refund occurs, then add idempotency keys and confirm it does not.
  3. Make a search tool always return 'Error'. Confirm a loop, then add repeat detection and a helpful error message.
  4. Change an order's status between the agent's check and its cancel call. Add a re-read inside the cancel tool.
  5. Crash the process after the charge step of a three-step action. Add a ledger and compensation.
  6. Add the answer-verification check and feed it a reply that claims an action that did not happen.
  7. Compute your task success rate over twenty runs with faults enabled at 10 percent, before and after the defences.

You are done when

  • Each injected fault has a test that fails without the defence and passes with it.
  • No fault you can inject produces a duplicate write.
  • Every forced stop ends with an accurate account of what was and was not done.
  • You have before-and-after task success rates under injected faults.

Stretch. Add a circuit breaker to one tool and show the agent degrades gracefully when the dependency is fully down.