Agent Reliability Engineering Design Guide - Retries, Loop Detection, Timeout Budgets, and Human Escalation for AI Agents
First Published:
Last Updated:
This article is about that second artifact. It is a design guide for the reliability of the agent execution loop: how a task is declared finished, how much the agent is allowed to spend before it must stop, how you notice that it is going in circles, what "retry" even means when the thing you are retrying is a decision rather than a request, how to keep a repeated tool call from charging a customer twice, when to hand the work to a person, and how to resume afterwards.
1. Introduction
1.1 Who this is for
This guide is written for the engineers, SREs, and platform teams who have an agent in production, or are about to. The assumed starting point is an agent that already works: it calls the right tools, it produces good output on a good day, and the remaining problem is everything else. If you are still choosing a framework or designing the tool surface, the Enterprise AI Agent Design Notes series is the better starting point; come back here once the agent runs unattended.No familiarity with any particular framework is assumed. Where a concrete mechanism helps, this article names the product that implements it, but always as one example of a design decision rather than as a recommendation.
1.2 A note on the term "agent reliability engineering"
This article uses "agent reliability engineering" as a convenient label for a design area, not as the name of an established discipline. The phrase appears in a small number of independent practitioner write-ups, generally in the same sense used here — applying the vocabulary of reliability engineering to agent systems — but there is no standards body behind it, no canonical definition, no shared curriculum, and no body of practice that the term reliably points to. A search of arXiv for the exact phrase returns nothing.Nothing here depends on the label; the section headings are the actual subject matter. The only reason to name the area at all is that its pieces — budgets, guards, escalation, idempotency, resumability — are usually documented one at a time, framework by framework, and the interactions between them are where production systems actually break.
Where this article does borrow established vocabulary — error budgets, cascading failure, idempotency, load shedding, blameless postmortems — it borrows it from the published reliability literature and cites it, because those concepts have precise meanings that predate agents and are worth keeping precise.
1.3 What this article covers, and what it deliberately does not
Reliability work on a generative AI system splits into layers, covered here in separate articles. The split is worth stating explicitly because the vocabulary overlaps: "retry", "timeout", "idempotency", and "fallback" all appear at more than one layer and mean different things at each.* You can sort the table by clicking on the column name.
| Layer | Concerns | Where it is covered |
|---|---|---|
| Inference call | SDK retry modes, exponential backoff and jitter, retry quotas, circuit breakers, connect and read timeouts, interrupted stream recovery, deduplicating the handler that wraps a single call | LLM Inference Resilience Patterns on AWS — not repeated here |
| Agent execution loop | Termination conditions, step and token and time budgets, loop and stagnation detection, retry scope, side-effect idempotency, budget layering, human escalation, degradation, checkpoint and resume, multi-agent containment, incident response, reliability testing | This article |
| Output correctness | Whether a produced answer is right: structural checks, grounding, self-verification, cross-model review, citation enforcement | LLM Output Verification Patterns |
| Instrumentation | Span and metric naming, attribute conventions, what to emit and in what shape | OpenTelemetry GenAI Semantic Conventions Implementation Guide |
| Security | Prompt injection, tool abuse, egress control, layered defenses | AI Agent Defense in Depth Model |
| Memory | What the agent remembers, where, for how long, and with what retrieval semantics | AI Agent Memory Design Guide |
The practical consequence of the first row is worth stating plainly: if your problem is that a single model call occasionally throttles or drops mid-stream, this is the wrong article. That problem is solved by client configuration and a circuit breaker, and the companion article covers it in implementation detail. This article assumes the individual call is already as reliable as it is going to get, and asks what happens when the agent makes forty of them in sequence, each one deciding what the next one will be.
Also deliberately out of scope: cost modelling, benchmark scores, model comparison, SDK tutorials, and organisational process. Where the cost of a reliability decision matters it is described in technical terms — added latency, additional token consumption, increased side-effect risk — because those are what you can measure in your own system.
1.4 How specifications are cited here
Agent runtime behaviour moves quickly, and several mechanisms below were renamed, relocated, or given new defaults during 2026. Every specification, API, and default value named here was checked against its primary source on 2026-07-31, with the version named inline wherever one exists. Two are worth flagging at the outset because their published location changed recently:- The Model Context Protocol revisions cited here are from specification version
2026-07-28, in which cancellation and progress moved underbasic/patterns/. - The OpenTelemetry generative AI semantic conventions have moved out of the main semantic-conventions repository into a dedicated GenAI semantic conventions repository; the page in the original location is marked as no longer maintained. Instrumentation detail is deferred to the companion article, but if you are reading an older guide that points at the previous location, that is why.
Nothing here should be treated as a stable API contract. Treat named defaults as illustrations of a design pattern, and re-check the current value before you depend on it.
1.5 The shape of the problem
The figure below is the map for the rest of the article. It shows the agent execution loop with the three things that this guide argues must be designed rather than inherited: a budget admitted before the loop starts, guards evaluated on every pass, and a termination check whose outcomes include more than "done" and "error".
2. How Agents Fail Differently
Everything in classical service reliability still applies: a tool call is a remote call, and an unbounded retry loop is still the fastest way to turn a small problem into an outage. What follows is not a replacement for that knowledge but the set of properties it has no ready answer for, because they do not arise when the caller is a deterministic program.2.1 Non-determinism is in the control flow, not just the output
In a conventional service the code path is fixed and the data varies. In an agent, the path varies: two runs of the same task with the same inputs can call different tools, in a different order, a different number of times. The consequence that catches people out is that a retry is not a repeat — re-running a failed step can produce a different plan, take a different action, and succeed at something other than what the first attempt was attempting.It also has a consequence for measurement. A single successful run tells you very little. The reliability question is about the distribution of outcomes over repeated attempts, which is why the τ-bench paper (arXiv 2406.12045, v1 2024-06-17) introduced a metric for consistency across repeated trials of the same task rather than reporting a single pass rate. Whatever you measure internally, measure it that way.
2.2 Partial success is the normal outcome, not the edge case
A task is a sequence of externally visible effects. A conventional request either applies its effect or does not; with an idempotency key you can make that guarantee crisp. An agent task of seven steps can stop after three, having already sent an email, created a ticket, and left a database row half-updated. There is no single "did it happen" bit to consult.This is not a rare failure. Work on realistic, multi-step office tasks — for example TheAgentCompany (arXiv 2412.14161, v1 2024-12-18) — reports that agents frequently complete part of a task and not the whole of it, and the authors score partial progress separately from full completion precisely because full completion is not the common case. Design accordingly: partial completion is a first-class outcome that needs a representation, a reporting path, and a recovery path. If your API can only return "result" or "error", you will end up encoding half-done work as one of those two, and both choices are wrong.
2.3 The agent is also the reporter
An agent that says "I have updated the records" is making a claim about the world using the same faculty it used to act on it. That is not a measurement, and it is what makes agent monitoring different in kind: the most convenient place to read status from is the least trustworthy.The research picture here is worth stating carefully, because it is often flattened in both directions. Papers such as Self-Refine (arXiv 2303.17651, v1 2023-03-30) and Reflexion (arXiv 2303.11366, v1 2023-03-20) report that iterative self-critique improves results when the critique is grounded in some feedback signal. In the other direction, "Large Language Models Cannot Self-Correct Reasoning Yet" (arXiv 2310.01798, v1 2023-10-03) reports that when a model is asked to revise its reasoning using only its own judgement, without external feedback, performance can get worse rather than better. The engineering reading of both results is the same: self-assessment is a hypothesis; it becomes evidence only when checked against something outside the model.
Concretely, "the agent said it succeeded" should never be the signal that closes a task. The signal should be a check the agent did not author: the row exists, the file has the expected hash, the ticket is in the expected state, the external API returns the new value. Where the correctness of a produced answer — rather than the completion of an action — is what needs checking, that is the subject of the companion article on LLM Output Verification Patterns.
2.4 The objective is restated on every pass
The goal lives in natural language inside a context window that grows, gets truncated, gets summarised, and fills with the debris of failed attempts. Every step is an opportunity to re-interpret it. The symptoms are familiar from any long agent run: it starts optimising a sub-goal it invented, substitutes a task it can do for the task it was given, or quietly narrows the scope and reports success against the narrowed version.Goal drift is not primarily a prompting problem. It is a state-management problem: the invariant part of the task should live somewhere that cannot be edited by the loop, and the completion check should be written against that invariant, not against the agent's current restatement of it.
2.5 The blast radius is the tool surface, not a code path
In a conventional system you can read the code and enumerate the writes. In an agent, the set of possible actions is the cross product of the tool surface and the model's judgement. A tool that can send email can send any email; a tool that can run shell commands can do anything the process can do. The reliability consequence — distinct from the security consequence, which the defense in depth article covers — is that you cannot bound the damage of a malfunctioning loop by reasoning about the loop. You bound it by constraining the tools. Reversibility, rate limits, dry-run modes, and approval gates are reliability features, not just security features.2.6 Multi-agent systems add failure modes that no single agent has
Delegation introduces failures that are properties of the arrangement rather than of any participant. The study "Why Do Multi-Agent LLM Systems Fail?" (arXiv 2503.13657, v1 2025-03-17) collects failures from multi-agent systems and organises them into a taxonomy whose top-level categories include problems with the specification given to agents, misalignment between agents during execution, and inadequate verification or premature termination of the overall task. The paper's framing is the useful part for design work: a large share of multi-agent failures are organisational rather than cognitive — unclear contracts, mismatched assumptions, nobody responsible for deciding the work is finished. Section 11 takes this up in detail.2.7 Summary of the difference
* You can sort the table by clicking on the column name.| Property | Conventional service | Agent execution loop |
|---|---|---|
| Control flow | Fixed by code; data varies | Chosen per step by the model; varies between runs |
| Retry semantics | Repeat the same request | Re-run a decision, which may choose differently |
| Failure granularity | Applied or not applied | Any prefix of the intended effects may have applied |
| Status signal | Return code from the executor | Self-report from the actor; requires external corroboration |
| Objective stability | Encoded in code, immutable at runtime | Restated in context every pass; can drift |
| Blast radius | Enumerable from the source | Bounded only by the tool surface and its guards |
| Termination | Function returns | A judgement that must be designed, budgeted, and guarded |
3. Defining Success and Termination
Almost every runaway agent story reduces to the same root cause: nobody wrote down what "finished" means, so the loop had nothing to compare against and kept going. Termination is the first design decision, not the last.3.1 Three ways a loop should be able to end
A production loop needs all three of these, and they are independent:- Goal satisfaction — an externally checkable condition says the work is done.
- Budget exhaustion — the agent has spent what it was allotted and must stop regardless of progress.
- Guard activation — something has gone wrong in a way that makes further steps pointless or unsafe (a loop, a policy violation, an unrecoverable tool failure).
Implement only the first and the loop hangs; only the second and it burns a full budget on a task that became impossible at step two; only the third and it never finishes cleanly. Each needs its own outcome path, which is why the figure in Section 1.5 has four terminal boxes rather than two.
3.2 Success criteria must be checkable by something other than the agent
Write the completion condition as a predicate over observable state, evaluated by code you control. "The report has been written" is not a predicate; "a file exists at the agreed path, is non-empty, parses as the agreed schema, and contains a section for each requested topic" is. When the deliverable is prose rather than structured data, the predicate becomes a verification step rather than an assertion — which is where the companion article on LLM Output Verification Patterns picks up.Two rules make this workable in practice. First, fix the criteria before the run starts and store them outside the mutable context, so the loop cannot rewrite the exam it is sitting. Second, make the criteria the same object the agent is shown, so it is optimising against the real target rather than an approximation of it. Some managed platforms formalise exactly this: a task can be submitted with an explicit rubric and an iteration cap, and a separate grader — running in its own context — scores each attempt and either accepts it or sends it back with the gaps identified. Whether you use a platform feature or build it yourself, the shape is what matters: the thing that decides "done" should not be the thing that did the work.
3.3 Budgets: three currencies, and they do not convert
A budget is a promise about the maximum resources a task may consume. Agents need three, and they constrain different failure modes:- Steps (or turns, or iterations, or tool calls) — bounds structural runaway: the loop that keeps going. This is the one that catches "the agent tried the same fix nineteen times".
- Tokens — bounds context and generation growth. This is the one that catches "each step re-reads the whole file, and the context is now enormous".
- Wall-clock time — bounds the user's and the platform's patience. This is the one that catches "the agent is not looping, it is just waiting on a tool that will never answer".
An agent can exhaust any one of the three while barely touching the others, so one budget is not a substitute for the rest: a step budget does not bound wall-clock time (one step can block for an hour), and a wall-clock budget does not bound token consumption (a fast loop can burn a context window in seconds).
3.4 The ceiling the runtime enforces versus the ceiling the model is told about
This distinction is subtle, easy to miss, and changes how an agent behaves when it approaches a limit.An enforced ceiling is applied by the runtime. When it is reached, generation or iteration stops, and whatever the agent was doing is cut off mid-thought. The agent does not know the ceiling exists and cannot plan around it, so the typical result of hitting one is a truncated, unusable artifact.
An advertised budget is told to the model, which can then pace itself: prioritise, cut scope, and finish gracefully rather than being interrupted. The Anthropic API implements this distinction explicitly —
max_tokens is a hard per-response ceiling the model is not aware of, while a task budget (beta, minimum 20,000 tokens) injects a countdown the model can see during generation, so it can wind the task down as the budget depletes. The documentation is direct about the difference in intent: use the advertised budget when you want the model to self-moderate, and the enforced ceiling as the hard cap.The design rule that follows applies whether or not your provider offers the feature: every enforced ceiling should have a corresponding advertised budget set somewhat below it, so that the normal path to termination is a graceful wind-down and the enforced ceiling is the backstop that should rarely fire. If your platform has no way to advertise a budget, approximate it — inject the remaining step count into the prompt each pass, and instruct the agent to produce its best partial deliverable when the remainder gets small.
3.5 What the runtimes actually count
Agent frameworks all ship a structural bound, and every one of them counts a different unit. This is the most common source of confusion when porting a budget from one system to another, so it is worth tabulating. All values below were checked on 2026-07-31 against the vendors' own documentation.* You can sort the table by clicking on the column name.
| Runtime | Control | Unit counted | Behaviour at the limit |
|---|---|---|---|
| OpenAI Agents SDK (Python) | max_turns | Turn (one model call plus the tool calls it triggers) | Raises MaxTurnsExceeded. Passing max_turns=None disables the limit entirely |
| Google Agent Development Kit | RunConfig.max_llm_calls | LLM call | Default 500. Setting 0 or a negative value means unlimited, which the documentation explicitly does not recommend for production |
| LangGraph | recursion_limit | Super-step (one iteration over the graph; parallel nodes share a super-step) | Raises GraphRecursionError. Default 1000 as of version 1.0.6 |
| Anthropic API tool runner | Iteration cap on the runner | Runner iteration | Loop stops; the last message is returned |
| Anthropic API server-side tools | Server-side loop limit | Server-side sampling iteration | Returns stop_reason: "pause_turn" after a default of 10 iterations; the caller re-sends to resume |
Four observations matter more than the individual numbers.
The units are not interchangeable. A LangGraph super-step containing five parallel tool nodes is one super-step and five tool calls. An Agents SDK turn may contain several tool calls. If you migrate a budget of "50" from one runtime to another without converting the unit, you have silently changed the bound by an order of magnitude.
Some defaults are effectively "no limit" for reliability purposes. A default in the hundreds or thousands exists to stop infinite recursion, not to bound a task. It is a backstop against a bug, not a budget. Treat framework defaults as the outer fence and set your own bound well inside it.
An "unlimited" setting is a decision, not an absence of one. Both the Agents SDK and ADK let you disable the cap; ADK's documentation flags the production risk in the same sentence that describes the option. If your codebase has one of these set to unlimited, that should be a documented, reviewed choice with a compensating control elsewhere — typically a wall-clock deadline enforced by the caller.
Not every stop is a failure. The
pause_turn row is a useful reminder: a runtime can stop a loop with the explicit intent that the caller resume it. Handling that as an error produces a silently truncated answer with no exception to alert on. Enumerate your runtime's stop reasons and classify each one as terminal or resumable before you write the loop.3.6 Choosing budget values without inventing them
There is no correct universal number, and any article that gives you one is guessing about your workload. Use a procedure instead:- Measure the legitimate distribution. Run your real task suite and record the steps, tokens, and wall-clock time consumed by the runs that succeeded. Failed runs are contaminated by the behaviour you are trying to bound.
- Set the advertised budget above the tail of that distribution, with enough headroom that legitimate long tasks are not truncated. The cost of setting it too low is a task that degrades unnecessarily; the cost of setting it too high is wasted spend on tasks that were never going to finish.
- Set the enforced ceiling above the advertised budget, so it only fires when the wind-down itself misbehaves.
- Alert on the ratio, not the count. The number of budget exhaustions is meaningless on its own; the fraction of tasks ending in budget exhaustion, tracked over time, tells you whether the workload or the model has changed.
- Re-derive after any model, prompt, or tool change. Step counts are highly sensitive to all three. A model that reasons more per step may need fewer steps and more tokens; a new tool that returns bigger payloads shifts the token distribution without touching the step distribution.
Budgets should also be per-task and inherited, not global constants. A task submitted with a five-minute user-facing deadline and a task submitted to a nightly batch queue have different correct answers, and if the budget is a module-level constant you cannot express that.
4. Loop and Stagnation Detection
A step budget guarantees the loop stops. It does not guarantee the loop stops soon. If an agent enters a repetitive state on step 4 of a 60-step budget, a budget-only design burns 56 steps of latency, tokens, and tool load before doing anything about it — and every one of those steps is an opportunity to perform an unwanted side effect. Detection exists to shorten that gap.4.1 Three distinct phenomena that get called "looping"
* You can sort the table by clicking on the column name.| Phenomenon | What it looks like | Signal to compute |
|---|---|---|
| Exact repetition | The same tool is called with the same arguments, repeatedly | Fingerprint of (tool name, canonicalised arguments); count occurrences in a sliding window |
| Stagnation | Actions differ superficially but nothing changes: the same file is read, the same search is rephrased, the plan is restated | A progress predicate you define: has any tracked state advanced since step n? |
| Cycling | The agent alternates between two or more states: fix A breaks B, fix B breaks A | Fingerprint of the observed state; detect a repeat of a previously seen state hash |
They need separate detectors because they have separate remedies. Exact repetition usually means the agent has not registered that an action failed — the fix is to make the failure explicit in the observation. Stagnation usually means the agent lacks the information or the capability to proceed — the fix is escalation, not another attempt. Cycling usually means the task has conflicting requirements — the fix is to surface the conflict to a human, because more attempts cannot resolve it.
4.2 Canonicalising the fingerprint
Naive fingerprinting fails for a reason that is specific to this domain: tool arguments arrive as model-generated JSON, and semantically identical arguments can be serialised differently between attempts. Key order can vary, whitespace can vary, and escaping can vary — Anthropic's documentation warns explicitly that models in current families may produce different Unicode or forward-slash escaping in tool-call inputs, and instructs callers to parse the input rather than string-match the serialised form. A detector that hashes the raw argument string will therefore miss real repetitions.Canonicalise before hashing:
import hashlib, json
VOLATILE = {"request_id", "timestamp", "trace_id", "attempt", "nonce"}
def call_fingerprint(tool_name: str, arguments: dict) -> str:
"""Stable identity for 'the agent tried this exact thing'."""
cleaned = {k: v for k, v in arguments.items() if k not in VOLATILE}
# sort_keys makes key order irrelevant; separators removes whitespace variance.
canonical = json.dumps(cleaned, sort_keys=True, separators=(",", ":"),
ensure_ascii=False)
return hashlib.sha256(f"{tool_name}\x00{canonical}".encode("utf-8")).hexdigest()
Two refinements are usually worth adding. Normalise semantically irrelevant fields that are not volatile but are noise for this purpose — a free-text
reason or comment argument that the model rewrites each attempt will otherwise defeat the detector while the actual action is unchanged. And fingerprint the result as well as the call, because "same call, same error" is a much stronger signal than "same call" alone, and it distinguishes a genuine retry loop from legitimate repeated polling.4.3 Defining progress
Stagnation detection requires a definition of progress, and this is the part you cannot outsource to a framework, because progress is domain-specific. The definition should be a small, explicit set of facts whose change counts as advancing:def progress_key(state) -> tuple:
"""Anything in this tuple changing counts as progress.
Keep it small, observable, and owned by the harness -- not by the agent."""
return (
len(state.files_written),
len(state.records_updated),
state.checklist_items_completed,
state.current_subgoal_id,
)
Deliberately absent from that tuple: anything the agent asserts about itself. "The agent said it made progress" and "the agent produced more text" are not progress. If the only thing that changed since the last check is the length of the transcript, that is the definition of stagnation.
4.4 Threshold design: window, tolerance, and response
A detector needs three parameters, and they should be set independently:- Window — how many recent steps are examined. Too short and you miss a three-cycle alternation; too long and stale history keeps a resolved situation flagged.
- Tolerance — how many occurrences within the window are permitted before the guard fires. This must be greater than one. Legitimate repetition exists: polling for an asynchronous result, paginating through a list, and re-attempting after a transient error all look like repetition and all are correct.
- Response — what happens when it fires, which should be graduated rather than binary.
The graduated response is the part most implementations skip, and it is where most of the value is. A detector wired directly to "abort" trades one bad outcome for another. A better ladder:
- Inform. Inject an explicit, factual observation into the loop: "The call
search(query='x')has now been made three times with an identical result. That result will not change." This alone resolves a large share of repetitions, because the underlying cause is often that the failure was not visible to the agent in the first place. - Constrain. Remove the repeated action from the available choices for the next few steps, or require a different tool. This forces a change of approach without ending the task.
- Escalate or terminate. If the loop persists through both, further steps are very unlikely to help. Hand off with the evidence (Section 8) or stop cleanly (Section 9).
Instrument each rung. The rate at which each level fires is one of the most informative reliability signals an agent system produces, and it moves immediately when a model, prompt, or tool changes.
4.5 Detectors get evaded, not defeated
An agent that is told "you have called this three times" will sometimes respond by changing the call trivially — adding a space to a query string, reordering a filter, appending a comment — and then continuing the same unproductive behaviour with a fresh fingerprint. This is not adversarial intent; it is a plausible local response to the feedback it was given. The countermeasures are the same ones used against trivial-variation problems everywhere:- Canonicalise aggressively, including case, whitespace, and argument ordering.
- Fingerprint at a coarser semantic level in parallel — for example the tuple (tool name, target resource identifier), ignoring the rest — and run both detectors.
- Keep the progress predicate (Section 4.3) as the backstop. It is immune to argument variation, because it looks at the world rather than at the call.
This is also the reason the framework-level counter is not a detector.
max_turns, recursion_limit, and max_llm_calls cannot be evaded — but they also fire only at the end. They are the fence at the edge of the property, not the alarm on the door.5. Retry Semantics for Agents
"Retry" is the most overloaded word in this area. Before deciding a retry policy, decide what is being retried, because the safety analysis is completely different at each scope.5.1 Four scopes, four different questions
* You can sort the table by clicking on the column name.| Scope | What is repeated | Precondition for safety | Owner |
|---|---|---|---|
| Model call | The same request to the model | Error is classified retryable; backoff and a retry quota are in place | Inference resilience layer |
| Tool call | The same call with the same arguments | The tool is idempotent, or the call carries a deduplication key | Tool wrapper |
| Step | The decision plus the action it produced | Any side effect from the failed attempt is compensated or reconciled first | Agent loop |
| Task | The whole plan, from a checkpoint or from scratch | External state is known and consistent; prior partial effects are accounted for | Orchestrator |
The interesting scopes are the bottom two, because they are the ones with no direct analogue in conventional systems.
5.2 Replay versus re-decide
Retrying a step can mean two quite different things, and conflating them causes real incidents.Replay re-issues the same action with the same arguments. It is deterministic, it is cheap, and its safety reduces entirely to the idempotency of the tool. This is the right choice when the step failed for a transport reason — the tool timed out, the connection dropped, the service returned a retryable error.
Re-decide puts the failure back into context and asks the agent what to do next. It is the right choice when the failure was semantic — the arguments were wrong, the resource did not exist, the approach was mistaken. But it has a property replay does not: the second attempt can do something completely different from the first, including something that conflicts with a side effect the first attempt already committed. The check in the decision diagram below exists for exactly this reason.

