Implement Error Propagation Strategies Across Multi-Agent Systems
This module is about how subagents should report failure so the coordinator can recover intelligently.
The exam’s core idea:
Bad subagent output:
"Search unavailable."
Good subagent output:
"I searched X and Y, source X timed out after 2 retries, source Y returned 3 partial matches, source Z was not attempted because credentials were missing, and the coordinator can try A or B next."
MCP-style tool execution errors are meant to contain actionable feedback that a model can use to self-correct or retry, while protocol-level errors represent request-structure failures that are less likely to be model-recoverable. That same idea applies at multi-agent level: subagents should return enough structured context for the coordinator to decide whether to retry, route around the failure, ask the user, or synthesize with caveats. (Model Context Protocol)
1. Core mental model
In a multi-agent system:
Subagent = attempts a focused task.
Coordinator = decides what to do with the result.
Synthesizer = combines successful, partial, and failed results into a final answer.
The coordinator cannot recover from vague errors.
Vague error:
"Search failed."
Structured error:
{
"failure_type": "transient_timeout",
"attempted_query": "refund policy competitor price matching",
"sources_attempted": ["policy_db", "help_center"],
"partial_results": [
{
"source": "help_center",
"finding": "Own-site price adjustment policy found, but no competitor price match policy found."
}
],
"retryable": true,
"attempts_made": 2,
"alternative_approaches": [
"Search internal policy exceptions database",
"Escalate policy gap to human reviewer"
]
}
Exam shortcut:
Subagents should not just report failure.
They should report what failed, what was tried, what partially worked, and what could be tried next.
Subagents in Claude Code run in separate context windows and return a result back to the parent conversation; the parent does not see all intermediate subagent tool calls unless the subagent includes relevant details in its final output. (Claude)
2. Why generic errors are bad
Generic errors hide the recovery path.
Bad error output
The coordinator does not know:
Was the source unavailable?
Was the query malformed?
Was access denied?
Did the source return no matches?
Were there partial results?
Should it retry?
Should it ask the user?
Should it try another source?
Should it synthesize with a coverage caveat?
Better error response:
{
"status": "partial_failure",
"failure_type": "source_timeout",
"failed_source": "policy_db",
"attempted_query": "competitor price match refund exception",
"attempts_made": 2,
"retryable": true,
"partial_results": [
{
"source": "public_help_center",
"finding": "Policy covers own-site price drops within 7 days.",
"confidence": "high"
}
],
"missing_coverage": [
"Internal exception policy database unavailable."
],
"alternative_approaches": [
"Retry policy_db later",
"Escalate competitor price-match request as policy gap"
]
}
Now the coordinator can decide.
3. Structured error context
A strong subagent error should include:
1. failure_type
2. attempted_query or attempted_action
3. sources/tools attempted
4. attempts_made
5. retryability
6. partial_results
7. missing_coverage
8. alternative_approaches
9. coordinator_recommendation
10. coverage_impact
3.1. Recommended schema
{
"agent_name": "policy_research_agent",
"task": "Find refund policy for competitor price matching",
"status": "success | empty_result | partial_failure | failure",
"failure_type": "none | transient_timeout | permission_denied | rate_limited | invalid_query | source_unavailable | policy_gap | unknown",
"attempted_query": "competitor price match refund exception",
"sources_attempted": [
{
"source": "policy_db",
"status": "failed",
"failure_type": "transient_timeout",
"attempts": 2
},
{
"source": "help_center",
"status": "success",
"result_count": 1
}
],
"partial_results": [
{
"finding": "Own-site price adjustments are allowed within 7 days.",
"source": "help_center",
"confidence": "high"
}
],
"missing_coverage": [
"Internal policy exception database could not be searched."
],
"retryable": true,
"alternative_approaches": [
"Retry policy_db",
"Escalate competitor price matching as policy gap"
],
"coordinator_recommendation": "escalate_policy_gap_if_policy_db_remains_unavailable"
}
This is much more useful than "search unavailable".
4. Access failure vs valid empty result
This is one of the highest-yield distinctions.
Valid empty result
The query ran successfully, but no matches were found.
{
"status": "empty_result",
"failure_type": "none",
"attempted_query": "customer_id=CUST-12345 open_refunds",
"source": "refund_db",
"result_count": 0,
"message": "Query succeeded. No open refunds found for this customer.",
"retryable": false
}
Coordinator action:
Proceed using the fact that no matching records exist.
Do not retry blindly.
Do not treat it as system failure.
Access failure
The query did not successfully execute.
{
"status": "failure",
"failure_type": "transient_timeout",
"attempted_query": "customer_id=CUST-12345 open_refunds",
"source": "refund_db",
"result_count": null,
"message": "refund_db timed out before returning results.",
"retryable": true,
"attempts_made": 2
}
Coordinator action:
Exam distinction
Empty result = source was accessed successfully and returned zero matches.
Access failure = source was not successfully accessed, so the truth is unknown.
5. Anti-patterns
Anti-pattern 1: Silently suppressing errors
Bad:
when the actual situation was:
Why bad:
Correct:
{
"status": "failure",
"failure_type": "transient_timeout",
"results": [],
"message": "Policy database timed out; no conclusion can be drawn from this source."
}
Anti-pattern 2: Terminating the whole workflow on one failure
Bad:
Better:
Preserve partial results.
Mark missing coverage.
Try alternative sources.
Let the coordinator decide whether the final answer can proceed with caveats.
Anti-pattern 3: Generic status only
Bad:
Better:
{
"status": "partial_failure",
"failure_type": "rate_limited",
"attempted_query": "orders with chargebacks in last 30 days",
"attempts_made": 1,
"retryable": true,
"retry_after_seconds": 60,
"partial_results": [],
"alternative_approaches": [
"Retry after rate limit window",
"Use analytics replica if freshness of 24h is acceptable"
]
}
6. Local recovery before propagation
Subagents should not immediately send every transient failure to the coordinator. They should first attempt local recovery when safe.
Local recovery examples
Timeout → retry with backoff.
Rate limit → wait or use smaller query window.
Large result set → narrow query or paginate.
Invalid query → correct syntax if obvious.
One unavailable source → try approved backup source.
Claude Code’s agent loop generally follows a gather-context, take-action, verify-results pattern, and subagents can run focused tasks in isolated contexts before returning summarized results to the parent. (Claude)
Good subagent behavior
{
"status": "partial_failure",
"task": "Search refund policy sources",
"local_recovery_attempted": true,
"recovery_steps": [
{
"attempt": 1,
"action": "query policy_db",
"result": "timeout"
},
{
"attempt": 2,
"action": "retry policy_db with narrower query",
"result": "timeout"
},
{
"attempt": 3,
"action": "search public_help_center",
"result": "success"
}
],
"partial_results": [
{
"source": "public_help_center",
"finding": "Own-site price adjustment policy found."
}
],
"unresolved_errors": [
{
"source": "policy_db",
"failure_type": "transient_timeout",
"retryable": true
}
]
}
Exam rule
Subagents should recover locally from transient failures when reasonable.
They should propagate only unresolved errors, plus what they already tried and any partial results.
7. Coordinator recovery decisions
The coordinator should use error context to choose the next action.
Coordinator decision table
| Subagent output | Coordinator action |
|---|---|
success with strong results |
Use result |
empty_result |
Treat as successful no-match |
partial_failure with useful partials |
Synthesize with caveat or assign another agent |
transient_timeout, retryable |
Retry or use backup source |
permission_denied |
Escalate, request access, or omit source with coverage note |
invalid_query |
Ask subagent to reformulate query |
policy_gap |
Escalate to human/policy owner |
| multiple sources unavailable | Synthesize only if coverage remains sufficient |
| no meaningful progress | Escalate or ask user for more information |
Example coordinator reasoning
Subagent says:
{
"status": "partial_failure",
"failure_type": "source_unavailable",
"partial_results": [
{
"source": "help_center",
"finding": "Own-site price adjustment policy found."
}
],
"missing_coverage": [
"Internal exception database unavailable."
],
"alternative_approaches": [
"Escalate competitor price match request"
]
}
Coordinator should not say:
Better:
The available source confirms own-site price adjustment policy, but the internal exception database was unavailable. Competitor price matching remains unverified, so this should be escalated as a policy gap.
8. Partial results
Partial results are valuable. Do not throw them away.
Bad
Good
{
"status": "partial_failure",
"completed_sources": [
{
"source": "orders_db",
"status": "success",
"result": {
"order_id": "ORD-73821",
"status": "delivered",
"actual_delivery_date": "2026-06-18"
}
}
],
"failed_sources": [
{
"source": "refund_policy_db",
"status": "timeout",
"attempts": 2,
"retryable": true
}
],
"coverage_impact": "Order facts are verified, but refund eligibility policy could not be verified."
}
Coordinator can now proceed carefully:
9. Structuring synthesis with coverage annotations
The synthesizer should not present partial research as complete.
Use coverage annotations.
Good synthesis output
## Well-supported findings
1. Order ORD-73821 was delivered on 2026-06-18.
- Source: orders_db
- Coverage: complete for order status and delivery dates
- Confidence: high
2. The customer requested a full refund.
- Source: conversation history
- Coverage: complete for customer-stated request
- Confidence: high
## Gaps / unavailable sources
1. Refund eligibility policy could not be verified.
- Source attempted: refund_policy_db
- Failure: timeout after 2 attempts
- Impact: cannot determine whether the order qualifies for refund autonomously
## Recommended next step
Escalate refund eligibility decision or retry policy lookup later.
Exam point:
10. Coverage annotation schema
{
"synthesis": {
"well_supported_findings": [
{
"finding": "Order ORD-73821 was delivered on 2026-06-18.",
"sources": ["orders_db"],
"coverage": "complete",
"confidence": "high"
}
],
"partial_findings": [
{
"finding": "Own-site price adjustment policy exists.",
"sources": ["public_help_center"],
"coverage": "partial",
"missing_sources": ["internal_policy_db"],
"confidence": "medium"
}
],
"coverage_gaps": [
{
"topic": "competitor price match exceptions",
"reason": "internal_policy_db unavailable",
"attempted_queries": ["competitor price match refund exception"],
"impact": "Cannot determine if autonomous approval is allowed."
}
],
"recommended_next_actions": [
"Retry internal_policy_db",
"Escalate to human policy reviewer"
]
}
}
This prevents the final answer from overstating what the agents actually verified.
11. Subagent output contract
For exam purposes, think of every subagent as needing an output contract.
{
"agent_name": "string",
"task": "string",
"status": "success | empty_result | partial_failure | failure",
"summary": "string",
"key_findings": [
{
"finding": "string",
"source": "string",
"confidence": "high | medium | low",
"evidence": "string"
}
],
"attempted_actions": [
{
"action": "string",
"target": "string",
"result": "success | empty | failed",
"failure_type": "string | null"
}
],
"partial_results": [],
"errors": [
{
"failure_type": "transient_timeout | permission_denied | invalid_query | rate_limited | source_unavailable | unknown",
"source": "string",
"attempted_query": "string",
"retryable": true,
"attempts_made": 0,
"message": "string"
}
],
"alternative_approaches": [],
"coverage_impact": "string",
"coordinator_recommendation": "string"
}
Subagents should not return long reasoning chains unless specifically needed. Claude Code workflow guidance notes that when Claude orchestrates agents directly, every result lands in a context window; scripted workflows can keep intermediate results in script variables and pass only the final answer into context. (Claude)
12. Example: search subagent
Scenario
A research subagent must find evidence for three claims:
1. Refund policy for late delivery
2. Competitor price-match exception
3. Prior customer refund history
One source times out, one returns no matches, one succeeds.
Good subagent result
{
"agent_name": "support_research_agent",
"task": "Research refund eligibility for CUST-44391 and ORD-73821",
"status": "partial_failure",
"key_findings": [
{
"finding": "Order ORD-73821 was delivered 6 days after promised delivery date.",
"source": "orders_db",
"confidence": "high",
"evidence": "promised_delivery_date=2026-06-12; actual_delivery_date=2026-06-18"
},
{
"finding": "No prior refunds found for customer CUST-44391.",
"source": "refund_history_db",
"confidence": "high",
"evidence": "Query succeeded with result_count=0"
}
],
"attempted_actions": [
{
"action": "lookup_order",
"target": "ORD-73821",
"result": "success",
"failure_type": null
},
{
"action": "search_refund_history",
"target": "CUST-44391",
"result": "empty",
"failure_type": null
},
{
"action": "search_policy_db",
"target": "late delivery refund competitor match",
"result": "failed",
"failure_type": "transient_timeout"
}
],
"partial_results": [
{
"source": "orders_db",
"data": {
"order_id": "ORD-73821",
"delay_days": 6,
"status": "delivered"
}
}
],
"errors": [
{
"failure_type": "transient_timeout",
"source": "policy_db",
"attempted_query": "late delivery refund competitor match",
"retryable": true,
"attempts_made": 2,
"message": "Policy DB timed out twice."
}
],
"alternative_approaches": [
"Retry policy_db later",
"Search public help center policy",
"Escalate policy eligibility decision to human agent"
],
"coverage_impact": "Order facts and refund history are verified. Refund policy eligibility is not verified.",
"coordinator_recommendation": "Do not approve refund autonomously until policy source is available or a human reviews."
}
Notice the important distinction:
13. Example: coordinator response
Given the above subagent output, the coordinator should produce:
## Verified facts
- Order ORD-73821 was delivered 6 days late.
- The customer has no prior refunds in the refund history database.
## Unverified area
- Refund eligibility policy could not be checked because policy_db timed out after 2 attempts.
## Decision
Do not autonomously process the refund yet. Either retry the policy lookup or escalate the eligibility decision to a human agent.
Bad coordinator response:
Why bad:
14. Local recovery pattern
A subagent should have a recovery ladder.
1. Try primary source.
2. If transient failure, retry with backoff.
3. If still failing, try approved alternate source.
4. If alternate succeeds, return partial result with missing coverage.
5. If all fail, propagate structured error.
Example:
{
"recovery_steps": [
{
"step": 1,
"action": "query primary policy_db",
"result": "timeout"
},
{
"step": 2,
"action": "retry policy_db with narrower query",
"result": "timeout"
},
{
"step": 3,
"action": "query public_help_center",
"result": "success"
}
],
"final_status": "partial_failure"
}
Do not over-retry forever. Cap retries and propagate the unresolved error.
15. Multi-agent workflow example
Coordinator
├── Order subagent
├── Policy subagent
├── Refund history subagent
└── Synthesis subagent
Order subagent output
{
"status": "success",
"key_findings": [
{
"finding": "Order delivered 6 days late.",
"confidence": "high"
}
]
}
Policy subagent output
{
"status": "partial_failure",
"key_findings": [
{
"finding": "Own-site price adjustment policy exists.",
"confidence": "high"
}
],
"errors": [
{
"failure_type": "permission_denied",
"source": "internal_exception_policy_db",
"retryable": false
}
],
"coverage_impact": "Cannot verify exception policy."
}
Refund history subagent output
{
"status": "empty_result",
"source": "refund_history_db",
"result_count": 0,
"message": "Query succeeded. No prior refunds found."
}
Synthesis should say
Order facts are verified.
Refund history has a valid empty result.
Exception policy coverage is missing due to permission denial.
Escalate the exception-policy decision.
16. Common exam traps
Trap 1: Empty result treated as error
Wrong:
Right:
Trap 2: Access failure treated as empty result
Wrong:
Right:
Trap 3: Generic error propagated
Wrong:
Right:
Trap 4: Silent suppression
Wrong:
Right:
Trap 5: One failed subagent kills whole workflow
Wrong:
Right:
Trap 6: Synthesis hides uncertainty
Wrong:
when policy source was unavailable.
Right:
Order facts support a possible refund claim, but refund policy eligibility could not be verified because the policy source timed out.
17. Scenario-based practice questions
Question 1
A subagent returns:
The coordinator cannot decide whether to retry, use another source, or synthesize partial results.
What is the best fix?
A) Terminate the whole workflow.
B) Return structured error context including failure type, attempted query, attempts made, partial results, and alternative approaches.
C) Hide the error and return empty results.
D) Ask the coordinator to guess.
Answer
B. Structured error context enables recovery decisions.
Question 2
A refund history query succeeds and returns zero matches.
How should the subagent report this?
A) As an access failure.
B) As a valid empty result with result_count: 0.
C) As a transient timeout.
D) As a policy gap.
Answer
B. A successful query with no matches is not an error.
Question 3
A policy database times out before returning results.
How should the subagent report this?
A) As result_count: 0.
B) As an access failure with retryability and attempts made.
C) As proof that no policy exists.
D) As success.
Answer
B. Timeout means the truth is unknown, not that the source has no matching policy.
Question 4
A subagent searched three sources. Two succeeded and one failed. What should it return?
A) Only the failure.
B) Only the successes, hiding the failure.
C) Partial results plus structured error details for the failed source.
D) Nothing.
Answer
C. Partial results and unresolved errors should both be propagated.
Question 5
A transient source timeout occurs. What should the subagent do before escalating the error?
A) Immediately terminate the entire workflow.
B) Attempt local recovery, such as retrying with backoff or trying an approved alternate source.
C) Return success with empty results.
D) Invent results.
Answer
B. Subagents should locally recover from transient failures when reasonable.
Question 6
A coordinator receives:
What should it infer?
A) The source failed.
B) The query succeeded and found no matches.
C) The result is unknown.
D) The subagent should be retried indefinitely.
Answer
B. empty_result means successful no-match.
Question 7
A coordinator receives:
What is the best next step?
A) Treat it as no policy found.
B) Escalate, request access, or synthesize with a coverage gap.
C) Ignore the error.
D) Mark all findings as well-supported.
Answer
B. Permission failure leaves a coverage gap. It is not equivalent to an empty result.
Question 8
A synthesizer combines several subagent outputs. Some findings are fully supported, while others depend on sources that failed.
What should the synthesizer include?
A) Only a confident final answer with no caveats.
B) Coverage annotations distinguishing well-supported findings from gaps due to unavailable sources.
C) Raw logs only.
D) No answer.
Answer
B. Coverage annotations prevent overclaiming and help downstream users understand reliability.
Question 9
A subagent fails to find a document because it queried the wrong identifier format. It reports only “not found.”
What context should it include?
A) Nothing else.
B) The attempted query and identifier format, so the coordinator can retry with a corrected query.
C) A fake document.
D) A generic success status.
Answer
B. The attempted query helps diagnose whether the empty result is valid or caused by a query issue.
Question 10
Which is the best subagent error output?
A)
B)
{
"status": "partial_failure",
"failure_type": "rate_limited",
"attempted_query": "recent chargebacks by customer",
"attempts_made": 1,
"retryable": true,
"retry_after_seconds": 60,
"partial_results": [],
"alternative_approaches": ["retry after rate limit", "use analytics replica"]
}
C)
D)
Answer
B. It includes the context needed for coordinator recovery.
18. Final D5.3 checklist
Memorize this:
1. Generic errors hide recovery options.
2. Subagents should return structured error context.
3. Include failure_type, attempted_query, attempted sources, attempts_made, and retryability.
4. Include partial_results when available.
5. Include alternative_approaches for coordinator recovery.
6. Distinguish access failures from valid empty results.
7. Empty result means the query succeeded with zero matches.
8. Access failure means the truth is unknown.
9. Do not silently suppress errors by returning empty success.
10. Do not terminate the entire workflow because one subagent failed.
11. Subagents should attempt local recovery for transient failures.
12. Propagate only unresolved errors, with recovery attempts documented.
13. Coordinators should decide retry, alternate source, escalation, or caveated synthesis.
14. Synthesis should annotate coverage.
15. Clearly separate well-supported findings from gaps caused by unavailable sources.
D5.3 mental model
Subagent errors should be useful, not merely visible.
Empty result = successful no-match.
Access failure = unknown result.
Partial failure = preserve partial results and mark gaps.
Coordinator = recovery decision-maker.
Synthesizer = truth plus coverage caveats.
The exam’s favorite distinction is:
A failed search is not the same as no results. Subagents must propagate enough structured context for the coordinator to recover intelligently.