Study guides / CCAR-F / Domain 5

Context Management & Reliability · Lesson 3 of 6

5.3 - Error Propagation in Multi-Agent Systems

Implement error propagation strategies across multi-agent systems, including structured error context, the distinction between access failures and valid empty results, and coverage annotations.

Error propagation determines whether a multi-agent system recovers gracefully or fails silently. When a subagent encounters a failure - a timeout, a permission error, an invalid query - how that failure information flows back to the coordinator dictates the system's reliability. The exam tests your understanding of structured error context, the two critical anti-patterns, and the distinction that most developers get wrong: access failures versus valid empty results.

Structured Error Context

When a subagent fails, it must return structured error context that enables the coordinator to make intelligent recovery decisions. This context must include four elements:

1. Failure type. Categorise the failure: transient (timeout, rate limit - may succeed on retry), validation (bad input - fix the query), business (rule violation - escalate or find alternative), or permission (access denied - cannot be retried without authorisation changes).

2. What was attempted. The specific query, parameters used, and target system. "Searched academic database for 'renewable energy policy' with date range 2022-2024" is actionable. "Search failed" is not.

3. Partial results gathered before failure. If the subagent retrieved three of five sources before timing out, those three results are valuable. Discarding them because the overall operation failed is wasteful.

4. Potential alternative approaches. The subagent knows its domain. If an academic database is down, it might suggest trying a different database, broadening the search terms, or checking cached results. These suggestions help the coordinator decide on recovery strategy.

{
  "status": "partial_failure",
  "failureType": "transient",
  "attemptedAction": {
    "tool": "search_academic_db",
    "query": "renewable energy policy",
    "dateRange": "2022-2024"
  },
  "partialResults": [
    {
      "title": "EU Renewable Energy Directive 2023",
      "source": "EUR-Lex",
      "retrieved": true
    }
  ],
  "alternativeApproaches": [
    "Retry with narrower date range (2023-2024)",
    "Search alternative database: government_publications",
    "Use cached results from previous research session"
  ]
}

This structure gives the coordinator everything it needs to decide: retry the same query, try an alternative, proceed with partial results, or escalate.

The Two Anti-Patterns

The exam tests these explicitly. Both are catastrophic in different ways:

Silent suppression: returning empty results marked as success. This is the worst anti-pattern. The subagent encounters a timeout but returns { "results": [], "status": "success" }. The coordinator believes the search ran and found nothing. It won't retry, won't try alternatives, and produces a synthesis that silently omits an entire research area. The final output looks complete. It is missing critical content.

Silent suppression is especially dangerous because it's invisible. The output looks correct - it just has gaps that nobody can detect. In a customer support context, it might mean the agent reports "no orders found" when the order lookup system was actually down, leading the agent to tell the customer they have no account.

Workflow termination: killing the entire pipeline on a single failure. One subagent times out and the entire research pipeline crashes. The other four subagents completed successfully, but their results are thrown away. This is a disproportionate response that wastes completed work and provides no recovery path.

The correct middle ground is structured error propagation: the failing subagent reports what happened, the coordinator assesses the damage, and the system continues with partial results or targeted recovery.

Access Failure vs Valid Empty Result

This distinction is critical and the exam tests it directly:

Access failure: The tool could not reach the data source. A timeout, a connection error, a permission denial. The search did not execute. Consider retry with the same or modified parameters.

Valid empty result: The tool reached the source and executed the query. It found no matches. This IS the answer. No retry is needed because the system worked correctly - there simply are no results for this query.

Conflating these leads to two problems:

# Access failure - consider retry
{
    "status": "error",
    "failureType": "transient",
    "message": "Connection timeout after 30s",
    "shouldRetry": True
}

# Valid empty result - no retry needed
{
    "status": "success",
    "results": [],
    "message": "Query executed successfully. No matching records found.",
    "shouldRetry": False
}

Coverage Annotations

When a synthesis agent combines findings from multiple subagents, the output should note which topic areas are well-supported and which have gaps. If one subagent failed to retrieve sources on geothermal energy, the synthesis should say:

"Section on geothermal energy is limited due to unavailable journal access during research."

This is far better than silently omitting the topic. Coverage annotations let the consumer know what the report covers fully and where there are known limitations. Without them, a gap in the synthesis looks like the topic was not relevant rather than the source being unavailable.

Local Recovery for Transient Failures

Subagents should implement local recovery for transient failures - retry logic, fallback sources, degraded responses - before propagating errors to the coordinator. Only propagate errors the subagent can't resolve locally. When propagating, always include what was attempted and any partial results gathered.

