learn-ai-observability-with-phoebe / Builder session 6 of 10
Learn AI Observability with Phoebe · Builder track · Session 6 of 10

The failure taxonomy: every failure mode has a signal, if you emit it

Session b5 taught you to read one broken trace. This session generalises it. There is exactly one empirically grounded taxonomy of agent failures, and its headline finding is not the one people quote: the largest category is not model stupidity, it is specification and design. We take all fourteen modes, attach the telemetry signal that reveals each one, and then confront the fact that makes tracing non-negotiable - you cannot replay a production run and get the same answer back.

🟠 Builder track AI engineers · platform engineers · technical PMs 45 min
0-3 · Welcome 3-17 · MAST v3 and the 14 modes 17-40 · Build-along: a signal per mode 40-45 · Q&A
Part 0

A taxonomy is only useful if each entry comes with a detector

Evals tell you whether it works. Observability tells you what actually happened. Most "agent failure modes" lists you will find are somebody's intuition arranged into bullet points, and intuition is a bad guide here because the failures that hurt you are the ones that do not look like failures. This session uses the one taxonomy built from hand-annotated production traces, and it insists on the same discipline for every row: name the mode, name the field in the telemetry that reveals it, name the fix. A mode with no detector is a story, not an operating procedure.

Live - presented in session Self-study - read after class Official sources covered
★ What you walk out with today The full MAST v3 taxonomy with the version caveat you need to avoid quoting the wrong numbers, a failure-mode-to-signal-to-fix table you can paste into a runbook, the four HTTP 200 failures that are invisible to error-rate alerting, the three shapes error propagation takes and why the middle one is the dangerous one, and the reason a production trace can never be reproduced by replay.
The sentence this session is built on Twenty-eight percent of documented agent failures are control-loop bugs. Not reasoning failures, not knowledge gaps, not prompt-wording problems - the agent repeated a step, or did not know it was finished. Both of those are visible in a span count. You do not need a smarter model to fix them; you need to be looking.
Part 1 · the evidence

MAST, and the number everybody gets wrong 8 min live

Cemri et al., Why Do Multi-Agent LLM Systems Fail? (arXiv:2503.13657, NeurIPS 2025 Datasets & Benchmarks) is the only agent failure taxonomy in this course with an inter-annotator agreement figure attached to it. 150 hand-annotated traces, kappa 0.88. MAST-Data extends it to more than 1600 traces across seven named frameworks - ChatDev, MetaGPT, HyperAgent, AppWorld, AG2, Magentic-One, OpenManus - and an LLM annotator reproduces the expert labels at 94% accuracy, kappa 0.77. That last figure is what makes the taxonomy operational rather than academic: you can label your own traces at volume.

MAST v3 · 150 hand-annotated traces · inter-annotator kappa 0.88 FC1 Specification & design 44.4% the largest bucket - not "the model was dumb" FC2 Inter-agent misalignment 32.35% includes reasoning-action mismatch at 13.2% FC3 Task verification 23.5% nobody checked the work before returning it 28% of all failures are pure control-loop bugs, visible in telemetry Step repetition 15.7% plus unaware of termination conditions 12.4%. Neither of those needs a smarter model. Both are span-count and termination-condition problems, and both are countable. Do not quote 41.8 / 36.9 / 21.3 Those are the v1/v2 figures over the original 150-trace corpus. The bars above are v3.
🔍 Click to zoom - the three MAST v3 categories, and the 28% that live in your span counts
The version caveat, stated plainly Widely circulated summaries of MAST quote 41.8 / 36.9 / 21.3 for the three categories. Those are the v1/v2 figures over the original 150-trace corpus. This page cites v3: 44.4 / 32.35 / 23.5, and the per-mode percentages in the table below sum to the v3 totals. If you present these numbers to anyone, name the version out loud. A taxonomy that revises its own figures across preprint versions is behaving correctly; quoting it without a version is not.

The full fourteen, with v3 frequencies

