Agentic Distributed Systems

The Failures That Produce No Errors

Non-determinism makes replay unreliable, so the debugging technique this domain leans on hardest is weakened. A retried model call may take a different path, so a retry is not a repeat. Context is lost between steps and the agent forgets what it did. And a tool error can be read as content, so a failure becomes an answer. None of these raise an exception.

▶ Run the lab

The question this answers

The question

What goes wrong in an agent system that no error rate, latency graph or health check will show me?

The guarantee — the property claimed, and its scope

None of the failures here are prevented by the mechanisms in the rest of this module. Idempotency keys, step logs and leases guarantee that effects are not duplicated and work is not lost — they guarantee nothing about the *content* being correct. The only guarantee available against this class is detection: recorded model outputs, validated tool results, bounded budgets and evaluation against expected outcomes.

Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.

What a node knows — observation versus inference

The orchestrator knows what bytes it sent and received. It cannot tell a tool result that means "here is your answer" from one that means "I failed and here is a message about it" unless the tool’s contract makes that distinction machine-readable — and it cannot tell whether the model’s output was influenced by content that was truncated away several steps ago. The model knows only its context, and has no way to represent "something is missing here".

A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
failure modesnon-determinismdebuggingobservabilitycontext loss

Non-determinism breaks replay

Distributed debugging leans on replay: capture the inputs, re-run, observe. [[distributed-debugging]] and deterministic-replay in Concurrency both depend on the assumption that the same inputs produce the same execution. A model breaks that assumption at the first step.

Re-running an agent with the same prompt may produce a different plan, call different tools, in a different order, and reach a different answer — even at temperature zero, since batching, model versions, provider-side routing and floating-point non-associativity all move the result. So "I reproduced it locally" is a much weaker statement here than elsewhere, and a bug that appears in one run in fifty is genuinely hard to pin down.

The workable substitute: record outputs, not just inputs. If every model response, tool call and tool result is captured, a run can be replayed *deterministically from the recording* — feed the stored responses back instead of calling the provider. That gives you a reproducible trace of what actually happened, which is what debugging needs, at the cost of storing a lot of text. It does not let you ask "what would the model do differently", but that is a different question and usually not the one blocking you.

This also changes what a test is. An assertion that the agent produces a specific output is flaky by construction. An assertion that the agent, given a recorded trace, took a permitted sequence of actions is stable. Structure and constraint are testable; content is evaluated statistically, which is what evals-overview is for.

A retried model call is a new request, not a repeat

This one quietly invalidates an assumption the rest of the domain relies on. When an ordinary RPC is retried, the retry means the same thing: same method, same arguments, same intended effect. That is why retry policies are safe to configure generically.

When a model call is retried, the provider regenerates. The new response may call a different tool, call the same tool with different arguments, or decide no tool is needed. The retry is a fresh non-deterministic sample, not a repetition of a request. Which means retrying at the model-call layer is not equivalent to retrying at the step layer, and conflating them produces a specific bug: the first attempt called create_ticket, the retry calls escalate_to_human, and now the workflow has done both.

The consequence for design is to be explicit about which layer retries. Retrying the *transport* — a connection reset before any tokens arrived — is safe and should be automatic. Retrying the *generation* is a decision to allow a different path, and should be bounded and recorded. Retrying the *step* means re-driving the tool with the same idempotency key, which is what [[agent-idempotency]] provides and which is the layer where retry is actually safe.

The nastiest variant is a partial stream. The connection breaks after the model has emitted half a tool-call block. You have a syntactically incomplete instruction: a tool name, some arguments, no closing structure. It cannot be executed, and it cannot be assumed not to have been intended. The only safe handling is to treat the whole turn as not having produced a call, and to make sure the *previous* turn’s effects are already durably recorded so that re-generating cannot repeat them.

Context loss: the agent forgets it already did something

The context window is a bounded, lossy replica of the workflow state, and it degrades in ways with no error signal at all. Truncation drops the oldest turns. Summarisation compresses ten steps into three sentences and loses the tool result that mattered. A new process rebuilds context from a log that was missing a step. A subagent is given a scoped context that omits what its parent already tried.

