0Pricing
AI Engineering Academy · Lesson

Classifying Agent Failure Modes

Build a taxonomy of agent failures: tool errors, malformed outputs, reasoning loops, context exhaustion, and external service unavailability, and design recovery strategies for each.

Classifying Agent Failure Modes is a free AI Engineering Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Agents Fail in Distinctive Ways

Agents fail differently from simple LLM calls. A single-turn call either returns an answer or throws an error. An agent executing a multi-step task can fail at any point — and the failure might not be obvious from the final output. Understanding the taxonomy of agent failure modes is the first step toward building agents that detect, diagnose, and recover from their own failures.

Failure Mode 1: Tool Errors

Tool errors occur when the agent calls a tool with invalid arguments, the tool raises an exception, or the tool returns an empty or malformed result. Examples include: calling a search API with a malformed query, querying a database with invalid SQL, or invoking a code executor that times out. Tool errors are the easiest to detect because they produce explicit exception signals that can be caught and handled.

class ToolError(Exception):
    def __init__(self, tool_name: str, args: dict, error: Exception):
        self.tool_name = tool_name
        self.args = args
        self.original_error = error
        super().__init__(f'Tool {tool_name} failed: {error}')

def safe_tool_call(tool_func, args: dict) -> str:
    try:
        result = tool_func(**args)
        if not result:
            return 'Tool returned empty result. Try a different approach.'
        return str(result)
    except Exception as e:
        raise ToolError(tool_func.__name__, args, e)

Failure Mode 2: Malformed Outputs

Malformed outputs happen when the agent generates text that does not match the expected format — for example, returning natural language when the next step expects JSON, or calling a tool with arguments in the wrong structure. This often happens when the agent confuses its current step with a previous one. Validate the format of every agent output before using it and re-prompt when the format is wrong.

import json

def validate_agent_output(raw_output: str, expected_format: str) -> dict:
    if expected_format == 'json':
        try:
            return json.loads(raw_output)
        except json.JSONDecodeError as e:
            return {
                'valid': False,
                'error': f'Expected JSON but got invalid JSON: {e}',
                'raw': raw_output[:200]
            }
    return {'valid': True, 'data': raw_output}

Failure Mode 3: Reasoning Loops

Reasoning loops occur when an agent repeats the same action or thought indefinitely without making progress. The agent might call the same search query 10 times in a row, getting the same empty result and not knowing what to try next. Detect loops by tracking recent actions and checking for repeats. When a loop is detected, inject a meta-prompt telling the agent to try a different approach.

from collections import Counter

class LoopDetector:
    def __init__(self, window: int = 5, threshold: int = 3):
        self.recent_actions = []
        self.window = window
        self.threshold = threshold

    def record(self, action: str) -> bool:
        self.recent_actions.append(action)
        if len(self.recent_actions) > self.window:
            self.recent_actions.pop(0)
        counts = Counter(self.recent_actions)
        most_common_count = counts.most_common(1)[0][1] if counts else 0
        return most_common_count >= self.threshold  # True = loop detected

Failure Mode 4: Context Exhaustion

Context exhaustion happens when the agent's accumulated history (tool calls, observations, thoughts) exceeds the model's context window. The model either truncates the history silently (losing critical information) or raises a token limit error. Prevent this by tracking token usage across steps and compressing the history (summarizing old steps) before it reaches the limit.

import tiktoken

CONTEXT_LIMIT = 100_000  # tokens
COMPRESS_AT = 80_000     # trigger compression with headroom

enc = tiktoken.encoding_for_model('gpt-4o')

def total_tokens(messages: list) -> int:
    return sum(len(enc.encode(str(m))) for m in messages)

def check_context(messages: list) -> str:
    tokens = total_tokens(messages)
    if tokens > COMPRESS_AT:
        return 'compress'
    if tokens > CONTEXT_LIMIT:
        return 'critical'
    return 'ok'

Failure Mode 5: External Service Unavailability

External service failures occur when a tool's underlying service is down, rate-limited, or returns unexpected errors. An agent that cannot reach the database it needs to query is stuck. Unlike reasoning loops (agent's fault), external failures are environment failures. Handle them with retry + exponential backoff, and have fallback tools that can approximate the result using different data sources.

import asyncio

async def resilient_tool_call(tool_func, args: dict, max_retries: int = 3) -> str:
    for attempt in range(max_retries):
        try:
            return await tool_func(**args)
        except (ConnectionError, TimeoutError) as e:
            if attempt == max_retries - 1:
                return f'Service unavailable after {max_retries} attempts. Error: {e}'
            wait = 2 ** attempt  # 1s, 2s, 4s
            await asyncio.sleep(wait)
    return 'Unexpected error in resilient_tool_call'

Failure Mode 6: Goal Misunderstanding

Goal misunderstanding is when the agent misinterprets the task and pursues a subtly different objective. This is the hardest failure to detect because the agent may complete successfully — just not the task the user intended. Mitigate it by asking the agent to restate the goal in its own words at the start, and by implementing a final verification step that checks whether the result actually answers the original question.