This reduces coordinator complexity. The coordinator doesn't need to manage retry logic for every possible transient failure across every subagent. Each subagent handles its own transient failures and escalates only the persistent ones.

Key Concept

Structured error context (failure type, attempted action, partial results, alternatives) enables intelligent coordinator recovery. The two anti-patterns are silent suppression (empty results as success) and workflow termination (killing the pipeline on one failure). Access failures need retry consideration; valid empty results do not.

Exam traps

Practice question

A web search subagent in a multi-agent research system times out while researching a complex topic. You need to design how this failure information flows back to the coordinator. Which approach best enables intelligent recovery?

  • A Return structured error context including failure type, attempted query, partial results, and potential alternative approaches Correct

    This gives the coordinator everything it needs to decide: retry with modified query, try an alternative approach, or proceed with partial results.

  • B Implement automatic retry with exponential backoff, returning a generic search unavailable status only after all retries are exhausted

    The generic status hides valuable context from the coordinator, preventing informed recovery decisions even after retries fail.

  • C Catch the timeout and return an empty result set marked as successful, so the rest of the workflow carries on regardless of the failure

    Silent suppression prevents any recovery. The coordinator believes the search succeeded and found nothing, so it will not attempt alternatives.

  • D Propagate the timeout exception to a top-level handler that terminates the entire research workflow at once, discarding everything

    Workflow termination wastes partial results from other subagents that may have completed successfully.

Build exercise: Build a Structured Error Propagation System

Advanced · 50 minutes

