8.2 Agent Loop Implementation & State Tracking

Key Takeaways

  • The classic agent loop executes an alternating five-phase state machine: sending context to Claude, detecting stop_reason: 'tool_use', executing tools client-side, appending both tool_use and tool_result blocks to history, and recurring until Claude yields stop_reason: 'end_turn'.
  • Robust production agents enforce multi-dimensional safety boundaries: hard turn ceilings (max_turns), per-turn and global wall-clock timeouts, and cumulative token budget caps to prevent infinite cycles and billing exhaustion.
  • Tool failures must never crash the client application; instead, runtime exceptions must be captured and returned to Claude in a tool_result block with is_error: true, providing the model with actionable diagnostic data to enable autonomous self-correction.
  • Context window accumulation across iterative turns introduces quadratic token growth; production loops must implement active context compaction, such as truncating voluminous tool outputs and pruning historical tool payloads while strictly maintaining message schema integrity.
  • State tracking requires maintaining a deterministic execution record that records thread IDs, turn indices, cumulative token consumption, and intermediate tool invocation states to support auditing, observability, and reproducible debugging.
Last updated: September 2026

Agent Loop Implementation & State Tracking

Exam Blueprint Focus: Building autonomous agents with Anthropic's Claude requires a deep understanding of the client-driven agent loop. Unlike black-box agent frameworks that obscure underlying API calls, production engineers must understand the exact mechanics of tool use request-response cycles, how Claude signals execution requests via stop_reason: "tool_use", how to capture and return tool outputs or runtime errors via tool_result blocks, and how to enforce defensive recursion guardrails and context management strategies.


The Anatomy of the Classic Agent Loop: The Five-Phase Cycle

At its core, an autonomous agent powered by Claude is an iterative client-driven state machine. Claude does not execute tools directly on its own servers; instead, Claude acts as a reasoning engine that emits structured tool invocation intents, leaving tool execution, security sandboxing, and state management entirely to the client application.

The complete agent loop follows a deterministic Five-Phase Lifecycle:

  1. Phase 1: Request Dispatch (Client -> Claude) The client application sends an HTTP POST request to /v1/messages containing the conversation history (messages array), system prompt, and available tool declarations (tools array with JSON Schema parameters).

  2. Phase 2: Intent Evaluation & Stop Reason Inspection (Claude -> Client) Claude processes the context. If Claude determines that external tools are required to answer the query or advance the objective, it returns an HTTP 200 response with:

    • stop_reason: "tool_use"
    • A content block array containing one or more blocks of type "tool_use", each possessing a unique id, a tool name, and an input dictionary containing validated arguments. If no tools are required, Claude responds with stop_reason: "end_turn" and normal text content, indicating loop termination.
  3. Phase 3: Tool Interception & Execution (Client-Side) The client intercepts the response, inspects the stop_reason, and extracts the tool calls. The client validates the input arguments, resolves the tool implementation in its local registry (e.g., executing a database query, fetching a URL, or running a calculation), and captures the execution result or runtime error.

  4. Phase 4: History Appending & Schema Enforcement (Client-Side) The client updates the conversation history by appending two mandatory message turns:

    • The Assistant Turn: The complete message returned by Claude in Phase 2, containing the tool_use content block(s).
    • The User Turn: A user message containing corresponding tool_result content blocks. Each tool_result must explicitly link back to its triggering invocation via tool_use_id: block.id.
  5. Phase 5: Re-invocation & Iteration (Client -> Claude) The client re-invokes /v1/messages with the updated history. Claude reads the tool outputs, evaluates whether additional steps are needed, and either emits another tool_use block (looping back to Phase 2) or provides a final synthesized answer with stop_reason: "end_turn".


Recursion Limits, Timeouts, and Defensive Safety Boundaries

In an unconstrained agent loop, an edge case—such as an ambiguous user prompt, an oscillating tool bug, or an unreachable file—can cause the model to enter an infinite loop. This risks runaway inference billing, resource exhaustion, and degraded user experiences. Production agent loops must enforce four non-negotiable defensive guardrails:

1. Hard Maximum Turns Ceiling (max_turns)

Always define a hard iteration limit (typically 10 to 25 turns depending on task complexity). If the turn counter exceeds max_turns without Claude emitting end_turn, the client must break the loop, log a warning, and prompt Claude for a best-effort summary of progress achieved so far.

2. Wall-Clock Execution Timeouts

Implement dual-layer timeouts:

  • Per-Turn Timeout: Enforce an HTTP read timeout (e.g., 30 to 60 seconds) on each individual API call and tool execution.
  • Global Session Timeout: Enforce an overall wall-clock deadline (e.g., 300 seconds) for the entire agent task. If the deadline expires, the agent aborts execution gracefully.