async def confirm_goal_understanding(original_task: str) -> str:
    resp = await client.chat.completions.create(
        model='gpt-4o',
        messages=[
            {'role': 'system', 'content': 'Restate the task in your own words. Be specific about what the final deliverable should be.'},
            {'role': 'user', 'content': f'Task: {original_task}'}
        ]
    )
    return resp.choices[0].message.content

# Use the restatement as the first step of the agent
# to catch misunderstandings before any tools are called

Building a Failure Classification System

Create a structured failure classifier that labels every agent exception with its type. This enables automatic routing to the appropriate recovery strategy. Store failure logs with type labels so you can analyze which failure modes are most common and prioritize which to address first. Tool errors and loops are typically most frequent and most fixable.

from enum import Enum
from dataclasses import dataclass

class FailureType(Enum):
    TOOL_ERROR = 'tool_error'
    MALFORMED_OUTPUT = 'malformed_output'
    REASONING_LOOP = 'reasoning_loop'
    CONTEXT_EXHAUSTION = 'context_exhaustion'
    EXTERNAL_SERVICE = 'external_service'
    GOAL_MISUNDERSTANDING = 'goal_misunderstanding'
    MAX_ITERATIONS = 'max_iterations'
    UNKNOWN = 'unknown'

@dataclass
class AgentFailure:
    failure_type: FailureType
    step: int
    tool_name: str | None
    error_message: str
    recoverable: bool

Mapping Failures to Recovery Actions

Each failure type has an appropriate recovery action. Tool errors warrant a retry with adjusted arguments. Loops warrant a diversity prompt telling the agent to try something new. Context exhaustion warrants compression. External service failures warrant fallback tools. Goal misunderstanding warrants a clarification request. Map these explicitly in a recovery router that the agent runtime invokes when failures occur.

RECOVERY_ACTIONS = {
    FailureType.TOOL_ERROR:           'retry_with_corrected_args',
    FailureType.MALFORMED_OUTPUT:     'reformat_output',
    FailureType.REASONING_LOOP:       'inject_diversity_prompt',
    FailureType.CONTEXT_EXHAUSTION:   'compress_history',
    FailureType.EXTERNAL_SERVICE:     'use_fallback_tool',
    FailureType.GOAL_MISUNDERSTANDING:'request_clarification',
    FailureType.MAX_ITERATIONS:       'escalate_to_human',
    FailureType.UNKNOWN:              'escalate_to_human'
}

Setting Maximum Iteration Limits

Every agent must have a maximum iteration limit as a hard safety boundary. Without it, a looping agent runs indefinitely, consuming tokens and money. Set the limit based on expected task complexity: a simple question-answering agent might cap at 5 steps, while a complex research agent might allow 20. When the limit is reached, log the failure, save the partial results, and escalate to a human or return a partial answer.

MAX_ITERATIONS = 15

async def run_agent(task: str) -> str:
    messages = [{'role': 'user', 'content': task}]
    loop_detector = LoopDetector()
    for iteration in range(MAX_ITERATIONS):
        response = await get_agent_action(messages)
        if response.is_final:
            return response.answer
        action_key = f'{response.tool}:{response.args}'
        if loop_detector.record(action_key):
            messages.append({'role': 'system', 'content': 'You are repeating yourself. Try a completely different approach.'})
            continue
        result = await execute_tool(response.tool, response.args)
        messages.append({'role': 'tool', 'content': result})
    return 'Task exceeded maximum iterations. Partial results: ' + get_partial_result(messages)

Logging Failures for Post-Mortem Analysis

Log every agent failure with enough context to diagnose it after the fact: the full task description, the complete action history up to the failure point, the failure type and error message, the iteration count, and the token usage. Store this in a failures table with an index on failure_type and task_id. Regularly review failure logs to identify which task types are most prone to specific failure modes and prioritize fixes accordingly.

import json
from dataclasses import asdict

async def log_agent_failure(task_id: str, failure: AgentFailure, history: list, pool):
    async with pool.acquire() as conn:
        await conn.execute('''
            INSERT INTO agent_failures
            (task_id, failure_type, step, tool_name, error_message,
             recoverable, action_history, failed_at)
            VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
        ''',
            task_id,
            failure.failure_type.value,
            failure.step,
            failure.tool_name,
            failure.error_message,
            failure.recoverable,
            json.dumps(history)
        )

Quick Check

Test your understanding of agent failure mode classification.

Lesson Recap

In this lesson you learned: six major agent failure modes include tool errors, malformed outputs, reasoning loops, context exhaustion, external service failures, and goal misunderstanding, loop detection via action history catches repetitive patterns before they exhaust the iteration budget, and mapping failure types to recovery actions enables automated self-healing. Next up we implement self-correction and reflective prompting.

Frequently asked questions

Is the “Classifying Agent Failure Modes” lesson free?

Yes — the full text of “Classifying Agent Failure Modes” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.

What will I learn in “Classifying Agent Failure Modes”?

Build a taxonomy of agent failures: tool errors, malformed outputs, reasoning loops, context exhaustion, and external service unavailability, and design recovery strategies for each. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start AI Engineering Academy?

No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Classifying Agent Failure Modes” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this AI Engineering Academy lesson?

Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Classifying Agent Failure Modes
  2. Self-Correction and Reflective Prompting
  3. Checkpointing and Task Resumption
  4. Human-in-the-Loop Escalation
← Back to AI Engineering Academy