Security

Testing Failure Propagation in a Chain of Agents

Antonio Evans9 min read

A side view of a chain of four linked nodes on a workbench, connected left to right by short cables.
Key points

Key points

  1. 1Write the failure model as a table before writing tests, with one row per hop that names the failure, the error type, the caller's decision and the log fields that prove it.
  2. 2Replace every downstream agent with a switchable stub that can return an explicit error, a timeout, a well-formed wrong answer or nothing at all.
  3. 3Assert on the caller's decision and on the audit trail, not only on the final user-facing text.
  4. 4A well-formed but wrong answer is the hardest case, because semantic failures propagate without raising exceptions and a successful status code is not evidence of correct behavior.

Turn the reviewed failure model into a test table

Review comments about a chain of agents usually describe the happy path. The failure model is the part that says which hop can fail, what error each caller must raise, and what each caller is allowed to do with that error. Until that model is written down, no test can be wrong or right, because there is nothing to assert against.

Start by listing every hop in the chain. A typical research pipeline has a planner, a retriever, a tool executor, a summarizer and a writer. Each hop has one downstream dependency and one upstream caller, which gives you two questions per hop: what can fail here, and what must my caller do about it.

The table below is the artifact to get reviewed. Fill one row per hop and per failure class. The point of the exercise is that the caller decision column holds a decision, not a sentence. If you cannot choose retry, fall back or abort for a row, you do not yet understand that hop.

HopFailure classError the hop raisesCaller decisionLog fields that prove it
RetrieverTool returns HTTP 500ToolError with tool name and statusRetry up to 2 times, then abort the tasktrace_id, hop, tool, status, attempt, final_decision
RetrieverTool call exceeds 20 sTimeoutError with elapsed msAbort, no retry, set partial context flagtrace_id, hop, timeout_ms, retry_suppressed
PlannerModel returns unparseable planContractError with raw payload hashAbort the run, do not pass to executortrace_id, hop, validator, payload_hash
ExecutorValid plan names a nonexistent toolValidationErrorAbort, log tool name, page the owning teamtrace_id, hop, requested_tool, known_tools_version
SummarizerPlausible but wrong summaryNone raised (semantic)Re-check claims against retrieved source, flag mismatchtrace_id, hop, claims_checked, claims_failed
WriterOutput omitted a required fieldSchemaErrorAbort delivery, keep the drafttrace_id, hop, missing_field, draft_id

Two rows in that table behave very differently. The first four failures raise exceptions and stop the chain. The summarizer row is the dangerous one, because nothing throws. Work on LLM-based multi-agent systems has shown that they coordinate through unstructured natural language, which makes them prone to semantic failures such as hallucinations, misinterpreted instructions and reasoning drift that propagate silently without raising runtime exceptions MAS-FIRE. A test suite that only checks for exceptions will pass while the worst class of failure runs through your chain untouched.

Place a fault injector at every hop

The practical way to test that table is to make each hop's downstream dependency a stub you can switch into one of four modes. Do it in a test environment only, behind a flag that is off by default in production.

  • Error stub: returns a well-formed error payload with a status code and a message. Tests your error mapping.
  • Timeout stub: sleeps past the configured deadline, then closes the connection. Tests deadline handling and retry suppression.
  • Wrong-answer stub: returns a schema-valid response whose content is incorrect, for example a summary citing a value that is absent from the source. Tests your semantic checks.
  • Empty stub: returns success with zero content or an empty list. Tests the code path most teams forget, the one between "success" and "something usable."

A transparent proxy is the least invasive place to put these. The Agent Error Simulator intercepts agent-to-model HTTP traffic through a transparent proxy and injects three categories of faults, format, logic and tool call, at configurable points in a planner, worker and aggregator workflow Agent Error Simulator. Using the same idea means your application code needs no testing flag, and the injected fault travels the same path as a real one.

The same paper defines the injection point as four parameters: error type, target agent role and index, refinement step, and optionally when and how the fault becomes visible in the audit trail Agent Error Simulator. That last parameter is the one most teams skip, and it is the one that proves detection works. Inject a fault that appears in the logs only after the chain has already delivered an answer, and you will find out whether your monitoring or your output validation is doing the real work.

A minimal stub in Python that covers all four modes is worth keeping in the repository. It deliberately separates the deadline from the returned error, so the caller cannot confuse a timeout with an explicit failure unless it reads the error type.

import time

class FaultyAgentStub:
    def __init__(self, mode="ok", delay_s=0.0):
        self.mode, self.delay_s = mode, delay_s

    def run(self, task):
        if self.delay_s:
            time.sleep(self.delay_s)
        if self.mode == "error":
            return {"ok": False, "error": {"type": "UpstreamError", "status": 503}}
        if self.mode == "timeout":
            raise TimeoutError(f"no response after {self.delay_s}s")
        if self.mode == "wrong":
            return {"ok": True, "answer": "The audit passed with zero exceptions."}
        if self.mode == "empty":
            return {"ok": True, "answer": ""}
        return {"ok": True, "answer": "expected text"}

That stub is small on purpose. A test that needs twenty lines to set up a fault will be deleted the first time the suite gets slow. The stub should be boring enough that every hop can use a copy of it.

Assert on the caller's decision, not on the prose

Your test has three things to check per row of the failure table, and they are separate assertions. First, the caller must raise or return the error type the model names. Second, the caller must take the decision the model names, which is usually observable as a call count, a fallback branch or a terminated task. Third, the log line must contain the fields that prove the decision happened.

def test_retriever_logs_and_retries_once_on_500(stub, caller, caplog):
    stub.mode = "error"
    result = caller.run_task(...)
    assert result.failed
    rec = caplog.records[-1]
    assert rec.hop == "retriever"
    assert rec.final_decision == "abort"
    assert stub.calls == 2   # retry cap of 2, not a loop

The retry cap deserves a numeric test rather than a comment. Every retry in an agent chain resends context to the model, so a retry loop does not just add HTTP round trips, it multiplies token spend. The cost argument is documented in practice: a retry loop that sends ten requests to a model consumes ten times the tokens of one attempt Preventing Cascading Failures in AI Agents. Put the cap in the failure table and assert the exact call count. A test that only asserts "failed" will pass at two retries and at two hundred.

Also test the interaction between deadline and retry. If the hop deadline is 20 seconds and the caller retries twice, the worst-case caller wait is roughly 60 seconds plus backoff. That number belongs in the table next to the decision, because it is the number your upstream coordinator and your user-facing timeout must be reconciled with.

Prove the cascade stops at the hop

Fault injection in multi-agent systems tends to find that recovery behavior depends on both model capability and architecture. The Agent Error Simulator reports a model-size dependent recovery threshold for logic errors between 7B and 27B parameters, and universal catastrophic failure under aggregator-level logic injection Agent Error Simulator. MAS-FIRE reports that architecture plays a decisive role, with iterative closed-loop designs neutralizing over 40 percent of faults that cause catastrophic collapse in linear workflows MAS-FIRE.

The decision rule that falls out of those findings is useful for test design. If a fault at hop N collapses the chain, ask whether the chain is linear or whether some loop caught it. If it is linear, the fix is a check at the boundary, not a bigger model. If it is a loop, the fix is a termination condition, because a loop that catches a fault by retrying can also catch it forever.

Write one test per hop that asserts the rest of the chain never ran. A simple way is a spy counter on the downstream stubs: after injecting a fatal fault at the planner, the executor and writer stubs must have zero calls. That single assertion turns "the error was caught" into "the error was contained."

The trust-boundary view explains why this matters beyond one run. Multi-agent trust boundary violations describe cascade failure propagation, where failures in one agent cause cascading failures in dependent agents and can bring down entire business processes FINOS risk guidance. The same guidance lists agent authority impersonation and cross-agent privilege inheritance among the mechanisms FINOS risk guidance. Both are testable with the same stub pattern: a stub that returns a valid answer while claiming a higher privilege level should be rejected by the caller, and that rejection is an assertion.

The same logic applies to shared state. If two hops write to the same store, inject a corrupted write at the first hop and assert that the second either refuses the record or quarantines it. The FINOS list calls this shared resource contamination FINOS risk guidance. A test that exercises one hop at a time will never see it.

Use red-teaming tooling where it fits

Failure injection and agent red teaming overlap, and it is worth being clear about which is which. Red teaming asks whether an adversary can make the agent misbehave. Fault injection asks whether the agent handles a broken dependency correctly. Both belong in your suite, but they assert different things.

Microsoft's AI Red Teaming Agent, built on the PyRIT framework, runs automated scans for content risks, scores attack-response pairs to produce metrics such as Attack Success Rate, and generates a score card of probing techniques and risk categories Microsoft Foundry documentation. That is the right tool for the adversarial half. Use it to populate the wrong-answer stub with payloads, so the well-formed but incorrect responses you inject look like real attacks rather than placeholder text.