Failure modev3 freqWhat it meansFirst place it shows in telemetry
FC1 · Specification and system design · 44.4%
Disobey task specification11.8%The agent does something adjacent to what was askedOutput assertion failure with span status OK
Disobey role specification1.5%An agent steps outside its assigned roleTool names appearing under the wrong gen_ai.agent.name
Step repetition15.7%The same step executed again and againRepeating (tool, args-hash) pairs; span count above p99
Loss of conversation history2.80%Earlier context vanishes mid-rungen_ai.conversation.compacted; input tokens dropping mid-conversation
Unaware of termination conditions12.4%The agent does not know it is finishedgen_ai.invoke_agent.tool_calls in the high buckets; duration rising with no new span types
FC2 · Inter-agent misalignment · 32.35%
Conversation reset2.20%The thread restarts from nothingNew gen_ai.conversation.id mid-session; input tokens back to baseline
Fail to ask for clarification6.80%Ambiguity resolved by guessingZero clarifying turns on requests a human would query
Task derailment7.40%The run drifts to a different taskTool distribution shift within one trace
Information withholding0.85%One agent does not pass on what it knowsA retrieved document id never appearing downstream
Ignored other agent's input1.90%A handoff result is discardedSibling invoke_agent output absent from the next prompt
Reasoning-action mismatch13.2%The agent says one thing and does another - including reading an error and answering as if it had notLeaf span in error, root span OK. This is the absorbed-error pattern in Part 2
FC3 · Task verification · 23.5%
Premature termination6.20%The run stops before the task is doneShort trace with a missing expected span type
No or incomplete verification8.20%Nothing checked the resultAbsence of a verification span - a coverage gap, not an error
Incorrect verification9.10%Something checked it and got it wrongVerification span present and OK, output assertion still fails
LiveWhy the largest bucket being "design" changes what you build4 min▶

If you believe agent failures are mostly the model being insufficiently clever, the roadmap writes itself: wait for the next model, spend on prompt engineering, add more examples. That roadmap is aimed at somewhere between a quarter and a third of your failures.

MAST v3 says the plurality of failures - 44.4% - come from specification and system design. Two of the five modes in that category, step repetition at 15.7% and unaware-of-termination at 12.4%, are things a loop counter detects. Together they are 28% of all documented failures, and the fix for both is written in configuration rather than in prose: a hard cap on steps, a machine-checkable termination condition, deduplication of identical calls.

That is a genuinely encouraging finding, and it is why this course exists. The largest slice of the problem is not waiting on a frontier model. It is waiting on somebody to count the spans.

Self-studyWhat MAST does not cover, and where the numbers came from3 min read▶

Three honest limits worth stating before you build a dashboard on these percentages.

  • The corpus is multi-agent systems. The seven frameworks in MAST-Data are agent frameworks - ChatDev, MetaGPT, HyperAgent, AppWorld, AG2, Magentic-One, OpenManus. A single-agent retrieval bot will not have the same distribution, and the FC2 inter-agent category partly does not apply to it. Use the taxonomy as a checklist, and derive your own frequencies from your own traces.
  • The percentages are frequencies within an annotated failure set, not a base rate. They tell you how failures divide up, not how often systems fail. Those are different questions and only the first one is answered here.
  • The v1/v2 versus v3 split is the reason to name your version. This course could not extract v1's own table to reconcile the 41.8/36.9/21.3 triplet line by line, so it is cited only as "the earlier figures". If you need that reconciliation, read v1 directly rather than trusting a summary.

The mechanically useful part is the 94% LLM-annotator accuracy. It means the taxonomy can be applied to your own trace store as a batch job, which turns a paper into a monthly report.

Part 2 · mode to signal to fix

The three shapes an error takes on its way up 7 min live

Before the table, one diagram, because it explains why the table's middle rows are the hard ones. An error at a leaf span has exactly three ways to reach the root, and only one of them is honest.

1 · FAITHFUL status propagates to the root root ERROR invoke_agent ERROR execute_tool ERROR The error reaches the root span. Your error-rate alert fires - the easy case, and the rare one. 2 · ABSORBED the dangerous one - swallowed root OK http 200 invoke_agent OK execute_tool ERROR The agent read the error text, apologised, and returned a plausible answer. Root stays OK, HTTP 200. This is MAST reasoning- action mismatch: 13.2% of all. 3 · AMPLIFIED one failure multiplied by retries root ERROR 40.2s invoke_agent 6 retries execute_tool TIMEOUT One leaf failure becomes six. Status is right; cost is 6x and duration is 40s. The signal is spend and shape, not status. Absorbed errors are why an assertion on output CONTENT, not on status, is the only reliable detector.
🔍 Click to zoom - faithful, absorbed and amplified propagation, and which one your dashboard misses