3. Cumulative Token Budget Caps

Track total accumulated input and output tokens across every iteration using Claude's response.usage metadata. If cumulative_input_tokens + cumulative_output_tokens > TOKEN_BUDGET (e.g., 100,000 tokens), terminate the loop to prevent denial-of-wallet scenarios.

4. Duplicate Tool Call Detection (Semantic Loop Breaker)

Agents occasionally get stuck in repetitive cycles—calling the exact same tool with identical input arguments 3 or 4 times consecutively (for example, repeatedly calling read_file({"path": "config.json"})). A production loop computes a deterministic hash of (tool_name, json.dumps(tool_input)). If the exact same call is observed more than twice consecutively, inject a synthetic tool_result warning Claude that the tool output has not changed and asking it to change its strategy.


Graceful Error Handling & Self-Correction via is_error: true

A common beginner anti-pattern is allowing client-side exceptions (such as FileNotFoundError, HTTP 500, or database timeout) to crash the agent script. Crashing destroys the conversational context and ruins agent autonomy.

Anthropic's Messages API provides a native primitive for error handling: the is_error: true flag within a tool_result content block.

How is_error: true Facilitates Self-Correction

When a tool fails, the client catches the exception and returns the error message in the tool_result payload with is_error: true:

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90tc1q09qlkj",
      "is_error": true,
      "content": "psycopg2.errors.UndefinedColumn: column 'created_at' does not exist in table 'users'. Available columns: [id, username, email, registration_date, status]"
    }
  ]
}

When Claude receives this payload:

  1. It parses the exception message and understands the technical failure.
  2. It inspects the diagnostic information provided (the list of available columns).
  3. In the subsequent turn, Claude self-corrects: instead of repeating the broken SQL query, it generates an updated query using registration_date instead of created_at.

Context Accumulation & Token Bloat Mitigation

Because the agent loop appends both assistant tool calls and user tool results to the conversation history on every single turn, context size grows quadratically ($O(N^2)$ token consumption across $N$ turns). A single 15-turn agent run where each tool returns a 4,000-token file dump can easily consume over 60,000 input tokens per call by turn 15.

Mitigation Strategies

  1. Result Windowing & Truncation: Never return raw megabyte payloads into tool_result. Tools should enforce client-side truncation, returning only the first $K$ lines or characters (e.g., max 100 lines or 4,000 characters) along with pagination offsets.
  2. Historical Payload Compaction: In long-running trajectories, older tool outputs from turns $t-3$ or earlier are rarely needed in verbatim detail. The client can replace the bulky content of historical tool_result blocks with compact placeholders:
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90tc1q09qlkj",
      "content": "[Database query returned 85 rows; schema verified; raw output compacted to conserve context]"
    }
    
    Critical Rule: You must preserve the outer tool_result block structure and the exact tool_use_id! Deleting the block or altering the turn sequence causes an API schema validation error.
  3. Prompt Caching on Static System & Tool Definitions: Always attach cache_control: {"type": "ephemeral"} to the system prompt and tool definitions. This ensures that across all 15 turns of the agent loop, the static tool schemas and system instructions are read from the KV cache at a 90% discount.

Complete Production-Ready Agent Loop Implementation

The following Python implementation demonstrates a production-grade agent loop featuring hard turn caps, cumulative token budget tracking, tool dispatching, is_error propagation, and clean loop termination:

import json
import time
from typing import Any, Callable, Dict, List
import anthropic

# Initialize Anthropic Client
client = anthropic.Anthropic()

# Define Tool Implementations
def execute_calculator(expression: str) -> str:
    """Safely evaluate basic mathematical expressions."""
    allowed_chars = set('0123456789+-*/(). ')
    if not all(c in allowed_chars for c in expression):
        raise ValueError(f"Invalid character in expression: {expression}")
    return str(eval(expression, {'__builtins__': None}, {}))

def execute_file_read(filepath: str) -> str:
    """Read file contents safely with windowed output."""
    if not (filepath.endswith('.txt') or filepath.endswith('.py') or filepath.endswith('.json')):
        raise PermissionError(f"Access denied: Restricted extension on '{filepath}'")
    with open(filepath, 'r', encoding='utf-8') as f:
        return f.read(2000)  # Constrain output to 2000 chars

TOOL_REGISTRY: Dict[str, Callable[..., str]] = {
    'calculator': lambda args: execute_calculator(args['expression']),
    'read_file': lambda args: execute_file_read(args['filepath']),
}

