← Back to Table of Contents

Chapter 4 β€” The Agent Loop (Heartbeat)

β€œThe agent loop is to AI agents what the event loop is to JavaScript β€” the engine that makes everything work.”

The Heartbeat

Every agent is driven by a loop β€” a repeated cycle of observing, thinking, and acting. This loop is the agent’s heartbeat. As long as it beats, the agent is alive and working. When it stops, the agent has either achieved its goal or given up.

The Agent Heartbeat
πŸ‘€ Observe
πŸ€” Think
⚑ Act
πŸ”„ Repeat
Phase What Happens Example
Observe Agent receives input β€” user message, tool result, environment state β€œSearch returned 5 articles about quantum computing”
Think LLM reasons about what to do next β€” possibly using chain-of-thought β€œI should read the top 3 articles and extract key facts”
Act Agent either calls a tool or returns a final answer Calls read_url(url) for each article
Repeat Tool results feed back as new observations Loop continues until goal is met

The ReAct Pattern

The most influential formalization of the agent loop is ReAct (Reasoning + Acting). Here’s how a single ReAct trace looks:

User: What's the population of the city that hosted the 2024 Olympics?

Thought: I need to find which city hosted the 2024 Olympics first.
Action: web_search("2024 Olympics host city")
Observation: The 2024 Summer Olympics were held in Paris, France.

Thought: Now I need to find the population of Paris.
Action: web_search("Paris France population 2024")
Observation: The population of Paris is approximately 2.1 million (city proper).

Thought: I have the answer. Paris hosted the 2024 Olympics and has ~2.1M people.
Answer: The 2024 Olympics were hosted by Paris, France, which has a population 
        of approximately 2.1 million (city proper) or 12.2 million (metro area).

Each Thought β†’ Action β†’ Observation cycle is one heartbeat.

Implementation: The Core Loop

Here’s a production-quality agent loop:

import json
from openai import OpenAI

client = OpenAI()

def agent_loop(user_message: str, tools: list, tool_registry: dict, max_iterations: int = 10):
    """The core agent loop β€” the heartbeat."""
    
    messages = [
        {"role": "system", "content": "You are a helpful research assistant."},
        {"role": "user", "content": user_message},
    ]
    
    for i in range(max_iterations):
        # THINK: Ask the LLM what to do
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
        )
        
        assistant_msg = response.choices[0].message
        messages.append(assistant_msg)
        
        # DECIDE: Does the agent want to act or is it done?
        if not assistant_msg.tool_calls:
            # No tool calls β€” agent is returning final answer
            return assistant_msg.content
        
        # ACT: Execute each tool call
        for tool_call in assistant_msg.tool_calls:
            fn_name = tool_call.function.name
            fn_args = json.loads(tool_call.function.arguments)
            
            # Execute the tool
            result = tool_registry[fn_name](**fn_args)
            
            # OBSERVE: Feed result back
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(result),
            })
    
    return "Max iterations reached β€” could not complete the task."

Loop Control: When to Stop

One of the hardest problems in agent design is knowing when to stop. An uncontrolled loop is expensive and potentially dangerous.

Loop Termination Conditions
βœ…
Happy Path: Goal Met
LLM returns a final answer without requesting tool calls β€” the natural exit
πŸ”’
Max Iterations
Hard limit on loop cycles (e.g., 10, 25, 50). Prevents runaway agents
πŸ’°
Budget Limit
Cap on token spend or API cost. The agent must finish within budget
⏱️
Timeout
Wall-clock time limit. Critical for user-facing applications
πŸ›‘
Error Threshold
Too many consecutive failures. Something is fundamentally wrong
πŸ‘€
Human Interrupt
User cancels or a checkpoint requires human approval

Synchronous vs. Asynchronous Loops

Loop Execution Models
Synchronous (Blocking)
  • User waits while agent runs
  • Simple to implement
  • Good for quick tasks (< 30 seconds)
  • Used in: chatbots, CLI tools
Asynchronous (Background)
  • Agent runs in background
  • User can check progress
  • Required for long tasks (minutes/hours)
  • Used in: Devin, coding agents, research agents

Streaming the Loop

Modern agents stream their heartbeat so users can watch the agent think in real time:

async def streaming_agent_loop(user_message: str):
    """Agent loop with real-time streaming."""
    messages = [{"role": "user", "content": user_message}]
    
    while True:
        # Stream the LLM's response
        stream = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            stream=True,
        )
        
        # Yield tokens as they arrive
        full_response = ""
        tool_calls = []
        
        for chunk in stream:
            delta = chunk.choices[0].delta
            if delta.content:
                yield {"type": "thinking", "content": delta.content}
                full_response += delta.content
            if delta.tool_calls:
                tool_calls.extend(delta.tool_calls)
        
        if not tool_calls:
            yield {"type": "answer", "content": full_response}
            break
        
        # Execute tools and show the user
        for tc in tool_calls:
            yield {"type": "tool_call", "name": tc.function.name}
            result = execute_tool(tc)
            yield {"type": "tool_result", "content": result}

The Loop in Different Frameworks

Every framework implements the agent loop differently, but the concept is identical:

Framework Loop Implementation
OpenAI Agents SDK Runner.run() β€” handles the loop internally, max_turns parameter
LangGraph State machine with edges β€” the loop is a graph cycle
CrewAI Task-based β€” agents loop on their assigned task until completion
AutoGen Message-passing β€” agents loop by sending messages to each other
Raw Python while True + break conditions β€” you control everything

Common Loop Anti-Patterns

Anti-Pattern Problem Fix
Infinite loop No max iterations, agent loops forever Always set max_iterations
No error handling Tool failure crashes the loop Catch exceptions, let LLM retry
Context explosion Messages grow until context window is full Summarize old messages, use sliding window
Tool call loops Agent calls the same tool with the same args repeatedly Detect repeated calls, force diversification
No observability Can’t debug what the agent did Log every Thought/Action/Observation

What’s Next

The agent loop drives the agent, but tools are what give it real-world power. Let’s deep-dive into how tools work, how function calling is implemented, and how to build your own.

Next: Chapter 5 β€” Tools & Function Calling β†’


← Previous: Chapter 3 β€” Anatomy of an Agent Β· Next: Chapter 5 β€” Tools & Function Calling β†’

Last updated: April 2026