The Beacon incident is pattern 2 exactly. The warehouse query times out after 4.1s and returns zero rows, a try / except swallows it, the fallback serves a 19.4-hour-old cache entry, and the model faithfully summarises stale data. Every span is green. The reason no alert fired is not that the monitoring was badly configured; it is that status-based monitoring is structurally incapable of seeing an absorbed error.

Failure mode to telemetry signal to fix

FailureThe signal that reveals itThe fix
Wrong tool selectiongen_ai.tool.name distribution shift; a tool result that is never referenced downstreamShrink and disambiguate the tool set. Anthropic's own test: "If a human engineer can't definitively say which tool should be used in a given situation, an AI agent can't be expected to do better"
Tool argument errorsexecute_tool span carrying error.type; a tool_result with is_error: true; retry-same-tool-different-args pairsstrict: true on tool definitions. Return machine-actionable error text, not a stack trace
Retrieval returns nothing usefulRetrieval span present but low max similarity; the answer carries no citation; downstream input length equals the no-context baselineLog retrieved ids and scores as span attributes, so null retrieval is visible rather than inferred
Context exhaustion and truncationstop_reason: "max_tokens" and "model_context_window_exceeded" - the docs say treat both as truncated; cumulative input tokens climbing across one conversation idCompaction, structured note-taking outside the window, sub-agents. Alert on approach to the limit, not on the error
Context rot - decay well before the limitA fixed canary eval score plotted against input length, not against the limitChroma's Context Rot, across 18 models: "LLMs do not maintain consistent performance across input lengths". Distractors hurt even at n=1, and models did better on shuffled haystacks than logically structured ones. Anthropic frames the same thing as an attention budget: "Every new token introduced depletes this budget"
Runaway loopsSpan count per trace above p99; the same (tool, args-hash) repeating; duration rising with no new span types; stop_reason: "pause_turn" from the server-tool 10-iteration defaultHard caps on steps, tokens and wall clock. Explicit machine-checkable termination conditions. Deduplicate identical calls
Silent partial failure - the hardest classAn empty 2-3 token response carrying stop_reason: "end_turn" at HTTP 200 - no error, no content, documented as "typically occurs... particularly after tool results". Also a tool_use block with no matching result blockAssert on the output, never on the status code. Post-conditions per step
Cascading retriesA retry-attempt counter as a span attribute - invisible if you never emit it; cost per trace going bimodal; parent duration roughly N times leaf durationIdempotency keys, retry budgets rather than per-call retries, circuit breakers
Rate limits and 429s429 plus retry-after plus the anthropic-ratelimit-* header family. Alert on *-remaining trending to zero, not on the 429 - by the time you see the 429 you have already dropped the requestRespect token-bucket enforcement: "a rate of 60 RPM might be enforced as 1 request per second", so bursts fail while you are nominally under quota. Acceleration limits will 429 you for ramping too fast
Mid-trajectory timeoutRoot span in error plus a dangling child span with no end timestamp; p99 clipped exactly at your client timeout valueCheckpoint state so a run resumes rather than restarts. Budget for it: "A single 300k-token generation can take over an hour"
Run-to-run non-determinismSame input, different output; divergence at a specific token indexPart 3. The fix is not reproduction, it is capture
Refusalsstop_reason: "refusal" with stop_details, returned as HTTP 200, not an error - completely invisible to error-rate alertingTrack refusal rate as a first-class SLI, segmented by feature and tenant
The four failures that arrive as HTTP 200 An empty response with stop_reason: "end_turn". A truncation with stop_reason: "max_tokens" or "model_context_window_exceeded". A refusal with stop_reason: "refusal". And an absorbed error where your own code caught the exception. All four are successes to an error-rate dashboard. Any monitoring built on status codes alone is blind to the majority of the failure modes in the table above, which is the entire reason this session exists.
Part 3 · why capture beats replay

Non-determinism, and the end of "just run it again" 6 min live

Every debugging instinct you have from ordinary software rests on reproduction: find the input, replay it, watch it fail, fix it. That instinct does not transfer, and there is a clean measurement of why.