In every case the model’s inference is correct given what it can see, and what it can see is wrong. This is exactly the stale-replica problem from the replication module, with one aggravating property: the replica cannot represent its own incompleteness. A database replica knows its log position and can tell you it is behind. A truncated context contains no marker saying "three steps were removed here". The model has no way to know it is missing something, and therefore no way to hedge.

The consequence is a family of bugs that all look like the agent being unreliable: it repeats a step it already completed, contradicts a decision it made earlier, asks the user for information already given, or abandons a constraint from the original instruction. None of these are model quality problems in the usual sense — they are consistency problems, and the fixes are the ordinary ones. Keep the durable step log authoritative, re-derive the important facts into the context on every turn rather than relying on them surviving, and make the constraints that must not be lost part of the system prompt rather than part of the history.

FailureWhat the operator seesWhat actually detects it
Non-deterministic path changetypicalTwo runs of the same task with different costs and outcomes; neither erroredRecorded traces compared across runs; action-sequence assertions
Context truncated or summarised awaytypicalThe agent repeats a step or drops a constraint. Success everywhere.Re-deriving key facts each turn, and asserting them; step-log-versus-action comparison
Tool error read as contentprotocolA confident answer containing a rate-limit message or an HTML fragmentTyped tool contracts; validating results before they enter context
Silent model fallbacktypicalStructurally valid output, quality drop, cost drop. Every metric improves.Evaluation against golden cases; recording which model actually served
Four failures with no error signal

Tool errors interpreted as content

A tool returns {"error": "rate limited, retry after 30s"}. The orchestrator appends it to the context as a tool result. The model reads it as information and writes it into the report: the summary now contains a sentence about rate limiting where a figure should be. Or a proxy returns an HTML error page, and the model, asked to extract a value, extracts something from the error page.

The structural cause is that in this architecture the error channel and the data channel are the same channel. In an ordinary program an exception propagates out of band; here every result — success, failure, partial, malformed — arrives as text in the same slot. Nothing separates them unless the tool contract does.

So the fix belongs at the boundary, not in the prompt. Validate every tool result against a schema before it enters context. Make failure a distinct, typed shape the orchestrator handles rather than a string the model reads. Never pass raw upstream error bodies through — map them to a small controlled vocabulary that the orchestrator understands and the model is told how to treat. tool-output-untrusted in Security makes the same argument from the trust direction, and it is the same boundary: what a tool returns is untrusted input to be validated, not a value to be consumed.

The same discipline handles the case where a tool succeeds but returns nothing useful — an empty search, a null field. Empty is a legitimate result and must be distinguishable from an error, or the model will treat one as the other and either fabricate or give up.

Amplification, silent degradation, and poison tasks

Three more, each a familiar distributed problem wearing agent clothing.

Fan-out amplification. An orchestrator spawns five subagents, each of which calls three tools, each with three retries, and each subagent may itself spawn. The multiplier is a product nobody chose, and against a rate-limited provider it becomes [[retry-amplification]] with a large constant. The mitigations are the ordinary ones — [[retry-budgets]], [[backoff-and-jitter]], and a depth cap — plus one that is specific: a *global* budget per task, in tokens and tool calls, that terminates the workflow regardless of what any individual component believes. That budget is the only backstop against a failure that produces no errors and does not stop on its own.

Silent degradation. A fallback to a cheaper or smaller model on provider failure produces structurally valid output at lower quality. Latency improves, cost improves, error rate is zero, and the product gets worse. No operational metric catches this — it is only visible in evaluation, which is why fallbacks-caching-routing and evaluation belong to the same conversation. At minimum, record which model actually served each call, so the correlation is available after the fact.

Poison tasks. A task that always fails — malformed input, an impossible instruction, a permanently broken tool — is retried forever, consuming budget and worker capacity. This is [[poison-messages]] exactly, and the answer is unchanged: bound the attempts and move it to a [[dead-letter-queues]] destination for inspection. Agent systems get this wrong more often than ordinary consumers because the failure is not an exception — the task "completes" each time with an unsatisfactory result, and the loop tries again.

What to instrument, given that nothing errors

The unifying property of this lesson is that none of it shows up in the three signals most systems monitor: error rate, latency, saturation. So the instrumentation has to be built specifically, and it is a short list.

One correlation id per workflow, propagated into every model call, tool call, memory write and subagent, so a trace can be assembled at all — [[correlating-distributed-logs]]. Without it, every investigation starts by trying to work out which log lines belong together.

Recorded model outputs and tool results, retained long enough to investigate, because the run cannot be reproduced by re-running it.

Cost and token count per completed task, which is the single best leading indicator in this whole module: loops, amplification, context bloat and retry storms all raise it before anything else moves.

Action-sequence assertions: did this workflow call the irreversible tool more than once, call tools outside its permitted set, or exceed its depth? These are cheap, deterministic checks over the trace and they catch a surprising share of real bugs.

Outcome evaluation on a sample, because structural correctness does not imply a correct answer, and the failures in this lesson are exactly the ones that are structurally fine.

Key points

  • Replay-based debugging is weakened: the same inputs may produce a different execution, so record outputs and replay from the recording.
  • A retried model call is a fresh sample, not a repetition — it may take a different path, so retry belongs at the step layer, not the generation layer.
  • A broken stream can leave a syntactically incomplete tool call, which must be treated as no call at all.
  • The context is a lossy replica that cannot represent its own incompleteness, so the model has no way to know it is missing something.
  • The error channel and the data channel are the same channel, so a tool error can become content unless results are validated at the boundary.
  • Fan-out with retries multiplies to a rate nobody configured; a global per-task budget is the only reliable backstop.
  • A silent fallback to a weaker model improves every operational metric and degrades the product.
  • A permanently failing task loops forever because it does not raise an error — bound attempts and dead-letter it.
  • None of this appears in error rate, latency or saturation; the instrumentation must be built deliberately.

The chain, answered

Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.

How it works
  • Every model call is a fresh sample; identical inputs do not guarantee identical outputs or identical tool selections.
  • Responses stream, so a broken connection can leave a partial instruction rather than a clean failure.
  • Tool results enter the context as text in the same slot regardless of whether they represent success, failure or emptiness.
  • The context is assembled per turn under a size bound, so older content is truncated or summarised away without a marker.
  • The model infers the next action from that context, correctly given what it can see.
  • Retries and subagents multiply the request rate against shared downstream dependencies.
  • Nothing in the loop raises an exception when the outcome is merely wrong, so the loop continues until a budget or a deterministic completion check stops it.
What can fail at the boundary
  • The same task produces different plans and different costs on successive runs, with no way to reproduce a specific one.
  • A regenerated retry calls a different tool from the original attempt, so both effects occur.
  • A stream breaks mid-instruction, leaving an unexecutable partial tool call.
  • Summarisation removes the tool result that a later step depends on.
  • A tool returns an error body that reads as plausible content and is incorporated into the answer.
  • A provider fallback silently swaps a weaker model into the path.
  • Subagent fan-out multiplies retries into a rate-limited dependency.
  • A task fails identically forever and is retried without bound.
How it fails — what an operator sees
  • Unreproducible incident: a customer reports a wrong action, and re-running the task produces a correct result. Without recorded outputs there is nothing to investigate, and the ticket is closed as unreproducible while the bug remains.
  • Both branches executed: the first generation created a ticket, the retry escalated to a human. The operator sees a ticket and an escalation for one request, both logged as successes.
  • Answer containing an error string: a report includes "rate limited, retry after 30s" or an HTML fragment in place of a value. No service errored; the only detection is reading the output.
  • Repeated step after summarisation: the agent re-sends a message it already sent, because the record of sending was compressed out of context. Two identical messages, two successful tool calls, no duplicate-key hit because the step index differed.
  • Cost blow-out with flat completions: token spend per task doubles week over week while the completion rate is unchanged — the signature of loops, context bloat or amplification, and usually the first signal anyone actually notices.
  • Quality regression with improved metrics: after a provider incident triggered a fallback, latency and cost improved and the error rate stayed at zero, while evaluation scores dropped 15%. Every dashboard says the change was good.
  • Rate-limit lockout from within: a single task’s subagent tree generates enough retries to exhaust the organisation’s provider quota, so unrelated workloads fail. The observable is fleet-wide 429s traced back to one workflow id.
