The agent loop, stripped to the atoms.
Welcome to BareMetal ReAct. This repository provides an educational, production-conscious, and extremely minimal implementation of the ReAct (Reasoning + Acting) agent loop in Python.
The goal of this project is radical clarity. Instead of using complex abstractions, decorators, or heavy frameworks (like LangChain, AutoGen, or LlamaIndex), this project implements the reasoning loop explicitly in ~110 lines of readable code.
Most modern agent frameworks hide the underlying mechanics of how Large Language Models (LLMs) reason and act. While abstraction is useful for rapid prototyping, it can lead to:
- Hidden prompt injection vulnerabilities
- Implicit and opaque tool discovery
- "Adapter hell" when switching LLM backends
- Confusing debugging when the agent gets stuck in a loop
BareMetal ReAct improves on these frameworks by:
- Zero Framework Dependency: You see the loop. You understand the loop.
- Type-Safe Tool Validation: We use
pydanticto strictly enforce tool input schemas. If the LLM hallucinates arguments, it fails safely and deterministically, feeding the validation error back to the LLM to auto-correct. - Local-First Support: First-class integration with Ollama for local LLM usage, alongside OpenAI and Anthropic.
- Resiliency: No dynamic
exec()oreval()for tools (except safely constrained mock examples). Tools are explicitly registered and whitelisted.
The entire agent logic lives in the agent.py file, which follows this explicit sequence:
- Prompt LLM: Feed the system prompt, available tools (with JSON schemas), and conversation history.
- Parse Reasoning & Tool Call: Expect the LLM to output a precise JSON contract containing its
thought,action, andaction_input. - Validate: Check that the requested tool exists in the whitelist registry and that the arguments perfectly match the Pydantic schema structure.
- Execute Tool: Run the associated Python function securely.
- Return Result: Append the result as an
Observation:message and repeat until maximum iterations or afinal_answeris reached.
The LLM is instructed to strictly return exactly one of the following JSON structures (no markdown wrappers):
When using a tool:
{
"thought": "I need to get the temperature for Paris to do this math calculation...",
"action": "get_weather",
"action_input": {"city": "Paris", "units": "metric"}
}When returning the final answer:
{
"thought": "I now have all the necessary information to answer.",
"final_answer": "The final calculated result is 88."
}-
Install Dependencies:
pip install -r requirements.txt
-
Configure the Agent: Open
config.pyto set up your desired backend:- For OpenAI:
ACTIVE_BACKEND = "openai",MODEL_NAME = "gpt-4o" - For Anthropic:
ACTIVE_BACKEND = "anthropic",MODEL_NAME = "claude-3-haiku-20240307" - For Ollama (Local):
ACTIVE_BACKEND = "ollama",MODEL_NAME = "llama3"
- For OpenAI:
-
Run the Implementation Example:
python main.py
Note: Because
DEBUG_MODE=Trueis set in config by default, the terminal will sequentially stream each exact thought, registry action, and parser result dynamically on standard out to prove that the loop is working under the hood.
To add a new tool, define a Pydantic BaseModel for its arguments, write the execution function, and register it explicitly in tools.py.
from pydantic import BaseModel
from tools import Tool, registry
class SearchArgs(BaseModel):
query: str
def mock_search(query: str) -> str:
return f"Mock search results for: {query}"
my_tool = Tool(
name="internet_search",
description="Search the web for real-time information.",
args_schema=SearchArgs,
func=mock_search
)
registry.register(my_tool)By ensuring strict pydantic types, the agent engine will automatically validate inputs during the execution loop and return descriptive errors back to the LLM if it hallucinates invalid arguments.
Built for transparency and educational empowerment.