1000 completions · one prompt · temperature 0 · Qwen3-235B tokens 1-102: all 1000 identical 992: "Queens, New York" 8: "New York City" divergence begins at token 103 80 unique completions most common one occurred 78 times Not the cause: GPU nondeterminism Kernels are run-to-run deterministic, and floating-point atomics are not the story here. This is the popular explanation and it is wrong. The cause: kernels are not batch-invariant Server batch size varies with other users' load, so the result changes with who else is calling. With batch-invariant kernels, all 1000 matched. You cannot reproduce a production trace by replaying it. So you must capture enough of it at the time. That is the entire argument for tracing over reproduction - and on Claude Opus 4.7+, temperature is deprecated and 400s anyway.
🔍 Click to zoom - 80 unique completions from one prompt at temperature 0, and what actually caused it
LiveThe quote that ends the argument3 min▶

Thinking Machines Lab, Defeating Nondeterminism in LLM Inference (10 September 2025), took the prompt "Tell me about Richard Feynman", ran it 1000 times against Qwen3-235B at temperature 0, and got 80 unique completions. The most common one appeared 78 times. All 1000 were byte-identical through token 102 and diverged at token 103, where 992 said "Queens, New York" and 8 said "New York City".

The interesting part is the diagnosis. It is not GPU nondeterminism - the kernels are run-to-run deterministic. They are simply not batch-invariant: the numerical result of a matmul depends on the batch shape it was executed in, and the batch shape depends on how many other people are hitting the same server at the same moment. In their words, "the other concurrent users are not an 'input' to the system but rather a nondeterministic property." With batch-invariant kernels in place, all 1000 completions came back identical.

Sit with the consequence for a second. The other tenants on your provider's inference server are an uncontrolled input to your output. You cannot set them, log them, or replay them. Therefore you cannot replay your own production request and expect the same trajectory. Whatever you needed to know about that run, you needed to have captured while it was running.

And the one lever people reach for here has been taken away: on Claude Opus 4.7 and later, temperature, top_p and top_k are deprecated and return a 400 on non-default values. Temperature-0 reproducibility is not merely unreliable, it is not on the menu.

Self-studyThree habits that follow from non-reproducibility3 min read▶
  • Never write a bug report that depends on a rerun. "I ran it again and it was fine" is not evidence of a fix, and "I cannot reproduce it" is not evidence of no bug. Both are the expected behaviour of the system. Argue from the captured trace or do not argue.
  • Alert on rates over samples, never on single requests. If 1000 identical inputs give 80 distinct outputs, any per-request quality threshold flaps by construction. Quality is a population statistic here even when the incident is one user's answer.
  • Treat coverage as your reproduction budget. Every span you chose not to emit is a question you have permanently given up the ability to answer about that run. In ordinary software, unclear instrumentation is an inconvenience because you can rerun. Here it is final.
Demo 1 of 2

Make an absorbed error impossible to absorb ★ 12 min · everyone builds

Beacon's Insight Bot is pattern 2 from Part 2. We are not going to fix the warehouse timeout - that is somebody else's ticket. We are going to make it impossible for the same failure to reach an executive silently again, which is the observability job.

Find the try / except that swallows the warehouse timeout. In the except block, before you return the cached fallback, do three things: span.record_exception(e), span.set_status(Status(StatusCode.ERROR)), and set a plain-language attribute such as beacon.fallback.used = true. The status makes it visible to the dashboard; the attribute makes it queryable.

Now add the thing status cannot give you: the age of what you served. Emit beacon.cache.age_seconds on the span. The Beacon incident served a 19.4-hour-old entry - 69,840 seconds. That number was always available in the code and never left the process. One attribute converts an invisible incident into a chart.

Add an assertion on the output, not the status. For Beacon the assertion is a post-condition: the answer must cite at least one row id that was returned in this trace. Zero rows returned means the assertion fails, which means an error status on the root span, which means somebody is told - even though the HTTP response is a perfectly formed 200 and the prose reads beautifully.

Handle the four HTTP 200 failures explicitly on every model call, using the real stop_reason values. Truncation counts both max_tokens and model_context_window_exceeded. Refusal is refusal. The empty-response case is the nasty one: a 2-3 token body with stop_reason: "end_turn", most often right after a tool result.

