# Implementing an Agent Loop from Scratch: A Pure Python Guide

> Build an agent loop from scratch with pure Python. Learn how this core control flow enables AI to reason, execute tools, and observe results to complete tasks efficiently.

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

---

**An agent loop is the core control-flow mechanism that enables language models to iteratively reason, execute tools, and observe results until completing a task, and you can build one from scratch using only Python's standard library by combining a message buffer, tool registry, stop condition, turn budget, and observation formatter.**

An agent loop forms the foundation of modern autonomous LLM applications, implementing the ReAct (Reason + Act) pattern to enable decision-making beyond single-turn responses. This article examines the reference implementation in the `rohitg00/ai-engineering-from-scratch` repository, specifically Phase 14, Lesson 01, which provides a complete, framework-free example of implementing an agent loop from scratch. The implementation relies solely on Python stdlib components and follows the canonical architectural pattern introduced by Yao et al. (2022).

## Anatomy of an Agent Loop

At its core, an **agent loop** is a control structure that allows an LLM to interact with external tools through a repeated cycle of reasoning and acting. According to the lesson documentation 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), the loop continues until a stop condition is met—typically when the model emits a `finish` action, produces no tool call, or exhausts a preconfigured turn budget. This pattern matches the **ReAct** loop (Reason + Act), which powers modern SDKs including Claude Agent, OpenAI Agents, LangGraph, and AutoGen v0.4.

## The Five Essential Ingredients

The curriculum defines five mandatory components for any robust agent loop implementation:

### Message Buffer

The **message buffer** maintains the complete conversation history, including the original user query, intermediate thoughts, tool actions, and observation results. This buffer provides the context window for each subsequent LLM call.

### Tool Registry

The **ToolRegistry** class maps tool names to executable callables and handles safe dispatch. It decouples the LLM's string-based action selection from actual function execution, validating arguments before invocation.

### Stop Condition

A **stop condition** determines loop termination. Valid stop triggers include explicit `finish` actions from the LLM, null tool calls indicating completion, or external interrupts.

### Turn Budget

The **turn budget** (`max_turns` parameter) prevents infinite recursion by capping the maximum number of reasoning-acting cycles. This safety mechanism is essential for production deployments.

### Observation Formatter

The **observation formatter** converts raw tool outputs (which may be objects, numbers, or exceptions) into string representations suitable for appending to the message buffer as context for the next LLM iteration.

## Project Structure and Key Files

The reference implementation is organized across two primary locations in the repository:

- **[`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)** — Contains the lesson description, architectural overview, and the five-ingredient checklist
- **[`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)** — Provides the full stdlib-only implementation including `ToolRegistry`, `ToyLLM`, and the orchestration logic

## Implementing the Core Components

### ToolRegistry and the dispatch Method

The `ToolRegistry` class in [`code/main.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/code/main.py) stores name-to-callable mappings and exposes a `dispatch` method for safe execution. When the LLM selects a tool, the registry validates the target exists, executes the function with provided arguments, and returns the result as an observation string. This abstraction allows the agent loop to remain agnostic about specific tool implementations while maintaining clean error handling.

### The ToyLLM for Deterministic Testing

The `ToyLLM` class implements a scripted policy that yields deterministic sequences of `thought`, `action`, and `finish` dictionaries. Unlike production LLMs, this toy model returns pre-programmed responses, making the loop fully testable offline. Each response follows a standardized dictionary structure containing `kind`, `thought`, `action`, `args`, and `content` keys, enabling seamless swapping with real API providers later.

### The AgentLoop Orchestrator

The `AgentLoop` class orchestrates the complete cycle with these steps:

1. **Initialize** with the user message and an empty history buffer
2. **Query** the LLM (via `respond` method) for the next action given full history
3. **Parse** the LLM's reply into optional *thought* and required *action* (tool name + arguments)
4. **Dispatch** the action through `ToolRegistry.dispatch()`, capturing the tool output as an *observation*
5. **Append** the observation to the history buffer
6. **Repeat** from step 2 unless a stop condition triggers (finish action, no tool call, or `max_turns` exceeded)

The orchestrator respects the configurable `max_turns` parameter to prevent runaway execution.

## Running the Complete Example

The repository includes a `build_demo_agent()` factory that wires together three demonstration tools: a **calculator** for safe arithmetic evaluation, and **kv_get** / **kv_set** for an in-memory key-value store. Running the agent produces a complete reasoning trace:

```python
from code.main import build_demo_agent

agent = build_demo_agent()
final_answer = agent.run(
    "What is 120 plus 15% tax, stored in kv?"
)
print("final answer:", final_answer)

```

Executing this script generates the following deterministic trace via the `pretty_trace` utility:

```text
[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

```

Because `ToyLLM` follows a deterministic script, this trace reproduces identically across runs, enabling reliable unit testing.

## Extending to Production LLMs

To replace the toy model with a production LLM such as Claude or GPT-4, implement a `respond` method that accepts the message history and returns a dictionary with the same schema: `kind` (thought/action/finish), `thought` (reasoning string), `action` (tool name), `args` (parameters dict), and `content` (final answer). The surrounding `AgentLoop` logic remains unchanged—the framework is provider-agnostic by design.

## Summary

- An **agent loop** implements the ReAct pattern through iterative cycles of reasoning and tool execution
- The implementation requires five components: message buffer, tool registry, stop condition, turn budget, and observation formatter
- [`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) provides a complete stdlib-only reference
- **ToolRegistry** handles safe tool dispatch while **AgentLoop** manages the execution cycle and `max_turns` safety limit
- **ToyLLM** enables deterministic testing, and the standardized `respond` interface allows swapping to production APIs without modifying loop logic

## Frequently Asked Questions

### What is the difference between an agent loop and a single tool call?

A single tool call executes one function and returns, while an **agent loop** enables multi-step reasoning where the LLM can chain multiple tool calls together. The loop maintains state across turns, allowing the model to observe intermediate results and decide on subsequent actions dynamically until completing the task.

### How does the ReAct pattern relate to agent loops?

**ReAct** (Reasoning + Acting) is the cognitive framework underlying most agent loops. It explicitly separates the LLM's reasoning process (thoughts) from its actions (tool calls), with each action producing an observation that feeds back into the reasoning context. The implementation in [`code/main.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/code/main.py) follows this pattern by parsing separate thought and action fields from each LLM response.

### Can I integrate this implementation with LangChain or LlamaIndex?

Yes. While this implementation is framework-free by design, you can adapt the `ToolRegistry` to wrap LangChain tools or use the `AgentLoop` as a custom agent within LlamaIndex. The key is ensuring the external framework's tools conform to the callable interface expected by the registry's `dispatch` method.

### How do I prevent infinite loops in production agents?

Set a conservative `max_turns` parameter in the `AgentLoop` initialization, implement a `finish` action that the LLM must emit to signal completion, and add timeout mechanisms at the infrastructure level. The reference implementation includes turn budgeting as a first-class safety feature to prevent runaway execution cycles.