5.3 Getting back to a known state
There are exactly three honest options after a step that may have committed a partial effect, and picking one per tool at design time is far easier than deciding in the middle of an incident:- Compensate. Apply a semantic inverse: cancel the order, delete the created row, revoke the grant. This requires the inverse to exist and to be safe, which is a property of the tool, not of the agent. Note that a compensating action is itself an action that can fail, so it needs its own retry and its own idempotency key.
- Reconcile. Do not attempt to undo; instead read the current state of the world and converge toward the intended state. This is usually the more robust choice for anything with an authoritative external source, because it is correct regardless of how much of the previous attempt applied.
- Refuse. Some effects cannot be undone or reconciled — an email has been read, a payment has cleared, a message has been posted to a public channel. For these, the correct policy is that the step is not retried automatically at all: it escalates.
Record the choice per tool in the tool registry, next to the schema, so the loop can look it up rather than infer it.
5.4 Retries must consume budget
If a retried step does not decrement the step budget, the budget does not bound anything: a loop that fails and retries indefinitely never advances the counter. This sounds obvious and is violated constantly, usually because the retry is implemented inside the tool wrapper where the budget is not visible.The same applies at every layer, and the multiplication is easy to underestimate. Client SDKs commonly retry automatically — the Anthropic SDKs, for example, default to retrying a request twice, and the documentation notes that because timeouts are themselves retried, the wall-clock time a single call can consume approaches the timeout multiplied by the attempt count. Layer that under a step-level retry and a task-level retry and the worst-case wall-clock time is the product of three factors, not the sum. Section 7 covers the arithmetic.
5.5 What to do with the failure in context
A failed step leaves an artifact: an error observation sitting in the agent's context, which will influence every subsequent decision. Two failure modes result, and they pull in opposite directions.Keeping every failure in context is informative — it is how the agent learns not to repeat the action — but a long run accumulates a context dominated by errors, which biases the agent toward pessimism and consumes the token budget. Removing failures keeps the context clean but risks the agent cheerfully repeating the removed mistake.
The mechanisms for managing this exist at the API level and are worth knowing by name, because rolling your own is easy to get subtly wrong. Context editing clears old tool results (and optionally the tool inputs) from the transcript without summarising, and compaction summarises earlier context server-side when it approaches a threshold. Both are beta features on the Anthropic API as of the 2026-07-31 check; equivalent mechanisms exist in other stacks under other names. The reliability-relevant caveat applies to all of them and is covered in Section 10.6: a summary can drop the fact that a side effect occurred. Keep the record of what the agent did to the world outside the conversation, where no summarisation step can reach it.
A workable default: retain the most recent failure per distinct action fingerprint, drop older duplicates, and never drop the side-effect ledger.
6. Idempotency and Side-Effect Safety
Everything above assumes you can retry without doing damage. This section is how that assumption is earned. It is the single highest-leverage area in agent reliability, because it converts an entire class of catastrophic failures — the duplicated payment, the triple-sent email, the deleted-then-recreated resource — into a non-event.The Amazon Builders' Library article Making retries safe with idempotent APIs makes the general case: idempotency is a prerequisite for safe retries, not an optimisation applied afterwards. Agents raise the stakes, because the retry decision is made by a component that cannot reason reliably about whether its previous attempt landed.
6.1 The unit that matters is the effect, not the call
Model inference is effectively side-effect-free with respect to your resources: it reads a prompt and produces tokens. That is why the companion inference article can conclude that retrying an inference call is safe in the sense that matters. The agent loop breaks that property, because between the model calls the agent performs writes: it creates tickets, sends messages, moves files, calls partner APIs.So the object to protect is not "the step" and not "the tool call" but the externally visible effect. Two tool calls that create the same ticket are one effect that must happen once. One tool call that creates two tickets is two effects. Design the deduplication boundary around effects, and the rest follows.
6.2 Deriving a deduplication key that actually works
The key must be stable across attempts (so the second attempt is recognised as a repeat) and distinct across intents (so two legitimately different actions are not collapsed). Most broken implementations fail the first property, because the key is generated at call time.import hashlib, json
def effect_key(task_id: str, step_index: int, tool: str, args: dict) -> str:
"""Derived from the INTENT, not from the attempt.
- task_id + step_index: stable across retries of the same step
- tool + canonical args: distinguishes different intents at the same step
- deliberately excludes: attempt number, wall-clock time, random UUIDs
"""
canonical = json.dumps(args, sort_keys=True, separators=(",", ":"),
ensure_ascii=False)
material = f"{task_id}\x00{step_index}\x00{tool}\x00{canonical}"
return hashlib.sha256(material.encode("utf-8")).hexdigest()[:32]
Note the tension with Section 5.2. If a retry is a replay, the arguments are identical and the key is naturally stable. If a retry is a re-decide, the agent may produce different arguments — and then the key differs, and deduplication does not fire. That is usually the correct behaviour, because a different action genuinely is a different effect. But it means re-decide retries must be preceded by reconciliation, not protected by a key. This is exactly the ordering the decision diagram in Section 5.2 enforces.
6.3 Enforce at the boundary, not in the prompt
The enforcement point must be the code that performs the effect — the tool implementation or the service behind it — and the mechanism should be a conditional operation rather than a check-then-act sequence:- Conditional write. Insert only if the key is absent; on conflict, return the previously stored result rather than an error, so the caller sees a successful, idempotent outcome.
- Compare-and-set on content. Where the effect is an update rather than a create, condition it on the current value. This is a well-established API shape; as one concrete example, the Anthropic Managed Agents memory API accepts a precondition of type
content_sha256on update and returns HTTP 409 when the stored content has changed, and it likewise returns 409 with the conflicting identifier when a create would collide with an existing path. - Native mechanisms first. Where the downstream service already offers a client request token, a message deduplication identifier, or a conditional-put primitive, use it rather than building a parallel one.
What must not be the enforcement point is the prompt. "Remember that you have already sent this email" is a request, not a constraint, and it competes for attention with everything else in a long context. Prompt-level instructions are a useful hint for reducing wasted attempts; they are not a safety mechanism.
6.4 Dry runs and plan-then-apply
For destructive or wide-reaching operations, split the tool in two: a planning call that returns a description of what would change, and an apply call that takes that plan as its input. Three benefits follow, and they compound:- The plan is an artifact that can be validated by code before anything happens — row counts within expected bounds, no protected resources touched, the diff is of the expected shape.
- The plan is what a human reviews at an approval gate, which is far more reviewable than a natural-language description of intent.
- The apply call becomes naturally idempotent when keyed on the plan's identity, because applying the same plan twice is a recognisable repeat.
The cost is an extra round trip per destructive action. Reserve it for operations whose blast radius justifies it.
6.5 Approval gates as a runtime primitive
An approval gate is a tool-level policy: this action does not execute until an authorised party says so. The important design property is that waiting must be durable — a gate implemented as an in-process block is lost when the process restarts, which converts an approval into a silent failure.Several runtimes provide the primitive directly, and the shapes are instructive because they solve the durability problem differently:
- Per-tool permission policy. A managed agent runtime can mark a tool as requiring confirmation rather than executing automatically; the session then goes idle and waits for an explicit confirmation event carrying the identifier of the pending call, and the denial can carry a message back to the agent explaining why — which lets the agent adapt rather than simply fail.
- Graph-level interrupt. LangGraph's
interrupt()pauses execution at an arbitrary point, persists the graph state through the checkpointer, and waits indefinitely; execution resumes when the graph is re-invoked with aCommandcarrying the resume value, which becomes the return value of the originalinterrupt()call. Thethread_idis the cursor that identifies which paused state to resume. - Workflow callback token. AWS Step Functions'
.waitForTaskTokenintegration pauses a state until an external party returns the token. This is the most durable of the three — the wait survives anything happening to the agent process — and it comes with an important caveat covered in Section 7.4.
Whichever you use, the reliability requirement is the same: a pending approval is a piece of state with an owner and a deadline, not an open socket.
6.6 Reservations and leases for exclusive work
When two agent runs might act on the same resource — a retry racing its own original, or two agents given overlapping tasks — the effect-level key is not enough, because the conflict is about ownership rather than repetition. The pattern that works is a lease: acquire an exclusive claim with an expiry, do the work, then confirm or release.The expiry is the part that must not be skipped. A lock without a time bound turns a crashed agent into a permanently blocked resource, and agents crash in ways that skip cleanup handlers — the runtime terminated the session, the budget was exhausted mid-step, a human aborted the run. Set the lease long enough for the slowest legitimate operation and renew it explicitly for anything longer.
6.7 Anti-patterns in this area
- Generating the key at call time. A fresh UUID per attempt is not a deduplication key; it is a request identifier. Both are useful, and they are not the same field.
- Deduplicating in the agent's memory. "The agent remembers it already did this" fails across process restarts, across context compaction, and across parallel branches.
- Idempotency on the read path only. Wrapping the model call and leaving the tool calls unprotected is the most common version of this mistake, because the model call is the one the SDK talks about.
- Assuming a timeout means the effect did not happen. It means you did not hear back. This is the oldest lesson in distributed systems and it is exactly as true here.
7. Timeout Budgets Across Layers
A timeout turns "slow" into "failed", which is what makes every other recovery mechanism possible — without one, a stuck call simply hangs and no retry logic ever runs. In an agent, timeouts exist at four layers plus the platform's own, and the failure mode is not usually a missing timeout. It is that the numbers at each layer were chosen independently and do not compose.7.1 The four layers
* You can sort the table by clicking on the column name.| Layer | Bounds | Typical enforcement point | What it protects against |
|---|---|---|---|
| Call | One model or tool invocation | SDK or HTTP client configuration | A hung dependency |
| Step | One decision plus its action, including in-step retries | Agent loop wrapper | A step that is individually slow enough to eat the task |
| Task | The whole run, across all steps | Orchestrator, as an absolute deadline | Many acceptable steps summing to an unacceptable total |
| User-perceived | What the requester is willing to wait for | API gateway or UI contract | A correct answer that arrives too late to be useful |
| Platform | What the hosting environment permits | The runtime; usually not adjustable beyond a documented maximum | Design assumptions the platform will not honour |
7.2 The arithmetic that gets missed
Nested attempts multiply. The worst-case wall-clock time of a task is approximately:steps × step_retries × call_retries × call_timeout (plus accumulated backoff)Each factor looks individually reasonable while the product is absurd. A thirty-second call timeout, three SDK attempts, two step-level retries, and forty steps is over two hours of worst case. If the user-perceived budget is five minutes, four of those five layers are decorative.
Two rules keep this honest:
Propagate an absolute deadline, not a duration. Compute the deadline once when the task is admitted and pass it down; every layer clamps its own timeout to the time remaining. This composes correctly under nesting, whereas per-layer durations do not.
import time
class Deadline:
def __init__(self, seconds: float):
self.at = time.monotonic() + seconds
def remaining(self) -> float:
return max(0.0, self.at - time.monotonic())
def expired(self) -> bool:
return self.remaining() <= 0.0
def clamp(self, requested: float) -> float:
"""Never let an inner layer outlive the task."""
return min(requested, self.remaining())
def run_step(deadline: Deadline, step):
if deadline.expired():
raise BudgetExhausted("task deadline reached before step start")
return step.execute(timeout=deadline.clamp(step.preferred_timeout))
Reserve time for the ending. A task that spends its entire budget acting has nothing left to summarise what it did, write the checkpoint, or produce a partial result. Hold back a slice of the task deadline for the wind-down, and treat entering that slice as the trigger for the degrade path rather than as an emergency.
7.3 Platform ceilings are part of your design, whether you acknowledge them or not
Every hosting environment imposes limits that no amount of client configuration overrides. Two concrete examples, both checked on 2026-07-31, illustrate the two shapes these take:Idle and lifetime ceilings. Amazon Bedrock AgentCore Runtime terminates a session after 15 minutes of inactivity and caps total session duration at 8 hours; both are adjustable within documented bounds through the lifecycle configuration (
idleRuntimeSessionTimeout and maxLifetime). Crucially, "inactivity" is determined from the runtime's health signal — a session reporting HealthyBusy is kept alive, while one reporting Healthy is treated as idle-eligible. An agent doing long background work that does not update its health status will be terminated mid-task, and the failure will look like an unexplained disappearance rather than a timeout.Unbounded waits that need an explicit bound. A Step Functions task using the callback pattern will wait for its token until the execution hits the service's one-year limit. The documented remedy is to set
HeartbeatSeconds, so that a task which receives no SendTaskHeartbeat within the interval fails with States.Timeout. Note the semantics carefully: TimeoutSeconds is the task's maximum allowed duration regardless of how many heartbeats arrive, while HeartbeatSeconds bounds the gap between them. You need both to express "this may legitimately take a long time, but it must show signs of life".The general lesson: find your platform's idle rule and its lifetime rule, and design the checkpoint interval against them (Section 10), rather than discovering them from a support ticket.
7.4 Heartbeats and progress: distinguishing slow from stuck
A timeout alone cannot tell the difference between an operation that is working hard and one that has died. Progress signalling closes that gap, and the Model Context Protocol specifies it in a form worth copying even if you are not using MCP.In MCP specification version
2026-07-28, a client that wants progress updates includes a progressToken in the request metadata; the server may then send notifications/progress messages carrying that token, a progress value, and optional total and human-readable message fields. The specification requires that the progress value increase with every notification, even when the total is unknown — which is precisely the property that makes it usable as a liveness signal rather than merely as a UI decoration.Design implications for your own long-running tools:
- Emit progress on a schedule, not only at milestones, so silence is unambiguous.
- Reset the stuck-detection clock on progress, not on bytes received — a connection can trickle keep-alives forever without the work advancing.
- Make the progress value monotonic. A value that can go backwards cannot be used to distinguish "still working" from "restarted from the beginning".
7.5 Cancellation has to be propagated, not merely abandoned
When a budget expires, the agent stops waiting. That does not stop the tool. Unless cancellation is propagated, an expired task leaves work running that will complete, consume resources, and possibly perform its side effect after everyone has stopped listening — which is how a "timed out" task ends up having sent the email after all.The MCP specification handles this with
notifications/cancelled, carrying the request ID and an optional reason, and the current revision makes the mechanism transport-aware in a way worth noting: over Streamable HTTP, closing the response stream is the cancellation signal and the server must treat a client disconnect as cancellation of that request, so no explicit notification is required; over stdio, where there is no per-request stream to close, the client must send the notification explicitly. The specification also requires both sides to handle the race gracefully, since a cancellation can arrive after the work has already finished.Two rules generalise from this:
- Know which of your transports cancels implicitly and which does not. Assuming the wrong one produces orphaned work that is invisible from the agent side.
- Treat "cancelled" as a state, not as an absence. A cancelled step whose side effect may have committed still needs the reconciliation path from Section 5.3. Cancellation is not a rollback.
7.6 Running out of time is an outcome, not an exception
The last rule of budget design is that expiry must lead somewhere designed. An agent that throws a timeout to the caller has converted a manageable situation — "we did most of it and here is what remains" — into an unmanageable one. Route deadline expiry into the degrade path (Section 9) or the escalation path (Section 8), and make the choice depend on whether the partial work is usable, exactly as the decision diagram in Section 5.2 shows.8. Human Escalation Design
Escalation is often treated as the failure branch: the thing that happens when the agent could not cope. That framing produces bad escalation design, because it makes the path an afterthought. The more useful framing is that escalation is a capability — the ability to convert an agent's partial work into a human's starting point — and a system that has it can safely attempt harder tasks than one that does not.8.1 Four questions, all of which need answers before launch
- When does the agent hand off?
- To whom, and how do they find out?
- With what — what does the human receive?
- What happens after — does anything resume, and with what context?
Most production escalation problems are failures of the third and fourth questions, not the first. Teams get the trigger right and then hand a human a raw transcript and no way to give an answer back.
8.2 Triggers
* You can sort the table by clicking on the column name.| Trigger | Condition | Design note |
|---|---|---|
| Policy | The next action is irreversible, high-value, or outside a pre-agreed envelope | Evaluated by code against the proposed action, before execution. Never conditional on the agent's judgement |
| Verification failure | An external check rejected the agent's output, repeatedly | Distinguish "failed once" (retry) from "failed the same way three times" (escalate) |
| Budget exhaustion | Steps, tokens, or time ran out with usable partial work | Should be the graceful wind-down, not a thrown exception |
| Guard activation | Loop, stagnation, or cycle detector fired through the full ladder | Carry the detector's evidence into the handoff; it is the most useful diagnostic the human gets |
| Novelty | The situation does not match any anticipated case | The hardest to detect reliably; approximate with schema mismatches, unknown error codes, and unexpected tool responses rather than asking the model to rate its own confidence |
8.3 The handoff packet
A transcript is not a handoff. The receiving human needs to reach a decision quickly, and the structure that supports that is closer to an incident summary than to a log:- The original goal, verbatim, from the immutable copy — not the agent's latest restatement of it.
- What has already been done to the world, from the side-effect ledger (Section 12.2), with identifiers the human can look up. This is the single most important field, because it determines what is safe to do next.
- What is pending or partially applied — leases held, approvals outstanding, half-applied changes.
- Why the agent stopped, in the system's vocabulary: which trigger fired, with the supporting evidence.
- What the agent proposes, if anything, clearly marked as a proposal rather than a finding.
- What is needed — the specific decision or input required, ideally as a small set of options rather than an open question.
- How to resume — the identifier that the resume call needs, and the deadline after which resuming is no longer safe.
The last item is easy to omit and expensive to omit. A paused task holding a lease has a shelf life; a human returning after it expires needs to know that reconciliation, not resumption, is the correct next step.
8.4 Parking the work durably
Blocking escalation — the agent waits — is acceptable only for short, interactive waits. Anything longer needs the work parked in durable storage with a resumption handle. The three mechanisms introduced in Section 6.5 differ in exactly this respect, and the difference is the main selection criterion:* You can sort the table by clicking on the column name.
| Mechanism | Survives | Resume handle | Bounding the wait |
|---|---|---|---|
| In-process wait | Nothing | None | Process-local timer |
| Session idle with a pending action (managed agent runtime) | Client disconnects; not necessarily long outages | Session identifier plus the pending call identifier | Platform idle and lifetime ceilings |
| Checkpointed graph interrupt | Process restarts, given a durable checkpointer | Thread identifier | Waits indefinitely by design; you must add your own expiry |
| Workflow callback token | Essentially everything on the agent side | Task token | Heartbeat interval plus task timeout, both explicit |
Two of the rows contain a trap worth restating. LangGraph's interrupt "waits indefinitely until you resume execution" — which is a feature for correctness and a hazard for operations, because a forgotten interrupt is invisible. And a Step Functions callback waits up to the execution's one-year limit unless you set a heartbeat. In both cases the platform's default is "wait forever"; the expiry is yours to add.
8.5 Resuming without losing the human's decision
The resumption path has one failure mode that dominates all others: the agent resumes, does not register the human's decision as authoritative, and re-derives the situation from scratch — sometimes re-asking the same question, sometimes proceeding as if the decision had gone the other way.The cause is almost always that the decision was injected as another conversational message. Messages are subject to summarisation, truncation, and being outweighed by the surrounding context. The fix is structural:
- Record the decision as state, not as dialogue. Put it in the same durable structure that holds the goal and the side-effect ledger, and render it into context on every subsequent step as a fact rather than as history.
- Make it survive compaction. Anything that a summarisation step could paraphrase away must not be the only copy.
- Re-establish the world before continuing. Time passed while the task was parked; other actors may have acted. Re-read the state the plan depends on rather than trusting the checkpoint's snapshot of it.
- Re-check the budget. The wall-clock budget kept running in human time. Decide explicitly whether the deadline is extended on resume, and make it a policy rather than an accident of implementation.
8.6 The escalation queue is a production system
An escalation path with no owner is worse than no escalation path, because it converts a visible failure into an invisible one. Escalations need a destination that is monitored, an owner who is accountable, an expectation for how quickly they are picked up, and an alert when that expectation is missed. They also need a feedback loop: a rising escalation rate is a product signal, telling you either that the task envelope is too wide or that a capability is missing. Track it alongside the agent's success rate, not separately from it.9. Degradation and Fallback
When the agent cannot fully succeed, the remaining question is what the caller gets. The two mechanisms usually conflated here have different risk profiles and deserve separate treatment.Degradation reduces what is delivered: fewer sources consulted, a shorter analysis, a partial result, a narrower scope. The path is the same; the ambition is lower.
Fallback switches to a different path: another model, another tool, a deterministic implementation, a cached answer.
9.1 Be sceptical of fallbacks
The Builders' Library article Avoiding fallback in distributed systems makes an argument that transfers directly and uncomfortably to agents: fallback paths are rarely exercised, so they are rarely known to work, and they are invoked precisely when the system is least healthy. A fallback that has never run in production is a hypothesis.Applied here, three tests are worth applying to any proposed agent fallback:
- Does it share a failure domain with the primary? Falling back to a different model behind the same gateway, in the same region, using the same credentials does not survive the outages that matter.
- Is it simpler than the primary? If the fallback is another agent loop, it inherits every failure mode in this article. A deterministic keyword search is a better fallback for a broken retrieval agent than a second retrieval agent is.
- Is it exercised? If it does not run in normal operation — on a small traffic share, or in a scheduled exercise — assume it is broken.
The strongest version of the advice is to prefer designs that do not need a fallback: make the primary path handle the degraded case explicitly, so the same code runs in both conditions.
9.2 A capability ladder
Rather than a binary, define the rungs in advance and record which one a given response came from:- Full autonomy. The complete tool surface, the full budget.
- Constrained autonomy. A reduced tool set (typically read-only), a smaller budget, a narrower scope. Appropriate when write operations are failing but reads still work.
- Assisted. The agent prepares work but executes nothing; a human applies it. This is the plan-then-apply split from Section 6.4 used as a degradation mode.
- Deterministic. A non-agentic implementation covering the common case — a template, a lookup, a fixed query.
- Honest refusal. A clear statement that the request cannot be served now, with a route to a human.
Every production agent needs rung 5 implemented, because it is the only rung that is guaranteed to be available.
9.3 Returning partial results honestly
Partial results are useful and dangerous in the same breath: useful because most of the work is done, dangerous because a partial result presented as complete is worse than no result. Three requirements make them safe:Machine-readable incompleteness. The response carries a status field the caller can branch on — not a sentence of prose that a downstream system will ignore. If your successful response shape has no room for "partially complete", that is the first thing to fix.
Explicit coverage. Say what was and was not done, in the terms of the request: "12 of 15 records updated; 3 failed validation, listed below" rather than "some records could not be updated".
A defined next action. Whether the remainder will be retried automatically, is queued for a human, or has been abandoned. Silence here guarantees that someone assumes the wrong one.
9.4 Degradation must be visible to operators
The purpose of degradation is to shield the user from a failure. The hazard is that it also shields you from it. A system that quietly degrades looks healthy on a success-rate dashboard while delivering progressively worse results.Emit a distinct signal on every degraded response, and treat the degraded-response rate as a first-class service level indicator. The SRE Workbook chapter on alerting on SLOs is the right model for how to alert on it: page on the rate of budget burn rather than on individual occurrences, so that a slow drift is caught without a noisy alert on every single degraded response.
10. State, Checkpointing, and Resumability
Long-running agents get interrupted — by platform lifetime limits, deployments, crashes, human escalations, and their own budgets. Resumability is what determines whether an interruption costs a few seconds or the entire run.10.1 Four kinds of state, only one of which usually gets persisted
* You can sort the table by clicking on the column name.| State | Contents | Reliability property |
|---|---|---|
| Conversation | Messages, tool calls, tool results, reasoning artifacts | Reconstructible in principle, expensive in tokens; safe to summarise |
| Agent working state | Plan, sub-goals, retrieved facts, scratch notes | Cheap to persist, expensive to regenerate; should be explicit rather than implicit in the transcript |
| External world state | Everything the agent has changed outside itself | Not reconstructible. Must be tracked in a ledger, never summarised |
| Budget ledger | Steps, tokens, and time consumed so far | Must survive resume, or the budget resets and bounds nothing |
Most frameworks persist the first row well and leave the other three to you. The fourth is the most commonly missed: an agent that resumes with a fresh budget after each interruption has an unbounded total budget, which is the exact failure the budget existed to prevent.
10.2 Where to checkpoint
Checkpoint at step boundaries, and specifically after the observation is recorded and before the next decision is made. That boundary is the one where the state is internally consistent: the action has completed, its result is known, and no partially-formed plan is in flight.The frequency trade-off is real, and LangGraph exposes it unusually clearly with three durability modes.
"exit" persists only when execution finishes — best performance for long graphs, but no recovery from a mid-execution crash. "async" persists while the next step runs — good performance, with a small window in which a crash loses the write. "sync" persists before the next step starts — every checkpoint is written before execution continues, at some performance cost. The general shape applies regardless of framework: choose the mode per workload from what a lost checkpoint would cost, and reach for synchronous durability specifically when the step that just completed had an external side effect.10.3 Resume must not replay side effects
This is where checkpointing and idempotency meet, and where naive implementations do damage. If a checkpoint was written before the effect landed but after the decision was made, a resume will re-execute the action. The protections are the ones already built:- The deduplication key from Section 6.2 is derived from task and step identity, so the replayed call is recognised as the same effect and is not applied twice.
- The side-effect ledger records what actually landed, so the resuming agent can be told rather than having to guess.
- For anything not covered by either, reconcile before continuing: read the world, compare it with the plan, and resume from where the world actually is.
Stated as an invariant: a resume is safe when every action between the checkpoint and the interruption is either idempotent, recorded, or reconciled. If none of the three holds for a given tool, that tool's step needs synchronous durability.
10.4 Non-determinism at the resume point
Resuming does not merely continue — it re-decides. The model may now choose a different action than it did before the interruption, based on a context that has been reassembled and possibly summarised. This is a feature when the earlier attempt was going wrong, and a hazard when it was going right and had already changed the world.The mitigation is to make the parts of the plan that have already been acted on non-negotiable on resume: render completed steps as established facts rather than as history that can be reconsidered, and re-plan only the remaining suffix. Where a framework offers durable execution semantics, this is what its guidance about wrapping non-deterministic operations is protecting.
10.5 Designing against the platform's clock
Section 7.3 named two ceilings; here is what they mean for checkpoint design. If the platform terminates a session after a fixed idle period, the checkpoint interval must be shorter than that period and the agent must emit whatever liveness signal the platform reads — on AgentCore Runtime, a health status ofHealthyBusy during background work, since a session reporting plain Healthy is treated as idle-eligible. If the platform caps total session duration, any task that can legitimately exceed the cap must be decomposed into resumable segments before launch, not after the first truncation.10.6 Compaction is a resumability hazard
Context compaction keeps long runs inside the window by summarising earlier turns. It is also, from a reliability standpoint, a lossy transformation applied automatically to the record of what the agent did.The danger is specific: a summariser optimises for the gist of a conversation, and "created record 4471 in the billing system" is exactly the kind of concrete, low-salience detail that a summary drops. If that line was the only record of the effect, the resumed agent will create it again.
The rule follows directly: the side-effect ledger lives outside the conversation. Compaction may freely summarise reasoning, discussion, and intermediate exploration. It must never be the custodian of what changed in the world. Memory design more broadly — what to retain, where, and with what retrieval semantics — is the subject of the AI Agent Memory Design Guide.
11. Multi-Agent Reliability
Delegation is a remote procedure call whose callee is non-deterministic, can fail by returning confident nonsense, and spends a shared budget. Every property established above must be re-established at the boundary, and a few new ones appear that have no single-agent analogue.The empirical picture is worth keeping in mind while designing. The taxonomy in "Why Do Multi-Agent LLM Systems Fail?" (arXiv 2503.13657, v1 2025-03-17) organises observed failures into categories that include problems with the specification given to agents, misalignment between agents during execution, and inadequate verification or premature termination of the task. The authors' framing is the useful one for engineers: a large share of these failures are organisational rather than cognitive. They are unclear contracts, mismatched assumptions about who owns what, and nobody being responsible for deciding that the work is finished. Those are all things a harness can fix; they are not model capability problems.
11.1 A delegated task needs the same four things as a top-level task
If the parent task required success criteria, a budget, termination conditions, and an escalation path, so does every sub-task. The common failure is to pass a sub-agent a natural-language instruction and nothing else, at which point the sub-agent inherits none of the parent's guarantees and the parent has no way to tell a slow sub-agent from a stuck one.A delegation contract that works, expressed as data rather than prose:
from dataclasses import dataclass
@dataclass(frozen=True)
class Delegation:
goal: str # immutable; not re-derivable by the child
success_check: str # identifier of a predicate the PARENT evaluates
step_budget: int # carved out of the parent's remaining budget
token_budget: int
deadline_at: float # absolute, inherited from the parent
allowed_tools: tuple # narrower than the parent's surface
on_failure: str # "report" | "escalate" | "abort_parent"
Two fields carry most of the weight.
success_check is evaluated by the parent, not the child, for the reason established in Section 2.3 — the actor must not be the sole judge of its own output. And on_failure forces the parent to decide, at delegation time, what a failed sub-task means; without it, the default is invariably "the parent keeps going and quietly incorporates whatever came back".11.2 Budgets partition; they do not replicate
If a parent with a budget of 100 steps spawns five sub-agents each granted 100 steps, the system's real budget is 600. This is the most common way a multi-agent system's cost and latency escape their bounds, and it usually happens by accident because the sub-agent is constructed with the same default configuration as the parent.The correct model is a shared pool: the parent's remaining budget is the resource, sub-agents draw from it, and the parent's own accounting decrements as children spend. Where the runtime does not support a shared counter, approximate it by dividing the parent's remaining budget at delegation time and refusing to delegate when the remainder falls below a floor.
Concurrency needs a cap of its own, separate from the budget. Parallel sub-agents multiply the instantaneous load on every shared dependency — the model endpoint, the retrieval index, the target APIs — and that is a throughput problem rather than a budget problem. Managed multi-agent runtimes typically impose their own ceiling on concurrent threads; whether or not yours does, set one.
11.3 Sub-agent output is untrusted input
A sub-agent's report is a claim, subject to exactly the self-reporting problem of Section 2.3, with the additional hazard that the parent is now further from the evidence. Treat the return value as data to be validated:- Validate structurally before use — the report should be a schema, not prose the parent must interpret.
- Verify externally against the parent-owned success check, not against the child's assertion of success.
- Attribute the effects. Anything the child changed in the world belongs in the parent's side-effect ledger, because the parent is the one that will have to compensate or reconcile.
The failure this prevents is a specific and nasty one: a sub-agent reports success, the parent builds three more steps on that foundation, and the error only surfaces at the final verification — by which point compensating requires undoing four steps' worth of effects instead of one.
11.4 Containing cascades
The Google SRE Book chapter on addressing cascading failures describes the general mechanism: a local overload triggers retries, retries increase load, and the increased load spreads the overload. Agents add a second amplifier on top of the retry one — re-delegation. A parent whose sub-agent failed may respond by spawning two more, each of which may do the same.Three controls, in order of importance:
- Bound the delegation depth. Many production systems are correct with a single level, and some runtimes enforce that: a managed coordinator may reject a roster containing an agent that is itself a coordinator, failing the configuration rather than silently flattening it. If your framework does not enforce depth, enforce it yourself — unbounded delegation depth is unbounded recursion with a token cost.
- Make the shared budget the circuit breaker. When the pool is exhausted, no new delegation is admitted regardless of how the parent feels about it. This is load shedding, and the Builders' Library treatment of load shedding applies: rejecting work you cannot complete is better than accepting all of it and completing none.
- Do not retry a delegation that failed for a systemic reason. If the sub-agent failed because a shared dependency is down, a second sub-agent will fail the same way while adding load. Classify the failure before re-delegating, exactly as Section 5 classifies step failures.
11.5 Shared workspaces need ownership, not etiquette
Sub-agents sharing a filesystem, repository, or database will conflict. Prompt-level coordination ("do not modify files another agent is working on") is not a concurrency control mechanism; it is a hope.Use the same primitives you would use for concurrent processes: partition ownership so that two agents cannot be assigned the same resource, or use the leases from Section 6.6 with explicit expiry. Where the runtime hands sub-agents a shared workspace by design, partitioning by path or by resource identifier at delegation time is usually simpler and more robust than locking.
11.6 Who decides the whole thing is finished
Multi-agent termination is a distinct decision from sub-task termination, and leaving it implicit is one of the failure categories the MAST taxonomy identifies. The parent must hold the completion predicate for the overall goal and must be able to stop children that are still working when that predicate is satisfied — otherwise a system can return an answer while sub-agents continue to act on the world behind it.Interrupt semantics deserve explicit attention here, because the defaults are not obvious. In one managed multi-agent runtime, an interrupt sent without naming a specific thread stops every non-archived thread in the session, including the primary one — it is not a primary-only stop. Whether that is what you want depends on the design, and the point is that it is a decision to make deliberately rather than to discover. Establish, for your own system: does stopping the coordinator stop the workers, and does a worker finishing trigger anything in the coordinator?
For orchestration patterns that make these boundaries explicit at the infrastructure level, see Step Functions Orchestration Patterns for Generative AI; for framework-level multi-agent topologies, Strands Agents Multi-Agent Patterns Guide; and for cross-vendor agent-to-agent communication, Agent Interoperability Architecture with A2A and MCP on AWS.
12. Observability and Incident Response for Agents
The question agent telemetry has to answer is not "was it up?" but "why did it do that?" — usually hours later, about a run that cannot be reproduced. That requirement drives what you record.This section covers what to record and how to investigate. The how of instrumentation — span names, attribute keys, metric shapes — belongs to the semantic conventions and is the subject of OpenTelemetry GenAI Semantic Conventions Implementation Guide. One relocation is worth repeating from Section 1.4: as of the 2026-07-31 check, the OpenTelemetry generative AI semantic conventions have moved into a dedicated GenAI semantic conventions repository, and the page at the former location is marked as no longer maintained.
12.1 The per-step decision record
The atomic unit of agent telemetry is the step, and a step record that supports incident investigation contains more than a log line:- Identity — task id, step index, parent step or thread if delegated, attempt number.
- Inputs to the decision — which tools were available, what state was visible, which prompt or template version was in force.
- The action chosen — tool name and canonicalised arguments, plus the fingerprint from Section 4.2 so repetition is queryable rather than requiring manual comparison.
- The outcome — result or error, and whether it satisfied the step's expectation.
- Spend — tokens, wall-clock time, and remaining budget after the step. Recording the remainder as well as the delta makes budget-exhaustion investigations trivial.
- Guard state — which detectors evaluated, and which fired.
The one that is nearly always missing is guard state. Without it, "why did this task stop at step 12?" requires reconstruction from surrounding evidence; with it, the answer is a field.
12.2 The side-effect ledger
If you take one artifact from this article, take this one. The side-effect ledger is an append-only record of every externally visible effect the agent attempted, with:- the deduplication key from Section 6.2,
- the target resource identifier in the external system's own namespace,
- the intended effect and the observed outcome,
- whether it is confirmed, unconfirmed, compensated, or superseded,
- the compensating action's identifier where one was applied.
It must live outside the conversation, for the compaction reason in Section 10.6, and it must be written before the effect is attempted — not after — so that an interruption between the write and the effect leaves an "unconfirmed" entry rather than no entry at all. An unconfirmed entry is a question to reconcile; a missing entry is an invisible side effect.
The ledger is what makes the three hardest questions answerable: what did this task actually change, what needs undoing, and what does the human taking over need to know.
12.3 Correlation across a very heterogeneous trace
One agent task spans model calls, tool calls, sub-agent runs, queue messages, and writes in systems that know nothing about agents. A single task identifier propagated into all of them — including into external systems' own request identifiers and idempotency keys where the API allows it — turns an investigation from archaeology into a query.Propagate deliberately: into model call metadata, into tool call arguments, into downstream HTTP headers, into the ledger. The place it matters most is the one most often skipped — the external system, where the record of the effect lives long after your traces have expired.
12.4 An event stream is not an audit log
Live event streams are how operators watch agents work, and it is tempting to treat the stream as the record. It is not, and the reason is structural: streams are typically delivered without replay, so anything emitted while a consumer was disconnected is simply not delivered to it.Managed agent runtimes are explicit about this. One documents that its server-sent event stream has no replay, and prescribes the pattern for reconnecting: open the new stream first, then fetch the event history through the list endpoint, then deduplicate by event identifier as the live stream catches up. Events also carry a processing timestamp that is null while an event is queued and populated once handled — so the same event legitimately appears twice with different values, and a consumer that assumes one delivery per event will double-count.
Design your own transport with the same assumptions: persist the authoritative record separately from the stream, give every event a stable identifier, and make consumers idempotent. A monitoring consumer that double-counts a step is an annoyance; a workflow consumer that double-acts on one is an incident.
12.5 Signals worth watching
Rates, not counts, and each one has a distinct diagnostic meaning:* You can sort the table by clicking on the column name.
| Signal | What a change in it usually means |
|---|---|
| Guard firing rate, by guard | The most sensitive early indicator of a model, prompt, or tool change. Moves before success rate does |
| Budget exhaustion rate | Tasks are getting harder, the agent is getting less efficient, or the budget was set from a stale distribution |
| Escalation rate, by trigger | Rising policy escalations mean the task envelope is too wide; rising verification escalations mean quality has moved |
| Partial completion rate | Whether "mostly worked" is becoming the norm. Easy to hide inside a binary success metric |
| Retry ratio (attempts per completed step) | Tool health and argument quality. A step-level analogue of a retry storm |
| Duplicate-effect rate | Should be zero by construction. Any non-zero value means an idempotency boundary is missing |
| Steps to completion, distribution | The shape matters more than the mean; a growing tail is the signature of emerging looping behaviour |
Alerting on these should follow the service level objective model rather than raw thresholds — define the objective, measure the error budget, and page on the rate at which it is being consumed. The SRE Book chapter on service level objectives and the Workbook chapter on alerting on them are directly applicable; nothing about agents changes that machinery. For an AWS-native implementation of the surrounding observability and evaluation architecture, see LLMOps Observability and Evaluation Architecture on AWS.
12.6 Postmortems when the incident is not reproducible
Agent incidents have a property that makes the usual postmortem method awkward: re-running the task does not reproduce the failure. The response is not to abandon the method but to record enough that the trajectory can be reconstructed even though it cannot be re-executed.Capture, at minimum: the model identifier and generation parameters, the prompt and template versions, the tool inventory and versions, the retrieved documents or context injected at each step, the full step records, and the side-effect ledger. With those, a reviewer can answer "given what it saw, was the decision reasonable?" — which is the question that leads to a fix, whereas "why did the model do that?" usually does not.
The cultural half of the practice transfers unchanged. Blameless analysis, a focus on the system rather than the operator, and action items that change the system are as applicable to a system that includes a model as to one that does not; the SRE Book chapter on postmortem culture is the reference. On this site, AWS Postmortem Case Studies and Design Lessons works through what published incident analyses teach about design.
One agent-specific addition to the template: ask which guard should have caught it. Nearly every agent incident ends with either "no guard covered this" or "the guard fired and the response was wrong", and both are directly actionable.
13. Testing Reliability
Testing for capability — can it do the task? — and testing for reliability — does it fail safely? — are different activities with different suites. This section is about the second.13.1 Test the distribution, not the run
Because the control flow is non-deterministic, a single passing run is weak evidence and a single failing run is weak evidence. Run each task in the suite many times and assess the distribution of outcomes.This is the practice the τ-bench work (arXiv 2406.12045, v1 2024-06-17) formalised with a metric for consistency across repeated trials, and the reason it matters is that averages conceal exactly the behaviour you care about: an agent that succeeds most of the time and occasionally sends a duplicate payment has a fine average.
Practical consequences: a reliability suite is slower and more expensive than a functional one, so keep it small and targeted; and a single failing run is a trigger to re-run rather than to revert, while a shift in the failure rate is a real regression.
13.2 Assert on end state, not on trajectory
Asserting that the agent called tools in a particular order makes the test fail whenever the model finds a better path. Assert instead on what the world looks like afterwards:- the required effects happened, exactly once each;
- no forbidden effect happened at all;
- the deliverable satisfies the same completion predicate that production uses;
- the budget consumed is within the expected envelope.
The "exactly once" assertion is the one that catches idempotency regressions, and it is worth writing even when nothing has changed, because it is the assertion that a refactor of the tool layer will break.
13.3 Fault injection: the cases that actually matter
A reliability suite is mostly a fault-injection suite. The high-value injections, roughly in order of how often they catch something real:* You can sort the table by clicking on the column name.
| Injected fault | What it proves |
|---|---|
| Tool succeeds but the response is lost | The retry does not duplicate the effect — the single most important test in the suite |
| Tool returns a malformed or unexpected payload | The agent does not treat garbage as success, and the schema check is actually enforced |
| Tool hangs past the step timeout | The deadline is enforced at the right layer and cancellation propagates rather than orphaning work |
| Process is killed mid-step, then resumed | Checkpoint and resume do not replay side effects |
| The same tool result is delivered twice | The loop is idempotent with respect to its own observations |
| A human never answers an escalation | The pending state expires rather than blocking forever, and leases are released |
| A sub-agent returns a confident but wrong report | Parent-side verification catches it before the parent builds on it |
| Budget is set very low | The wind-down path produces a usable partial result rather than a truncation |
13.4 Testing the guards specifically
Guards are code that runs rarely, which means it rots silently. Test each directly with a stub agent whose behaviour you control:- A stub that repeats one call verbatim — proves the repetition detector fires.
- A stub that repeats the same call with trivial argument variations — proves canonicalisation works, which is the evasion case from Section 4.5.
- A stub that alternates between two actions — proves cycle detection fires.
- A stub that produces plausible-looking text without changing tracked state — proves the progress predicate is not fooled by output volume.
- A stub that consumes budget without progressing — proves exhaustion routes to the wind-down path and not to an exception.
These tests are fast, deterministic, and cheap because no model is involved. They belong in the normal test suite, not in the expensive periodic one.
13.5 Record and replay, and its honest limits
Recording and replaying tool responses removes tool variance, making loop logic testable in ordinary continuous integration. It is genuinely useful for exercising guard wiring, budget accounting, and error handling.Be clear about what it does not test. Replaying tool responses does not make the model deterministic, so the trajectory can still diverge and reach a point the recording has no answer for. Decide in advance what a cache miss means: usually, failing the test loudly is better than synthesising a plausible response, because a silent synthetic response converts a divergence into a false pass.
13.6 Run against a sandbox, never against production effects
The suite in Section 13.3 deliberately provokes duplicate sends, half-applied writes, and abandoned leases. It needs an environment where those are free: a separate account or tenant, tool implementations backed by disposable state, and an outbound boundary that cannot reach real recipients. Where a tool has no safe sandbox, the reliability test for it is the plan-then-apply split from Section 6.4 — test the plan, and keep a human on the apply.14. Failure Modes and Anti-Patterns
A consolidated list. Each entry maps a symptom to the section that addresses it, so this doubles as a review checklist for an existing system.* You can sort the table by clicking on the column name.
| Anti-pattern | Why it fails | Correction |
|---|---|---|
| The framework default is the budget | Defaults in the hundreds or thousands exist to stop infinite recursion, not to bound a task. The task burns them before anyone notices | Derive budgets from the observed distribution of successful runs; keep the framework limit as an outer fence (§3.5, §3.6) |
| A counter is treated as a loop detector | A step cap fires only after the waste has already happened, and says nothing about why | Add repetition, stagnation, and cycle detection with a graduated response (§4) |
| Self-reported success closes the task | The actor is the least reliable narrator of its own actions; research on unaided self-correction reports it can degrade results | Close on an external predicate the agent did not author (§2.3, §3.2) |
| Retry before reconciling | A transport error says nothing about whether the side effect landed. Re-deciding after a committed effect can act on stale assumptions | Ask "did this commit an effect?" before "is this retryable?" (§5.2, §5.3) |
| Deduplication key generated per attempt | A fresh identifier per attempt is a request id, not a dedup key; the second attempt looks like a new intent | Derive the key from task, step, and canonical arguments (§6.2) |
| Safety rules live in the prompt | An instruction competes for attention with everything else in a long context; it is a hint, not a constraint | Enforce at the tool boundary in code (§6.3) |
| Retries do not consume budget | The counter never advances, so the bound never binds | Decrement the budget on every attempt, at every layer (§5.4) |
| Per-layer timeouts, no task deadline | Nested attempts multiply; individually reasonable values produce an absurd worst case | Propagate an absolute deadline and clamp every layer to the remainder (§7.2) |
| Timeout abandons the call | The tool keeps running and may perform its effect after everyone stopped listening | Propagate cancellation, and know which transports cancel implicitly (§7.5) |
| Escalation with no owner or no expiry | A queue nobody watches converts a visible failure into an invisible one; a wait with no expiry holds leases forever | Give escalations an owner, a response expectation, and a deadline (§8.4, §8.6) |
| The human's decision is injected as a chat message | Summarisation and context pressure can erase it; the agent re-asks or proceeds as if it never happened | Store the decision as durable state and render it as a fact each step (§8.5) |
| Fallback shares the failure domain | A second model behind the same gateway fails with the first; an untested path is a hypothesis | Make the fallback simpler, independent, and exercised — or design so none is needed (§9.1) |
| Partial results presented as complete | Downstream systems and humans act on work that was not finished | Machine-readable status, explicit coverage, defined next action (§9.3) |
| Degradation is invisible | Dashboards look healthy while quality drops | Emit a signal on every degraded response and treat the rate as an SLI (§9.4) |
| Budget resets on resume | An agent interrupted repeatedly has an unbounded total budget | Persist the budget ledger with the checkpoint (§10.1) |
| The side-effect record lives in the transcript | Compaction summarises away the one line that said the record was created | Keep the ledger outside the conversation, always (§10.6, §12.2) |
| Sub-agents get their own full budget | Five children with the parent's budget is six times the intended spend | Partition a shared pool; cap concurrency separately (§11.2) |
| Sub-agent reports are trusted | Errors compound silently until the final check, when compensation is expensive | Validate structurally and verify against a parent-owned predicate (§11.3) |
| Coordination by prompt in a shared workspace | "Do not touch another agent's files" is not a concurrency control | Partition ownership, or use leases with expiry (§11.5) |
| The event stream is the audit log | Streams commonly have no replay; a disconnected consumer misses events silently | Persist the record separately; make consumers idempotent by event id (§12.4) |
| A benchmark score stands in for reliability | Capability and failure behaviour are different properties; averages hide rare catastrophic outcomes | Maintain a separate fault-injection suite scored on distributions (§13) |
15. Frequently Asked Questions
15.1 Is this just SRE with new words?
Largely, and that is intentional — error budgets, idempotency, load shedding, cascading failure, and blameless postmortems all transfer without modification, and this article cites them rather than reinventing them. What does not transfer is the set of properties in Section 2: the control flow is chosen at runtime by a non-deterministic component, the actor is also the reporter of its own success, partial completion is the normal outcome rather than an edge case, and the objective itself can drift between steps. Those four are why loop detection, advertised budgets, and structured escalation are needed on top of the classical toolkit rather than instead of it.15.2 Should the step budget live in the prompt or in the runtime?
Both, and they are not the same control. The runtime bound is what makes the guarantee — it holds whether or not the model cooperates. The prompt-side budget is what lets the agent behave sensibly as it approaches the bound: prioritise, cut scope, and produce a usable partial result instead of being cut off mid-sentence. Section 3.4 covers the distinction; the short version is that the advertised budget should sit somewhat below the enforced ceiling, so that graceful wind-down is the normal path and the hard limit is the backstop that rarely fires.15.3 My framework already has a recursion limit. Do I still need loop detection?
Yes, for two reasons. First, a limit in the hundreds or thousands is a backstop against infinite recursion, not a task budget — by the time it fires the task has already consumed everything it was going to consume. Second, a counter cannot tell you why it stopped. Loop detection fires early, distinguishes repetition from stagnation from cycling, and produces evidence a human can act on. Keep the framework limit as the outer fence and detect inside it.15.4 Should the agent be allowed to retry its own failed tool calls?
For transport failures on idempotent tools, yes — ideally inside the tool wrapper, so the retry is bounded and observable, and so the loop is not tempted to treat a transient blip as a reason to change its plan. For semantic failures, letting the agent re-decide is usually right, because the agent has information the wrapper does not. For anything with an uncompensatable side effect, no: that step should escalate rather than retry, and the tool registry is the right place to record which category each tool falls into (Section 5.3).15.5 How do I keep a human in the loop without destroying throughput?
By making escalation selective and cheap. Selective: gate on properties of the action — irreversibility, value, scope — evaluated by code, rather than on the agent's own uncertainty. Cheap: hand the reviewer a decision, not a transcript. The plan-then-apply split (Section 6.4) is the highest-leverage technique here, because reviewing a concrete diff is fast while reviewing a natural-language intent is slow and error-prone. Also make the wait durable (Section 8.4), so that a human taking an hour costs an hour of latency rather than a lost run.15.6 How do I run regression tests when the agent is non-deterministic?
Change what you assert and what you compare. Assert on end state rather than trajectory, so a better path is not a failure. Compare distributions rather than individual runs, so a single unlucky run is not a regression. And put the deterministic part of the system — guards, budget accounting, deduplication, resume logic — under fast tests with a stubbed agent, where determinism is available and cheap (Section 13.4). Most of what this article calls reliability is in that deterministic part.15.7 On retry, should the agent keep the failed attempt in context or start clean?
Keep the most recent failure per distinct action, drop older duplicates of the same failure, and never drop the side-effect ledger. Keeping every failure biases the agent toward pessimism and consumes the token budget; dropping them all invites an immediate repeat of the mistake. Where the platform offers context editing or compaction, they are the right mechanisms — with the caveat from Section 10.6 that neither may be allowed to become the custodian of what the agent changed in the world.15.8 Is a multi-agent system more reliable than a single agent?
Not by default. Delegation adds contract, budget, containment, and termination problems that a single agent does not have, and published analyses of multi-agent failures attribute a large share of them to those organisational issues rather than to any participant's capability. Multi-agent designs earn reliability when the decomposition is genuinely independent — separate resources, separable success criteria, no shared mutable workspace — and when the parent verifies rather than trusts. If the sub-tasks are sequential and share state, one agent with good guards is usually the more reliable system.15.9 Where do output verification and this article meet?
They meet at the completion predicate. This article treats "is the work done?" as a control-flow question — what terminates the loop, what escalates, what gets retried. Verification treats "is this output correct?" as a content question — grounding, structure, citations, cross-checking. In a production system the verification result is an input to the termination decision: a rejected output is a guard firing, which routes through the same decision path as any other guard (Section 5.2). Design them together and keep them in separate modules.16. Summary
The reliability of an agent is decided by the parts of the system that are not the model.- Termination is a design decision with three independent paths — goal satisfied, budget exhausted, guard fired — and four terminal outcomes, not two. Complete, degrade, escalate, and abort all need to exist before one can be chosen (§3, §1.5).
- Budgets come in three currencies that do not convert, and every enforced ceiling should have an advertised budget below it so that winding down gracefully is the normal path (§3.3, §3.4). Derive the values from your own successful runs, not from an article.
- Detect early; the counter only fires late. Repetition, stagnation, and cycling are different phenomena with different remedies, and the response should be graduated — inform, constrain, then stop (§4).
- Decide what a retry is before deciding a retry policy. Replay and re-decide have different safety analyses, and the side-effect question comes before the retryability question (§5).
- Idempotency is the load-bearing property. Derive the key from intent rather than from the attempt, enforce it at the boundary in code rather than in the prompt, and split destructive operations into plan and apply (§6).
- Propagate an absolute deadline, not per-layer durations, reserve part of the budget for the ending, and propagate cancellation rather than merely abandoning the wait (§7).
- Escalation is a capability. Hand over a decision with evidence, park the work durably with an expiry, and store the human's answer as state rather than as dialogue (§8).
- Be sceptical of fallbacks and honest about partial results. An unexercised fallback is a hypothesis, and a degraded response that operators cannot see is a hidden outage (§9).
- Checkpoint at step boundaries, persist the budget ledger, and keep the side-effect ledger outside the conversation where no summarisation step can reach it (§10, §12.2).
- Delegation partitions a budget; it does not replicate one, and a sub-agent's report is untrusted input until the parent verifies it (§11).
- Record enough to answer "given what it saw, was that reasonable?" — the trajectory will not reproduce, so the record has to stand in for it (§12.6).
- Test the distribution and inject the faults, especially the one where the tool succeeded and the response was lost (§13).
None of this makes an agent correct. It makes an agent operable: bounded in what it can consume, observable in what it decided, honest about what it finished, safe to retry, and able to hand work to a person when it should. That is a lower bar than correctness and a far more useful one, because it is the bar that determines whether the system can be run at all.
The label at the top of this article is not important. The checklist in Section 14 is.
17. References
Specifications and vendor documentation
- Model Context Protocol — Cancellation (specification 2026-07-28)
- Model Context Protocol — Progress (specification 2026-07-28)
- OpenAI Agents SDK for Python — Running agents (agent loop, max_turns, exceptions)
- Google Agent Development Kit — Runtime configuration (max_llm_calls)
- LangGraph — Graph API (recursion limit, super-steps)
- LangGraph — Checkpointers (durability modes)
- LangGraph — Interrupts (pause and resume)
- Anthropic Claude API — Handling stop reasons
- Anthropic Claude API — Tool use overview
- Anthropic Claude API — Context editing
- AWS Step Functions — Service integration patterns (callback with task token, heartbeat timeout)
- AWS Step Functions API — SendTaskHeartbeat
- Amazon Bedrock AgentCore — Quotas and session lifecycle parameters
- Amazon Bedrock AgentCore — Runtime troubleshooting (idle sessions and health status)
- OpenTelemetry — GenAI semantic conventions repository
Reliability engineering literature
- Google SRE Book — Service Level Objectives
- Google SRE Book — Addressing Cascading Failures
- Google SRE Book — Handling Overload
- Google SRE Book — Postmortem Culture: Learning from Failure
- Google SRE Workbook — Alerting on SLOs
- Amazon Builders' Library — Timeouts, retries, and backoff with jitter
- Amazon Builders' Library — Making retries safe with idempotent APIs
- Amazon Builders' Library — Avoiding fallback in distributed systems
- Amazon Builders' Library — Using load shedding to avoid overload
- Amazon Builders' Library — Instrumenting distributed systems for operational visibility
Research papers
- Why Do Multi-Agent LLM Systems Fail? (arXiv 2503.13657, v1 2025-03-17)
- Large Language Models Cannot Self-Correct Reasoning Yet (arXiv 2310.01798, v1 2023-10-03)
- tau-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains (arXiv 2406.12045, v1 2024-06-17)
- TheAgentCompany: Benchmarking LLM Agents on Consequential Real World Tasks (arXiv 2412.14161, v1 2024-12-18)
- Reflexion: Language Agents with Verbal Reinforcement Learning (arXiv 2303.11366, v1 2023-03-20)
- Self-Refine: Iterative Refinement with Self-Feedback (arXiv 2303.17651, v1 2023-03-30)
- ReAct: Synergizing Reasoning and Acting in Language Models (arXiv 2210.03629, v1 2022-10-06)
- GAIA: a benchmark for General AI Assistants (arXiv 2311.12983, v1 2023-11-21)
Related Articles on This Site
- LLM Inference Resilience Patterns on AWS
The layer below this one: SDK retry configuration, backoff and jitter, circuit breakers, stream recovery, and timeouts for a single inference call. - AI Agent Defense in Depth Model
The security counterpart: layered controls against prompt injection, tool abuse, and unsafe egress. - AI Agent Memory Design Guide
What an agent retains between steps and sessions, and with what retrieval semantics. - LLM Output Verification Patterns
Whether a produced answer is correct: structural checks, grounding, self-verification, cross-model review, and citation enforcement. - OpenTelemetry GenAI Semantic Conventions Implementation Guide
The instrumentation layer beneath Section 12: span names, attributes, and metric shapes for LLM and agent applications. - Strands Agents Multi-Agent Patterns Guide
Framework-level topologies for delegation, and where the coordination boundaries fall. - Step Functions Orchestration Patterns for Generative AI
Durable orchestration, callback patterns, and human approval steps at the infrastructure layer. - Agent Interoperability Architecture with A2A and MCP on AWS
Protocol-level agent-to-agent and agent-to-tool communication. - LLMOps Observability and Evaluation Architecture on AWS
An AWS-native implementation of the telemetry and evaluation pipeline behind Section 12. - Amazon Bedrock AgentCore Production Guide
Running agents on a managed runtime, including the session lifecycle constraints referenced here. - Claude Agent SDK Complete Guide
One concrete agent harness end to end, including its loop and tool integration. - Enterprise AI Agent Design Notes (Part 1)
Platform selection and deployment architecture, upstream of everything in this article. - AWS Postmortem Case Studies and Design Lessons
What published incident analyses teach about designing for failure. - AI Agent Engineering Glossary
Definitions for the terminology used across this series.
References:
Tech Blog with curated related content
Written by Hidekazu Konishi