# Agent Loop Implementation Pattern in Phase 14: How ReAct Works

> Learn the ReAct agent loop pattern from Phase 14 of AI Engineering From Scratch. Understand its thought action observation cycle and deterministic orchestration.

- Repository: [Rohit Ghumare/ai-engineering-from-scratch](https://github.com/rohitg00/ai-engineering-from-scratch)
- Tags: deep-dive
- Published: 2026-07-30

---

**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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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:

```python
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:

```python
@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:

```python
@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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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.