Where coordination is required
  • None of these failures are coordination failures, which is precisely why the coordination mechanisms in the rest of the module do not prevent them.
  • Budgets are the one place coordination helps: a shared, decremented budget per task is a coordination point that bounds a workflow that cannot bound itself.
  • Retry budgets and concurrency limits shared across subagents require a common counter, which is a small piece of shared state with the usual availability question attached.
  • Recording model outputs centrally is a write on the critical path, and its failure should degrade observability rather than the workflow.
What still holds under failure
  • Effects already performed remain performed; the failures here corrupt content and consume budget rather than losing work.
  • The step log remains accurate about what was called, which is why it is the only reliable account of a run when the trace is ambiguous.
  • Budget caps continue to bound the blast radius even when every other signal is silent — which is why they are the backstop rather than a nicety.
  • Evaluation remains the only detector for wrongness, and it is a sampled, delayed signal rather than a real-time one.
How it recovers
  • Detect: evaluation on a sample, action-sequence assertions over traces, and cost-per-task trends. Error rate will not move.
  • Contain: enforce per-task budgets and subagent depth caps so an undetected failure terminates rather than compounding.
  • Recover: for a poisoned task, stop retrying and dead-letter it with its full recorded trace attached, so the investigation has material.
  • Reconcile: compare the recorded action sequence against what the tools actually recorded, which is the only way to find effects that happened outside the step log.
  • Verify: re-run affected tasks from recorded traces after a fix, and check the outcome rather than the status — a fix that makes the failure rarer without eliminating it looks identical to a fix that works.
How you would know
  • A single correlation id per workflow, present in every model call, tool call and subagent — the precondition for every other signal here.
  • Recorded model outputs and tool results with a retention long enough to investigate, since the run cannot be reproduced by re-running.
  • Tokens and cost per completed task, trended: the earliest and most reliable indicator of loops and amplification.
  • Which model actually served each call, so a silent fallback is correlatable with a quality change after the fact.
  • Assertion failures over traces: tools called outside the permitted set, an irreversible tool called more than once, depth or fan-out exceeded.
  • Evaluation scores on a sampled set, tracked against deploys and provider changes, because this is the only signal that measures wrongness.
When it helps
  • Any agent in production, since these are the failures that survive every other mitigation in this module.
  • Post-incident analysis, where the list turns "the agent did something weird" into a small set of testable hypotheses.
  • Reviewing an agent design, where asking "which of these would we detect?" typically exposes that the answer is none of them.
  • Cost investigations, which in practice are how most of these failures are first noticed.
When it hurts
  • A read-only, single-turn agent with typed tools has very little of this surface, and building the full apparatus for it is waste.
  • Early prototypes, where recording everything and evaluating continuously slows the loop that is still deciding what the product should be.
  • When it turns into fatalism: these failures are detectable and boundable, and "models are non-deterministic" is not an acceptable end to the conversation.
Simpler alternatives
  • Remove the non-determinism where you can: use the model to choose among a fixed set of actions rather than to author them, so the action space is enumerable and testable.
  • Use a deterministic pipeline for the parts that do not need judgement, leaving a small, well-bounded model step — far less of this lesson applies.
  • Typed tool contracts with validated results, which removes the error-as-content class entirely rather than mitigating it.
  • Human approval before irreversible actions, which converts several of these silent failures into a review someone can catch.
  • Bound everything — tokens, tool calls, depth, wall clock — as the cheapest single intervention, since it caps the damage of failures you have not identified yet.

The failures that raise no exception

