# How to Implement an Agent Loop From Scratch Without Frameworks

> Build an agent loop from scratch using five Python components: message buffer, tool registry, stop condition, turn budget, and observation formatter. Learn step-by-step implementation without frameworks.

- Repository: [Rohit Ghumare/ai-engineering-from-scratch](https://github.com/rohitg00/ai-engineering-from-scratch)
- Tags: how-to-guide
- Published: 2026-06-16

---

**The ReAct agent loop is built using five core components—a message buffer, tool registry, stop condition, turn budget, and observation formatter—implemented in pure Python within a single control loop 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).**

The `rohitg00/ai-engineering-from-scratch` curriculum demonstrates how to construct a fully functional ReAct (Reason + Act) agent loop using only the Python standard library. This framework-free implementation in Phase 14 proves that modern agent SDKs are simply wrappers around this fundamental pattern, making it ideal for understanding the underlying mechanics or building custom agent runtimes.

## The Five Core Ingredients of a Framework-Free Agent Loop

Building an agent loop from scratch requires five deterministic components that work together to create a testable, terminating system.

### Message Buffer for Conversation History

The **message buffer** maintains a chronological list of all turns—including user inputs, model thoughts, actions, observations, and final answers. This history allows the LLM to see its own reasoning trace across multiple steps.

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), the `AgentLoop` class stores this transcript in `self.history` (lines 100‑103). Each entry is a `Turn` object that preserves the complete context needed for the next iteration.

### Tool Registry for Action Dispatch

The **tool registry** maps string tool names to actual Python callables, enabling the loop to execute actions without external SDK dependencies.

The `ToolRegistry` class (lines 34‑44) maintains this mapping. Tools are registered using `register(name, callable)`, and the registry validates that requested tools exist before dispatching. This design keeps the agent loop agnostic to the specific tools available, whether they are calculators, API clients, or database queries.

### Stop Condition and Turn Budget

A robust agent loop requires **termination guarantees**. The implementation checks for three stop conditions:
- The LLM emits a `"finish"` turn type
- The **turn budget** (default 12 iterations) is exhausted
- A guardrail detects an invalid state

The `AgentLoop.run` method validates `reply["kind"] == "finish"` (lines 107‑110) and enforces `max_turns` through a hard iteration limit (line 101). This prevents infinite reasoning cycles and runaway API costs.

### Observation Formatter

The **observation formatter** converts raw Python return values into structured strings that the LLM can process. When `ToolRegistry.dispatch` executes a tool (lines 45‑53), it returns a string representation of the result. This string is then wrapped in a `Turn` object (lines 115‑118) and appended to the history, maintaining a consistent text domain for the model.

## Step-by-Step Control Flow Implementation

The control flow is implemented as a simple `for` loop over `range(self.max_turns)` that executes the ReAct pattern:

1. **Append user message** to the history buffer
2. **Call the LLM** using `ToyLLM.respond` with the current history as context
3. **Parse the reply** into a `thought` and an `action` (or `finish`)
4. **Dispatch the action** by looking up the tool in the registry, running it, and creating an observation `Turn`
5. **Repeat** until the stop condition is met or the budget depletes

All logic resides in the `AgentLoop.run` method, demonstrating that sophisticated agent behavior emerges from simple sequential execution rather than complex framework abstractions.

## Complete Code Reference

The entire implementation lives in a single file with no external dependencies beyond the standard library.

Run the demonstration agent from the lesson root:

```bash
python3 phases/14-agent-engineering/01-the-agent-loop/code/main.py

```

The script produces a detailed trace showing the ReAct loop in action:

```

[00   user] What is 120 plus 15% tax, stored in kv?
[01 thought] store the base price
[02  action] kv_set({'key': 'base', 'value': '120'}) -> stored base
[03 thought] compute 15% tax
[04  action] calculator({'expr': '120 * 0.15'}) -> 18.0
[05 thought] store the tax
[06  action] kv_set({'key': 'tax', 'value': '18.0'}) -> stored tax
[07 thought] compute total
[08  action] calculator({'expr': '120 + 18.0'}) -> 138.0
[09 thought] confirm stored values
[10  action] kv_get({'key': 'base'}) -> 120
[11 final] the total including 15% tax is 138.0
final answer: the total including 15% tax is 138.0
turns used:   5
tools used:   ['calculator', 'kv_get', 'kv_set']

```

## Extending the Loop for Production Use

Because the loop is intentionally framework-agnostic, you can replace the `ToyLLM` class with any real provider client (OpenAI, Anthropic, etc.) without modifying the core control flow.

To embed this loop in a production runtime:

```python
from my_llm_client import LLM
from phases_14_agent_engineering_01_the_agent_loop.code.main import (
    AgentLoop, ToolRegistry, KVStore, calculator
)

tools = ToolRegistry()
tools.register("calculator", calculator)
kv = KVStore()
tools.register("kv_get", kv.get)
tools.register("kv_set", kv.set)

# Replace ToyLLM with a real LLM that returns dicts matching the script format

real_llm = LLM(...)
agent = AgentLoop(llm=real_llm, tools=tools, max_turns=20)

answer = agent.run("Summarize the key steps in the ReAct loop.")
print(answer)

```

The only requirement is that your LLM client returns dictionaries matching the expected format with `kind`, `thought`, and `action` keys.

## Summary

- The **ReAct agent loop** in `rohitg00/ai-engineering-from-scratch` is implemented in pure Python at [`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).
- Five components—**message buffer**, **tool registry**, **stop condition**, **turn budget**, and **observation formatter**—create a deterministic, terminating system.
- The **control flow** uses a simple `for` loop over `max_turns` (default 12) that cycles through perception, reasoning, and action steps.
- The implementation is **framework-agnostic**; swapping `ToyLLM` for a production API client requires no changes to the core loop logic.
- Modern agent SDKs (LangChain, AutoGen, Claude API) wrap this exact pattern, making this scratch implementation valuable for understanding or customizing agent behavior.

## Frequently Asked Questions

### What is the ReAct pattern in agent loops?

The ReAct (Reason + Act) pattern is a cognitive architecture where the LLM alternates between reasoning steps (thoughts) and action steps (tool calls). The model observes the results of its actions and uses that information to inform subsequent reasoning. This loop allows the agent to solve multi-step problems by breaking them down into sequential tool use and analysis.

### How does the turn budget prevent infinite loops?

The turn budget acts as a hard ceiling on loop iterations. In the `AgentLoop` class, `max_turns` (default 12) creates a `for` loop that guarantees termination even if the LLM fails to emit a `"finish"` signal. This prevents runaway API calls and infinite recursion when the model gets stuck in reasoning cycles or repetitive tool calls.

### Can this implementation replace LangChain or AutoGen?

Yes, this implementation demonstrates the core functionality that frameworks like LangChain and AutoGen provide. The `AgentLoop` class offers equivalent capabilities—tool binding, history management, and execution control—without the dependency overhead. When you need specific framework features (parallel execution, persistent memory, or complex multi-agent orchestration), you can extend this base pattern rather than importing entire SDKs.

### Where is the tool registry implemented?

The tool registry is implemented as the `ToolRegistry` class 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 34‑44). It maintains a dictionary mapping tool names to Python functions and provides a `dispatch` method (lines 45‑53) that validates tool existence, executes the callable with provided arguments, and returns a formatted string observation for the agent's history.