You'll practice:

  1. Define a structured error schema with fields: failureType (transient/validation/business/permission), attemptedAction (tool, query, parameters), partialResults (array of any retrieved data), and alternativeApproaches (suggested recovery strategies)

    Structured error context enables intelligent coordinator recovery. The four elements give the coordinator everything it needs to decide: retry, try an alternative, proceed with partial results, or escalate. Generic error messages like search unavailable prevent all informed recovery.

    You should see: A TypeScript interface or JSON schema with failureType as an enum of the four categories, attemptedAction as an object with tool/query/parameters, partialResults as an array, and alternativeApproaches as a string array. Each field should have a description explaining its purpose.

    Hints
    1. The four failure types map to different recovery strategies: transient = retry, validation = fix input, business = escalate, permission = authorisation change needed.
    2. Include the status field with values success, partial_failure, and error to distinguish complete failure from partial success with some results gathered.
    3. interface StructuredError {
        status: "error" | "partial_failure";
        failureType: "transient" | "validation" | "business" | "permission";
        attemptedAction: {
          tool: string;
          query: string;
          parameters: Record<string, unknown>;
        };
        partialResults: Array<{ title: string; source: string; retrieved: boolean }>;
        alternativeApproaches: string[];
        message: string;
      }
  2. Implement a subagent that distinguishes access failures (timeout, connection error) from valid empty results (successful query, no matches) in its error reporting

    Conflating access failures with valid empty results is a critical error the exam tests directly. Access failures mean the query did not execute and should be retried. Valid empty results mean the query succeeded and found nothing, which IS the answer. Treating them the same leads to either never retrying when you should or wasting time retrying queries that will always return nothing.

    You should see: A subagent function that catches exceptions (timeouts, connection errors) and reports them as access failures with shouldRetry: true, while successful queries returning no results are reported as success with an empty results array and shouldRetry: false.

    Hints
    1. Use try/catch to separate infrastructure errors (access failures) from empty results (query succeeded, nothing found). The catch block handles access failures; the successful empty response is a valid result.
    2. Include the shouldRetry flag in your response schema. Access failures should be true; valid empty results should be false.
    3. async function searchSubagent(query, dateRange) {
        try {
          const results = await searchDatabase(query, dateRange);
          if (results.length === 0) {
            // Valid empty result - query executed, no matches
            return {
              status: "success",
              results: [],
              message: "Query executed successfully. No matching records found.",
              shouldRetry: false
            };
          }
          return { status: "success", results, shouldRetry: false };
        } catch (error) {
          // Access failure - query did not execute
          return {
            status: "error",
            failureType: "transient",
            message: `Connection timeout after 30s: ${error.message}`,
            shouldRetry: true,
            attemptedAction: { tool: "search_database", query, parameters: { dateRange } },
            partialResults: [],
            alternativeApproaches: ["Retry with narrower date range", "Try alternative database"]
          };
        }
      }
  3. Build local retry logic for transient failures within the subagent (3 retries with exponential backoff) before propagating to the coordinator

    Subagents should handle their own transient failures locally before escalating. This reduces coordinator complexity as the coordinator does not need to manage retry logic for every possible transient failure across every subagent. Only persistent failures that survive local retry should propagate.

    You should see: A retry wrapper with exponential backoff (e.g., 1s, 2s, 4s) that attempts the operation up to 3 times before propagating the structured error to the coordinator. Partial results gathered before failure should be preserved across retries.

    Hints
    1. Use exponential backoff to avoid overwhelming a recovering service. The delay should double with each retry attempt.
    2. Accumulate partial results across retry attempts. If attempt 1 retrieves 2 of 5 sources and attempt 2 retrieves 1 more, the propagated error should include all 3 partial results.
    3. async function withRetry(fn, maxRetries = 3) {
        let lastError = null;
        let allPartialResults = [];
        
        for (let attempt = 0; attempt < maxRetries; attempt++) {
          try {
            const result = await fn();
            return result;
          } catch (error) {
            lastError = error;
            if (error.partialResults) {
              allPartialResults.push(...error.partialResults);
            }
            const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
            await new Promise(resolve => setTimeout(resolve, delay));
          }
        }
        
        // All retries exhausted - propagate structured error
        return {
          status: "error",
          failureType: "transient",
          message: `Failed after ${maxRetries} retries: ${lastError.message}`,
          partialResults: allPartialResults,
          alternativeApproaches: ["Try alternative data source", "Proceed with partial results"]
        };
      }
  4. Create a coordinator that receives structured errors and decides between retry with modified query, alternative approach, or proceed with partial results

    The coordinator is the intelligent recovery decision-maker. With structured error context, it can make informed choices rather than applying blanket policies. This is the correct middle ground between silent suppression (ignoring failures) and workflow termination (killing the pipeline on one failure).

    You should see: A coordinator function that examines the failure type, checks partial results, evaluates alternative approaches, and selects the appropriate recovery strategy. It should handle all four failure types differently and never silently suppress errors.

    Hints
    1. Map each failure type to a default recovery strategy: transient = retry with modification, validation = fix query parameters, business = escalate, permission = alert administrator.
    2. When partial results exist, the coordinator should assess whether they are sufficient to proceed without full data or whether retry is worth the additional latency.
    3. async function coordinatorRecovery(error) {
        switch (error.failureType) {
          case "transient":
            if (error.partialResults.length >= 3) {
              return { action: "proceed_partial", data: error.partialResults };
            }
            if (error.alternativeApproaches.length > 0) {
              return { action: "try_alternative", approach: error.alternativeApproaches[0] };
            }
            return { action: "retry_modified", modification: "narrower query" };
          case "validation":
            return { action: "fix_query", details: error.message };
          case "permission":
            return { action: "alert_admin", details: error.attemptedAction };
          case "business":
            return { action: "escalate_human", context: error };
        }
      }
  5. Add coverage annotations to synthesis output noting which findings are well-supported versus which topic areas have gaps due to unavailable sources

    Coverage annotations let the consumer know what the report covers fully and where there are known limitations. Without them, a gap looks like the topic was not relevant rather than the source being unavailable. This transparency is far better than silently omitting topics.

    You should see: A synthesis output that includes a coverage section listing each topic area with its data quality status: well-supported, limited (with reason), or unavailable (with reason). Failed subagent topics should be explicitly noted, not silently omitted.

    Hints
    1. Include coverage annotations as a dedicated section in the synthesis output, separate from the findings themselves.
    2. For each topic, note whether it is fully covered, partially covered (with explanation), or unavailable (with what failed and why). This turns invisible gaps into visible, documented limitations.
    3. function addCoverageAnnotations(synthesis, subagentResults) {
        const coverage = subagentResults.map(result => {
          if (result.status === "success" && result.results.length > 0) {
            return { topic: result.topic, status: "well-supported", sources: result.results.length };
          }
          if (result.status === "partial_failure") {
            return { topic: result.topic, status: "limited", reason: `Only ${result.partialResults.length} of expected sources available` };
          }
          return { topic: result.topic, status: "unavailable", reason: result.message };
        });
        
        return {
          ...synthesis,
          coverageAnnotations: coverage,
          caveat: coverage.filter(c => c.status !== "well-supported")
            .map(c => `Section on ${c.topic} is ${c.status}: ${c.reason}`)
            .join("\n")
        };
      }

Sources