Break it deliberately and confirm each detector fires. Force zero rows from the warehouse; confirm the root span goes red. Cap max_tokens at 16 and confirm the truncation flag appears. Then re-read the trace and check that a colleague who was not in this room could tell what happened from the attributes alone. If they cannot, the attribute names are wrong.

★ The four HTTP 200 failures, in one guard

TRUNCATED = {"max_tokens", "model_context_window_exceeded"}  ·  if resp.stop_reason in TRUNCATED: span.set_attribute("beacon.truncated", True); span.set_status(Status(StatusCode.ERROR))  ·  if resp.stop_reason == "refusal": span.set_attribute("beacon.refused", True)  ·  if resp.stop_reason == "pause_turn": span.set_attribute("beacon.paused", True)  ·  if not resp.content or len(text_of(resp)) < 40: span.set_attribute("beacon.empty_end_turn", True); span.set_status(Status(StatusCode.ERROR))

The habit this demo is really teaching Status answers "did my code throw". Attributes answer "what did the system actually serve". The Beacon incident is entirely contained in the gap between those two questions, and so is MAST's reasoning-action mismatch at 13.2%. Whenever you catch something, ask what the user received as a result, and put that on the span.
Demo 2 of 2

Build the loop and timeout detectors ★ 10 min · everyone builds

Two of MAST's control-loop modes - step repetition at 15.7% and unaware-of-termination at 12.4% - need no model understanding at all to detect. They are shape problems, and shape is cheap. Third, we catch the timeout that leaves a span half open.

Emit a span attribute for the loop index on every iteration of the agent's tool loop, and an args hash for every tool call: beacon.step and beacon.tool.args_sha. Step repetition is now a duplicate-detection query rather than a reading exercise. Run your last week of traces through it and count.

Record the two agent-call-count metrics the GenAI conventions already define - gen_ai.invoke_agent.tool_calls and gen_ai.invoke_agent.inference_calls. Both are histograms with buckets running 1, 2, 4, 8, 16, 32, 64, 128. The high buckets exist precisely so a runaway loop becomes visible without anybody opening a trace. Note which buckets your agent currently lands in; that is your loop baseline.

Now the diagnostic that separates a legitimately long run from a loop: duration rising with no new span types. A run that gets slower because it is doing more kinds of things is working. A run that gets slower doing the same three span names over and over is stuck. Compute distinct span names per trace alongside span count and plot them together.

Find your dangling spans. Query for child spans with no end timestamp whose root span is in error - that is the mid-trajectory timeout signature. Then check whether your p99 latency is clipped flat at exactly your client timeout value. If it is, your p99 is not measuring your system, it is measuring your own configuration.

Last, turn the 429 alert around. Stop alerting on 429 responses and start alerting on the anthropic-ratelimit-*remaining headers trending toward zero. Token-bucket enforcement means "a rate of 60 RPM might be enforced as 1 request per second", so a burst can fail while your average utilisation dashboard shows comfortable headroom. The 429 is the post-mortem; the remaining counter is the warning.

What you have just built, in MAST's own terms Detectors for step repetition (15.7%), unaware of termination conditions (12.4%), reasoning-action mismatch (13.2%) and premature termination (6.20%). That is 47.5% of the documented failure distribution, caught with a loop counter, an args hash, an output assertion and a span-completeness query. No model changes, no eval suite, no vendor.
Homework

Try it yourself - this week ◐ 50-60 min total

Source material

Official sources covered

Every number on this page comes from a primary source catalogued in this course's source map, and each is cited with its version. Where a claim is derived rather than published, this page says so.

✓MAST: Cemri et al., Why Do Multi-Agent LLM Systems Fail?, arXiv:2503.13657, NeurIPS 2025 Datasets & BenchmarksPart 1 · v3 frequencies, 150 traces, kappa 0.88, MAST-Data 1600+ traces, 94% LLM annotator
✓The version caveat: 41.8 / 36.9 / 21.3 are the v1/v2 figures, not v3Part 1 · named explicitly so nobody quotes the wrong triplet
✓Anthropic stop_reason semantics: max_tokens, model_context_window_exceeded, refusal, pause_turn, and the empty 2-3 token end_turn body at HTTP 200Part 2 + Demo 1 · the four failures that are not errors
✓Rate limiting: the anthropic-ratelimit-* header family, token-bucket enforcement, acceleration limitsPart 2 + Demo 2 · alert on remaining, not on the 429
✓Chroma, Context Rot, 18 models - including the shuffled-versus-structured haystack resultPart 2 · plot quality against input length, not against the limit
✓Thinking Machines Lab, Defeating Nondeterminism in LLM Inference, 10 Sep 2025Part 3 · 1000 completions, 80 unique, divergence at token 103, batch invariance
✓gen_ai.invoke_agent.tool_calls and .inference_calls histograms, buckets 1 to 128Demo 2 · the runaway loop as a metric rather than a reading exercise
◐Sampling the traces these detectors depend on, and the cardinality cost of the attributes you just addedNamed here · b7 is the whole argument
◐Burn-rate alerting and what deserves a page versus a ticketThis page produces the signals · b8 decides who gets woken up
✕Base rates - how often agent systems fail at allNot available. MAST reports how failures divide, not how often they happen. Derive your own from your own traces
✕A line-by-line reconciliation of MAST v1's own category table against v3Could not extract v1's table. The earlier triplet is cited as "the v1/v2 figures" and nothing more is claimed
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · In MAST v3, which category holds the largest share of documented failures, and what does that imply for your roadmap?

The headline of MAST is that the largest bucket is not "the model was dumb". It is design. And the two biggest modes inside it are countable: the same step executed repeatedly, and an agent that does not know it is finished. Both show in span counts and in the gen_ai.invoke_agent.tool_calls histogram. Note the version: v3 is 44.4 / 32.35 / 23.5, and the widely quoted 41.8 / 36.9 / 21.3 are the earlier v1/v2 figures.

2 · A leaf execute_tool span is in error. The agent reads the error text, apologises in prose, and returns a confident answer. The root span is OK and the HTTP status is 200. Which detector catches this?

This is the absorbed pattern, and it is MAST's reasoning-action mismatch at 13.2%. The whole point is that the status signal was consumed by your own exception handler, so nothing status-based recovers it. The Beacon incident is exactly this shape: a swallowed warehouse timeout, a 19.4-hour-old cache entry served as fresh, and every span green. Assert on what the user received, not on whether your code threw.

3 · One prompt, temperature 0, 1000 completions, 80 unique results, all identical through token 102. What is the operational consequence?

The cause is not GPU nondeterminism - the kernels are run-to-run deterministic but not batch-invariant, and batch size varies with concurrent load, so "the other concurrent users are not an 'input' to the system but rather a nondeterministic property". With batch-invariant kernels all 1000 matched. C fails twice: temperature was already 0 in the experiment, and on Claude Opus 4.7+ temperature, top_p and top_k are deprecated and return 400 on non-default values. Capture, do not replay - which is the argument for tracing.

Builder session 6 cheat sheet · pin this

MAST v3, not v144.4 / 32.35 / 23.5 for specification, misalignment, verification. The circulated 41.8 / 36.9 / 21.3 are v1/v2. Always name the version.
The headlineThe biggest bucket is design, not model capability. Step repetition 15.7% + unaware-of-termination 12.4% = 28% of failures that a span count finds.
Absorbed errorsLeaf span in error, root span OK, plausible answer returned. MAST reasoning-action mismatch, 13.2%. Only an assertion on output content catches it.
The four HTTP 200 failuresEmpty 2-3 token end_turn; max_tokens AND model_context_window_exceeded; refusal; your own caught exception. Zero of them raise an error.
Context rotQuality decays well before the limit. Plot a canary score against input length, not against the limit. Shuffled haystacks beat structured ones.
Alert on remaining, not on 429Watch anthropic-ratelimit-*remaining trend to zero. Token-bucket enforcement means 60 RPM can be enforced as 1 per second, so bursts fail under quota.
Timeout signatureA dangling child span with no end timestamp, plus a p99 clipped flat at your client timeout. A flat p99 measures your config, not your system.
Capture, do not replay1000 completions at temperature 0 gave 80 unique outputs, diverging at token 103. Concurrent load is an uncontrolled input. Whatever you needed, you needed to capture it live.