The failures that raise no exception
Every fault-tolerance technique in this domain assumes a failed component stops producing, so its silence is a signal. A model fails by producing something plausible instead.
typicalThe numbers on the panels are illustrative examples of the shape each failure makes: the left column stays flat while the right column moves. Yours will differ; the asymmetry will not.
What your dashboard shows
error_rate 0.00              OK
tool_http_2xx 100%           OK
health OK                    OK
p99_latency 3.3 s            OK
Nothing errors. Timeouts do not fire, health checks pass, retries make it worse rather than better, and quorum-style redundancy does not help — two samples from the same distribution are not independent observations.
What actually moved
schema-validation failures
0 (nothing validates)
evaluation score
0.83 → 0.44
what is going wrong
The tool returned a string saying it failed. It entered the context as content, and the run continued as though that string were data.
why nothing catches it
The orchestrator knows what bytes it sent and received. It cannot tell "here is your answer" from "I failed, and here is a message about it" unless the tool contract makes the distinction machine-readable.
The control that works: Validate every tool result against a schema before it enters the context, and map failures to a typed error shape the loop handles explicitly. A 200 carrying an error message is the most dangerous response a tool can produce.
Idempotency keys, step logs and leases guarantee that effects are not duplicated and work is not lost. They guarantee nothing about the content being correct, and nothing in this panel is prevented by them. The only guarantee available against this class is detection: recorded outputs, validated results, bounded budgets, and evaluation against expected outcomes. What remains workable is putting every property that must hold into deterministic code around the model — validated inputs and outputs, an enumerable action space, budgets that terminate, assertions over traces — and treating everything the model produces as a proposal. That is the same conclusion the theory reaches for any component with arbitrary output, arrived at from the practical side.

What people believe, and what is true

Claim

We reproduced it locally, so we understand the bug.

Reality

A single reproduction of a non-deterministic system establishes that the behaviour is possible, not that it is understood or that a change fixed it. Compare recorded traces across many runs instead.

Claim

Retrying the model call is harmless.

Reality

It is a fresh sample that may take a different path. If the first attempt already caused an effect, the retry can cause a different one, and now both have happened.

Claim

A lower error rate means the agent is working better.

Reality

The characteristic failures here produce no errors at all. A falling error rate is equally consistent with a silent fallback to a weaker model that fails less loudly and answers worse.

Claim

The model will recognise an error message and handle it.

Reality

Sometimes it does. Sometimes it incorporates the error into the answer, or retries something it should not. Handling failure is the orchestrator’s job; the model should receive a typed, controlled representation.

Claim

Better prompts will fix the repeated steps.

Reality

The step was removed from context by truncation or summarisation. The model cannot act on information it does not have, and no instruction restores it — the durable log does.

Go deeper

Only the levels this lesson can honestly fill — a missing level is a claim nobody had.

Overview

Agents fail by being wrong rather than by stopping: different path on replay, forgotten steps after truncation, tool errors read as answers, quiet fallbacks to weaker models. Nothing errors, so nothing alerts.

Practical

Record model outputs and tool results so a run can be replayed from the recording. Retry at the step layer with an idempotency key, never at the generation layer. Validate every tool result against a schema before it enters context, and map failures to a typed shape. Cap tokens, tool calls, depth and wall clock per task. Then watch cost per completed task and evaluation scores, because those are the only two signals that move.

Advanced

The common structure across every failure here is that the model is a component whose output is *plausible* under failure rather than absent. Every fault-tolerance technique this domain teaches assumes the opposite — that a failed component stops producing, so its silence is a signal. Remove that assumption and the standard toolkit stops covering you: timeouts do not fire, health checks pass, retries make things worse rather than better, and quorum-style redundancy does not help because two samples from the same distribution are not independent observations. What remains workable is the discipline of putting every property that must hold into deterministic code around the model — validated inputs and outputs, enumerable action spaces, budgets that terminate, and assertions over traces — and treating everything the model produces as a proposal. That is the same conclusion [[byzantine-failures]] reaches for arbitrary-output components, arrived at from the practical side rather than the theoretical one.

Apply it

Build it, then break it
  • 🔧 Add output recording to one agent and replay a failed run deterministically from the recording.
  • 🔧 Write three assertions over your traces — permitted tools, at-most-one irreversible call, depth cap — and run them over last week’s workflows.
Reason about this
  • A provider incident triggers an automatic fallback to a smaller model for six hours. Cost falls, latency falls, errors stay at zero. Describe how and when anyone finds out.
  • One workflow’s subagent tree exhausts the organisation’s provider quota and unrelated services start failing. Trace the amplification and name each control that would have bounded it.
Interview questions
  • 💬 Why is replay-based debugging harder for an agent than for a normal service, and what do you do instead?
  • 💬 A model call is retried after a timeout. Why is that not the same as retrying an RPC?
  • 💬 A tool returns an error body and the agent puts it in the report. Where does the fix belong?
  • 💬 Which metric would move first if an agent started looping, and why would error rate not move at all?