The red teaming methodology guidance is also relevant at the mapping stage, because it forces you to enumerate every tool, every external system and every delegation path before testing, which is the same inventory your failure table needs DeepTeam guide. One enumeration, two uses.

Where an agent consumes external content, the prompt injection cheat sheet is the reference for building the malicious payloads that belong in your wrong-answer and poisoned-context cases, including indirect injection hidden in documents and encoded instructions OWASP prompt injection cheat sheet. A useful discipline is to keep those payloads in a shared fixture directory so the security tests and the resilience tests draw on the same inputs.

Run the suite continuously and watch the ratios

A failure test suite decays. The stub gets refactored, a timeout becomes a retry, and nobody notices that the assertion now passes for the wrong reason. Two habits keep it alive.

First, record the outcome of every run as a small set of ratios: how many injected faults were caught at the intended hop, how many were caught downstream, and how many reached the final output undetected. The third number is the one to watch, and it should be zero. The Agent Error Simulator uses exactly this kind of structured result logging to make fault tolerance comparable across runs Agent Error Simulator.

Second, keep the failure table in the same repository as the tests and treat a change to either as a change to both. If someone adds a retry, they must add the row and the assertion. This is ordinary change control, but teams that run continuous red teaming already have the muscle: practical guidance notes that agent behavior can change after model updates or new tool additions, so testing must be ongoing rather than a one-time gate Fiddler AI.

For the logging half, log provenance at each hop: the delegating agent, the receiving agent, the scoped credential, the task correlation id and the verification state. That field list is recommended so investigators can reconstruct why a downstream action was trusted NHI Management Group. It is also the exact list your log assertions should check, which means the observability work and the test work are the same work.

One more decision belongs in the model: who gets paged. A failure that aborts the run is a resilience event. A failure that reaches the final output as a wrong answer is a correctness incident with a different on-call and a different severity, and the log field that distinguishes them is the one your test should assert.

If the chain runs on several model providers and you want the failure tests to behave the same across all of them, one API key covering text, image, audio and embedding models keeps the stub matrix from multiplying. That is what Quanta Inference is for: https://quantaintelligence.ai/inference.

FAQ

FAQ

Do I need to test every hop against every failure mode?

No. Cross the table instead. Every hop gets the explicit error and the timeout, because those are generic, while only hops that consume or produce free text get the wrong-answer case. Only hops with a write side effect get the empty and corrupted-write cases, which keeps the suite at a few dozen tests rather than a few hundred.

How is a timeout different from an error in these tests?

They should take different paths in your caller. An explicit error means the dependency answered and said no, which is usually retryable if the cap allows, while a timeout means you do not know whether the operation happened and a retry can duplicate a side effect. The Agent Error Simulator separates error type from the step at which the fault is applied precisely because recovery behavior differs by mechanism Agent Error Simulator.

How do I test a failure that raises no exception?

Assert on content, not status. For a summarizer, check that every claim maps to a retrieved source span and fail the test when a claim does not, which is why the wrong-answer stub exists and why semantic failures deserve their own row MAS-FIRE.

Should fault injection run in staging or only in CI?

Both, with different goals. In CI, run the full matrix on every change to the chain, because the assertions are cheap and hermetic, while in staging run a smaller set with real credentials and real logging sinks, because that is where you learn whether the log fields arrive where your alerting expects them.

How many retries should the failure model allow?

Set the cap from the worst-case wait you can tolerate, not from what feels safe. If the hop deadline is 20 seconds, two retries plus backoff is already near a minute, and every attempt resends context and costs tokens Preventing Cascading Failures in AI Agents. Write the number in the table and assert the exact call count in the test.

Sources

Sources

  1. MAS-FIRE: Fault Injection and Reliability Evaluation for ...
  2. Agent Error Simulator: Fault Injection for Testing Recovery in ...
  3. Preventing Cascading Failures in AI Agents
  4. Multi-Agent Trust Boundary Violations
  5. AI Red Teaming Agent - Microsoft Foundry
  6. Complete Guide to Agentic AI Red Teaming | DeepTeam
  7. LLM Prompt Injection Prevention Cheat Sheet
  8. AI Agent Red Teaming: Techniques and Attack Surfaces | Fiddler AI Blog
  9. Multi-agent trust boundaries in AI systems: what breaks first?
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles