Implementing an Agent Loop from Scratch: A Pure Python Guide

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

Implementing the Core Components

ToolRegistry and the dispatch Method

The ToolRegistry class in 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:

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:

[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 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 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.

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 →