top of page

Tutorial 5: Handle Agent Failures

  • Contributor
  • 4 days ago
  • 3 min read

Agents fail in ways that simpler systems don't. They can loop, give up too early, call wrong tools, or get confused mid-task. This tutorial covers handling each.

What You'll Build

A production-safe agent with handling for the common failure modes.

Step 1: Identify the Failure Modes (10 min)

Common agent failures:

  • Loop: same tool called repeatedly with no progress

  • Stuck: agent reaches max steps without completing

  • Tool error: underlying tool failed

  • API error: LLM call failed

  • Confused: agent's response doesn't match intent

  • Overshooting: agent does too much, including unintended actions

Each needs different handling.

Step 2: Detect Loops (15 min)

def detect_loop(trace, lookback=3):
    if len(trace) < lookback + 1:
        return False
    
    recent = trace[-lookback:]
    return all(
        step["tool"] == recent[0]["tool"] and 
        step["input"] == recent[0]["input"]
        for step in recent
    )

# In the agent loop
if detect_loop(trace):
    # Insert a message that breaks the loop
    messages.append({
        "role": "user",
        "content": "You're repeating the same action. Try a different approach or give your best answer with what you have."
    })

Same input → same output → loop. Detect and break.

Step 3: Set Multiple Timeouts (10 min)

def agent_with_timeout(task, max_seconds=60, max_steps=10):
    start = time.time()
    
    for step in range(max_steps):
        # Check time
        if time.time() - start > max_seconds:
            return wrap_up(messages, "Time limit reached")
        
        # Check step
        if step == max_steps - 1:
            # Last step; force final answer
            messages.append({"role": "user", "content": "Give your best final answer."})
        
        # Check API call timeout
        response = client.messages.create(timeout=20, ...)
        # ...

Multiple boundaries prevent runaway.

Step 4: Handle Tool Failures (15 min)

def execute_tool_safely(tool_name, input):
    try:
        result = TOOL_FN[tool_name](**input)
        return {"success": True, "result": result}
    except KeyError:
        return {"success": False, "error": f"Unknown tool: {tool_name}"}
    except TypeError as e:
        return {"success": False, "error": f"Bad input: {e}"}
    except Exception as e:
        log_tool_error(tool_name, input, e)
        return {"success": False, "error": "Internal error; try a different approach"}

Return errors to the agent. It can recover.

Step 5: Handle LLM API Failures (15 min)

def llm_call_with_retry(messages, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.messages.create(messages=messages, ...)
        except anthropic.RateLimitError:
            wait = 2 ** attempt
            time.sleep(wait)
        except anthropic.APIError as e:
            if e.status_code >= 500:
                # Retry on server errors
                time.sleep(2 ** attempt)
            else:
                raise  # Don't retry client errors
    
    raise Exception("API call failed after retries")

Transient errors get retried; permanent errors fail clearly.

Step 6: Detect Confused Output (15 min)

After the agent completes:

def validate_output(task, output, trace):
    # Check if output addresses the task
    relevance_check = call_llm(f"""
        Task: {task}
        Agent output: {output}
        
        Does the output address the task? Respond with yes/no/partial.
    """)
    
    if "no" in relevance_check.lower():
        # Agent got confused; either retry or escalate
        return False, "Output didn't address the task"
    
    return True, None

Quality check before returning to user. Optional but useful for high-stakes.

Step 7: Build Fallback Paths (varies)

For each potential failure, what's the fallback?

def task_with_fallback(task):
    try:
        result = agent(task)
        if not validate_output(task, result.output):
            return fallback_response(task)
        return result.output
    except Exception as e:
        log_error(e)
        return fallback_response(task)

def fallback_response(task):
    return "I'm having trouble with that. Could you rephrase, or I can connect you with support?"

Graceful degradation. User experience preserved.

Step 8: Save Traces of Failures (15 min)

def save_failure(task, trace, error):
    db.insert("agent_failures", {
        "task": task,
        "trace": json.dumps(trace),
        "error": str(error),
        "timestamp": datetime.now(),
    })

The failures are how you improve.

Step 9: Periodic Review (weekly)

SELECT error, COUNT(*) as count
FROM agent_failures
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY error
ORDER BY count DESC;

Top failure modes are where to invest.

Step 10: Test Failure Handling (varies)

Intentionally cause failures:

  • Make a tool throw

  • Make the LLM API rate-limited

  • Submit a task that would loop

  • Submit a task the agent can't handle

For each, the system should fail gracefully — clear error to user; logged for the team; no infinite loop or crash.

What You Just Did

You built failure handling into your agent. Loops break; tools recover; users get sensible responses even when things go wrong.

Common Failure Modes

Optimistic loops. No max_steps; no time budget. Agent runs forever.

Silent tool failures. Tool throws; agent treats as if success.

No retries. Transient API error fails the whole task.

No fallback. Agent fails → user sees error.

No failure tracking. Can't improve what you don't measure.

bottom of page