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:
phases/14-agent-engineering/01-the-agent-loop/docs/en.md— Contains the lesson description, architectural overview, and the five-ingredient checklistphases/14-agent-engineering/01-the-agent-loop/code/main.py— Provides the full stdlib-only implementation includingToolRegistry,ToyLLM, and the orchestration logic
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:
- Initialize with the user message and an empty history buffer
- Query the LLM (via
respondmethod) for the next action given full history - Parse the LLM's reply into optional thought and required action (tool name + arguments)
- Dispatch the action through
ToolRegistry.dispatch(), capturing the tool output as an observation - Append the observation to the history buffer
- Repeat from step 2 unless a stop condition triggers (finish action, no tool call, or
max_turnsexceeded)
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.pyprovides a complete stdlib-only reference- ToolRegistry handles safe tool dispatch while AgentLoop manages the execution cycle and
max_turnssafety limit - ToyLLM enables deterministic testing, and the standardized
respondinterface 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →