How to Implement an Agent Loop From Scratch Without Frameworks

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.

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, 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:

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:

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.
  • 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 (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.

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 →