TOOL_DEFINITIONS = [
    {
        'name': 'calculator',
        'description': 'Perform arithmetic calculations on mathematical expressions.',
        'input_schema': {
            'type': 'object',
            'properties': {
                'expression': {'type': 'string', 'description': 'Math expression, e.g. (12 * 45) + 18'}
            },
            'required': ['expression']
        },
        'cache_control': {'type': 'ephemeral'}
    },
    {
        'name': 'read_file',
        'description': 'Read the first 2,000 characters of a text file from disk.',
        'input_schema': {
            'type': 'object',
            'properties': {
                'filepath': {'type': 'string', 'description': 'Path to text file on disk'}
            },
            'required': ['filepath']
        }
    }
]

def run_production_agent_loop(
    user_goal: str,
    max_turns: int = 10,
    token_budget: int = 50000
) -> str:
    """
    Executes a production-grade agent loop with defensive safety guardrails.
    """
    system_prompt = 'You are an autonomous engineering assistant. Use available tools to solve the goal.'
    messages: List[Dict[str, Any]] = [{'role': 'user', 'content': user_goal}]
    
    total_input_tokens = 0
    total_output_tokens = 0
    turn = 0
    
    while turn < max_turns:
        turn += 1
        print(f'--- Starting Agent Turn {turn}/{max_turns} ---')
        
        # Phase 1: Call Messages API
        response = client.messages.create(
            model='claude-sonnet-5',
            max_tokens=2048,
            system=system_prompt,
            messages=messages,
            tools=TOOL_DEFINITIONS
        )
        
        # Track Token Consumption
        total_input_tokens += response.usage.input_tokens
        total_output_tokens += response.usage.output_tokens
        
        if (total_input_tokens + total_output_tokens) > token_budget:
            raise RuntimeError(f'Agent exceeded cumulative token budget ({token_budget} tokens).')
        
        # Append Assistant Response to Conversation History
        messages.append({'role': 'assistant', 'content': response.content})
        
        # Phase 2: Inspect Stop Reason
        if response.stop_reason == 'end_turn':
            print('Agent completed task successfully.')
            text_blocks = [b.text for b in response.content if hasattr(b, 'text')]
            return '\n'.join(text_blocks)
        
        if response.stop_reason != 'tool_use':
            raise RuntimeError(f'Unexpected stop reason encountered: {response.stop_reason}')
        
        # Phase 3 & 4: Execute Tools and Build User Tool Result Turn
        tool_results: List[Dict[str, Any]] = []
        for block in response.content:
            if block.type == 'tool_use':
                tool_id = block.id
                tool_name = block.name
                tool_args = block.input
                
                print(f'Executing Tool {tool_name} (ID: {tool_id})...')
                
                if tool_name not in TOOL_REGISTRY:
                    tool_results.append({
                        'type': 'tool_result',
                        'tool_use_id': tool_id,
                        'is_error': True,
                        'content': f'Error: Tool {tool_name} is not recognized.'
                    })
                    continue
                
                # Execute tool with resilient error catching
                try:
                    output_str = TOOL_REGISTRY[tool_name](tool_args)
                    tool_results.append({
                        'type': 'tool_result',
                        'tool_use_id': tool_id,
                        'content': output_str
                    })
                except Exception as exc:
                    # Report runtime error back to model for self-correction
                    tool_results.append({
                        'type': 'tool_result',
                        'tool_use_id': tool_id,
                        'is_error': True,
                        'content': f'{type(exc).__name__}: {str(exc)}'
                    })
        
        # Append User Turn containing all tool results
        messages.append({'role': 'user', 'content': tool_results})
    
    raise TimeoutError(f'Agent failed to converge within hard limit of {max_turns} turns.')
Loading diagram...
Classic Agent Loop State Machine & Error Handling Lifecycle
Test Your Knowledge

During turn 4 of an autonomous agent loop, Claude attempts to invoke a custom database tool with query arguments. The database driver raises a 'TableNotFoundException: relation customer_orders does not exist'. What is the architecturally correct way for a production agent client to handle this runtime failure?

A
B
C
D
Test Your Knowledge

When constructing the conversation messages payload to continue an agent loop after executing a tool requested by Claude, which message sequence is strictly required by the Anthropic Messages API schema?

A
B
C
D
Test Your Knowledge

An autonomous agent performs a 20-turn research and coding task. By turn 18, the conversation context exceeds 90,000 tokens due to large historical file contents returned by earlier tool calls, causing significant latency and high inference costs. What is the most effective context management strategy to mitigate this issue while maintaining API schema validity?

A
B
C
D