Agent Loop Implementation Pattern in Phase 14: How ReAct Works

Phase 14 of the rohitg00/ai-engineering-from-scratch curriculum implements a ReAct (REason-and-Act) agent loop that cycles through thought, action, and observation steps while enforcing turn budgets and managing tool dispatch through a deterministic orchestration pattern.

The agent loop implementation pattern demonstrated in this phase provides a minimal, framework-free foundation for building autonomous AI agents. Located in the phases/14-agent-engineering/01-the-agent-loop/code/ directory, this reference implementation uses pure Python standard library components to illustrate the canonical ReAct control flow that underpins modern LLM-based systems.

The ReAct Pattern: Five Core Ingredients

According to the header comments in phases/14-agent-engineering/01-the-agent-loop/code/main.py (lines 1-8), the ReAct loop consists of five essential ingredients that work together to create a reasoning and acting cycle:

  1. Message buffer – Maintains conversation history across turns.
  2. Tool registration – Maps tool names to callable functions via a registry.
  3. Stop condition – Evaluates termination criteria such as a finish turn.
  4. Turn budget – Enforces a maximum iteration limit via max_turns.
  5. Observation formatting – Structures tool outputs before feeding them back to the LLM.

Inside main.py: Key Implementation Details

ToolRegistry and Tool Dispatch

The ToolRegistry class (lines 34-52) maintains an internal _tools dictionary mapping tool names to callable functions. It exposes a register method for adding tools and a dispatch method for executing them:

class ToolRegistry:
    def __init__(self) -> None:
        self._tools: dict[str, Callable[..., str]] = {}
    
    def register(self, name: str, fn: Callable[..., str]) -> None:
        self._tools[name] = fn
    
    def dispatch(self, call: ToolCall) -> str:
        fn = self._tools.get(call.name)
        if fn is None:
            return f"error: unknown tool {call.name!r}"
        try:
            return fn(**call.args)
        except Exception as e:
            return f"error: {type(e).__name__}: {e}"

Turn State Management

The Turn dataclass tracks the state of each iteration, capturing whether the turn comes from the assistant or a tool, along with any associated tool calls and observations:

@dataclass
class Turn:
    kind: str               # "assistant" or "tool"

    content: str
    tool_call: ToolCall | None = None
    observation: str | None = None

The AgentLoop Orchestrator

The AgentLoop dataclass (lines 97-108) glues the LLM interface and tool registry together. It drives the execution loop until either a finish turn occurs or the max_turns budget is exhausted:

@dataclass
class AgentLoop:
    llm: ToyLLM
    tools: ToolRegistry
    max_turns: int = 5

    def run(self) -> None:
        history: list[Turn] = []
        for _ in range(self.max_turns):
            resp = self.llm.respond(history)
            turn = Turn(kind=resp["kind"], content=resp["content"])
            history.append(turn)

            if turn.kind == "finish":
                break
            if turn.kind == "action":
                obs = self.tools.dispatch(turn.tool_call)
                turn.observation = obs

Execution Flow: How the Loop Runs

The AgentLoop.run() method implements the core ReAct cycle:

  • Step 1: Initialize an empty history list to serve as the message buffer.
  • Step 2: Query the LLM via respond(history) to receive a thought or action.
  • Step 3: Append the response to history as a Turn.
  • Step 4: Check for the stop condition (turn.kind == "finish"). If met, exit immediately.
  • Step 5: For action turns, dispatch the tool using ToolRegistry.dispatch() and capture the observation.
  • Step 6: Repeat until the turn budget (max_turns) is reached.

The accompanying lesson in phases/14-agent-engineering/01-the-agent-loop/docs/en.md details this flow, while phases/14-agent-engineering/01-the-agent-loop/quiz.json contains tests verifying the loop's correctness and budget handling.

Swapping ToyLLM for Production Models

While the reference implementation uses ToyLLM—a deterministic class that emits scripted thought, action, and finish responses from a pre-defined script—the surrounding AgentLoop requires no changes to work with production providers. To adapt this agent loop implementation pattern for OpenAI, Anthropic, or other APIs:

  • Replace ToyLLM with a client class implementing respond(history).
  • Ensure the return value contains kind (one of "thought", "action", or "finish") and content.
  • Preserve the ToolRegistry interface to maintain compatibility with the existing dispatch logic.

Summary

  • Phase 14 implements a ReAct agent loop using only Python standard library components.
  • The ToolRegistry class manages tool registration and execution via the dispatch method.
  • Turn budget enforcement prevents infinite loops through the max_turns parameter.
  • The AgentLoop dataclass orchestrates the cycle until a finish condition or budget exhaustion.
  • The pattern is provider-agnostic; you can substitute ToyLLM with any real LLM client maintaining the same interface.

Frequently Asked Questions

What is the ReAct pattern in AI agents?

The ReAct (REason-and-Act) pattern is an agent architecture that interleaves reasoning steps (thoughts) with action steps (tool calls) and observation steps (results). This creates a structured loop where the LLM explicitly thinks about what to do, executes an action, observes the result, and repeats until the task is complete.

How does the turn budget prevent infinite loops?

The AgentLoop enforces a hard limit through the max_turns parameter (defaulting to 5), which caps the number of iterations in the run() method's for loop. Once this budget is exhausted, the loop terminates regardless of whether a finish signal was received, preventing runaway execution during unexpected agent behavior.

Can this pattern integrate with OpenAI or Anthropic APIs?

Yes. The pattern is designed to be provider-agnostic. You can replace the ToyLLM class with any LLM client that implements a respond(history) method returning a dictionary with kind and content keys. The AgentLoop and ToolRegistry components remain unchanged, allowing seamless integration with OpenAI, Anthropic, or local model APIs.

What role does the ToolRegistry play?

The ToolRegistry serves as the bridge between the agent's reasoning and external capabilities. It maintains a mapping of tool names to Python functions, validates tool existence during dispatch, handles argument unpacking, and captures error states as observations. This encapsulation ensures that the AgentLoop remains agnostic to the specific implementations of individual tools.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →