Best Practices for Agent Loop Implementation and Debugging: ReAct Architecture from First Principles
A production-grade agent loop requires five core ingredients—a planner, tool registry, model driver, state store, and stop condition—combined with strict budget caps, JSON schema validation, and OpenTelemetry tracing to prevent runaway compute and enable deterministic debugging.
Implementing a reliable agent loop is the cornerstone of autonomous LLM systems. The AI Engineering from Scratch repository by rohitg00 provides a comprehensive curriculum for constructing these loops from first principles, emphasizing best practices for agent loop implementation and debugging through defensive programming and comprehensive observability. Mastering these patterns ensures your agents terminate safely, validate tool inputs rigorously, and provide full traceability into every decision.
The Five Essential Ingredients of a Production-Ready Agent Loop
According to phases/14-agent-engineering/01-the-agent-loop/docs/en.md, any robust agent implementation must contain five non-negotiable components. Omitting any element reduces the system to a simple chatbot incapable of autonomous tool use.
The Planner
The planner generates high-level execution strategies based on the current conversational state and available tools. In the reference implementation, this component analyzes the user query and determines the next discrete action to execute.
The Tool Registry
Every callable tool must register with a JSON schema defining its inputs and a deterministic function implementation. The loop validates all arguments against this schema before dispatch to prevent undefined behavior and type mismatches.
The Model Driver
This component interfaces with the LLM API, formatting the current state and planner output into the appropriate prompt structure. The curriculum demonstrates both production LLM calls and stub implementations for isolated testing.
The State Store
A persistent state store records each turn's prompt, model output, tool call, and observation in JSONL format. This append-only log enables deterministic replay, regression testing, and post-mortem analysis of failed runs.
The Stop Condition
Robust loops implement multiple termination criteria. These include max_turns iteration caps, max_budget_usd spending limits, explicit "DONE" signals from the planner, or tool-specific success metrics that trigger graceful exit.
Implementing the ReAct Pattern: Thought, Action, Observation
The ReAct pattern (Yao et al., 2023) forms the cognitive cycle of autonomous agents, following a strict Thought → Action → Observation sequence. The curriculum provides minimal, framework-free implementations demonstrating this pattern in both Python and TypeScript.
Python Implementation
The following stdlib-only implementation from phases/14-agent-engineering/01-the-agent-loop/code/main.py demonstrates the five ingredients in a toy ReAct loop:
# phases/14-agent-engineering/01-the-agent-loop/code/main.py
"""
Toy ReAct agent loop — stdlib only.
Implements the five core ingredients.
"""
import json
import sys
from typing import Callable, Dict, Any
# 1️⃣ Tool registry
Tool = Callable[[Dict[str, Any]], Dict[str, Any]]
TOOLS: Dict[str, Tool] = {}
def register(name: str, fn: Tool):
TOOLS[name] = fn
# Example tool
def echo_tool(inp: Dict[str, Any]) -> Dict[str, Any]:
return {"result": inp["text"]}
register("echo", echo_tool)
# 2️⃣ Simple planner (returns a single action)
def plan(user_msg: str) -> Dict[str, str]:
# Very naive: always call the echo tool
return {"tool": "echo", "args": json.dumps({"text": user_msg})}
# 3️⃣ Stub LLM (just echoes the plan)
def llm(prompt: str) -> str:
return f"Plan: {prompt}"
# 4️⃣ Loop state & stop condition
MAX_TURNS = 5
def run_agent(initial_msg: str):
turn = 0
state = {"messages": [initial_msg]}
while turn < MAX_TURNS:
turn += 1
# Planner
action = plan(state["messages"][-1])
tool_name, raw_args = action["tool"], action["args"]
args = json.loads(raw_args)
# Tool call (contract validation)
if tool_name not in TOOLS:
raise RuntimeError(f"Unknown tool: {tool_name}")
observation = TOOLS[tool_name](args)
# Log turn (JSONL)
log = {
"turn": turn,
"plan": action,
"observation": observation,
}
print(json.dumps(log))
# Update state
state["messages"].append(observation["result"])
# Stop condition – here we stop after one successful echo
if observation["result"] == initial_msg:
break
return state["messages"][-1]
if __name__ == "__main__":
user_input = sys.argv[1] if len(sys.argv) > 1 else "hello"
final = run_agent(user_input)
print("✅ Final output:", final)
This implementation demonstrates strict tool registration isolation, potential JSON schema validation hooks (omitted for brevity), and turn logging that outputs JSONL compatible with the curriculum's test harness.
TypeScript Implementation
The equivalent TypeScript version from phases/14-agent-engineering/01-the-agent-loop/code/main.ts mirrors the Python design, illustrating language-agnostic best practices:
// phases/14-agent-engineering/01-the-agent-loop/code/main.ts
/**
* Toy ReAct agent loop — TypeScript stdlib only.
* Mirrors the Python implementation.
*/
type Tool = (args: Record<string, unknown>) => Record<string, unknown>;
const TOOLS: Record<string, Tool> = {};
function register(name: string, fn: Tool) {
TOOLS[name] = fn;
}
// Example tool
register("echo", (args) => ({ result: args.text }));
function plan(userMsg: string) {
// Naïve planner: always echo the user message
return { tool: "echo", args: { text: userMsg } };
}
// Stub LLM (just returns the plan as a string)
function llm(prompt: string): string {
return `Plan: ${prompt}`;
}
// Loop driver
const MAX_TURNS = 5;
function runAgent(initialMsg: string): string {
let turn = 0;
const messages: string[] = [initialMsg];
while (turn < MAX_TURNS) {
turn += 1;
const action = plan(messages[messages.length - 1]);
const tool = TOOLS[action.tool];
if (!tool) throw new Error(`Unknown tool ${action.tool}`);
const observation = tool(action.args);
// Log turn (JSONL)
console.log(JSON.stringify({ turn, plan: action, observation }));
messages.push(observation.result as string);
// Simple stop condition
if (observation.result === initialMsg) break;
}
return messages[messages.length - 1];
}
// CLI entry point
if (require.main === module) {
const userInput = process.argv[2] ?? "hello";
console.log("✅ Final output:", runAgent(userInput));
}
Both implementations use console.log or print for JSONL logging, enabling capture by the curriculum's test harness for deterministic verification.
Defensive Programming: Budget Caps and Verification Gates
Preventing runaway computation requires strict resource governance. The curriculum in phases/15-autonomous-systems/13-cost-governors/docs/en.md mandates implementing hard limits to ensure safe operation.
Budget and Iteration Governors
Every production loop must enforce max_turns (iteration caps) and max_budget_usd (spending caps). When triggered, the loop aborts cleanly with a concise refusal reason rather than hanging or accumulating infinite API costs. These cost governors check accumulated token usage estimates at the start of each iteration.
Step-Wise Verification Gates
As detailed in phases/19-capstone-projects/25-verification-gates-observation-budget/code/main.py, implement step-wise verification gates that validate the "budget gate" and "refusal gate" after each turn. These gates surface failures immediately rather than allowing error propagation through subsequent iterations. The verification gates lesson supplies mini synthetic loops as isolated testbeds for tracing specific failure modes without consuming real API credits.
Debugging and Observability Strategies
Full visibility into the loop's hidden state transitions requires structured logging and distributed tracing to debug effectively.
Structured JSONL Logging
Each turn must write a JSON line containing turn_id, prompt, model_output, tool_call, and observation. This format supports deterministic replay and diff-based debugging, allowing developers to recreate exact execution paths from log files.
OpenTelemetry Integration
The curriculum in phases/13-tools-and-protocols/20-opentelemetry-genai/docs/en.md demonstrates tracing LLM calls → tool calls → loop iterations using OpenTelemetry. This exposes latency bottlenecks, unexpected tool usage patterns, and cascading failure chains that remain invisible in standard application logs.
# Debug wrapper illustrating OpenTelemetry integration
import opentelemetry.trace as trace
tracer = trace.get_tracer(__name__)
def run_agent_debug(msg):
with tracer.start_as_current_span("agent_loop"):
result = run_agent(msg) # call the core loop
# Accumulated span data can be exported to Jaeger or Zipkin
return result
Replay Capability
The state store's JSONL logs enable deterministic replay for post-mortem analysis. Developers can re-run the exact sequence of tool calls and model responses to isolate race conditions or non-deterministic LLM outputs.
Security and Deployment Patterns
Production deployment requires additional architectural considerations for permissions and secure hosting.
Permission Modes
Following phases/15-autonomous-systems/10-claude-code-permission-modes/docs/en.md, implement Anthropic's permission modes (default, limited, auto) to gate external state access. The loop should verify permissions before entering unattended operation or executing destructive tool calls.
Server-Hosted Loops via MCP Sampling
As shown in phases/13-tools-and-protocols/11-mcp-sampling/docs/en.md, servers can host agent loops without owning an LLM API key by using MCP sampling. In this architecture, the client supplies the model and tool list while the server executes the full loop, reducing key exposure and enabling centralized governance.
Summary
- Agent loops require five ingredients: planner, tool registry, model driver, state store, and stop condition.
- Implement the ReAct pattern with strict JSON schema validation for all tool contracts.
- Enforce
max_turnsandmax_budget_usdto prevent infinite loops and cost overruns. - Use step-wise verification gates to catch failures early in the iteration cycle.
- Log every turn as JSONL and trace with OpenTelemetry for full observability.
- Consider MCP sampling for secure, server-hosted loop architectures that minimize API key exposure.
Frequently Asked Questions
What are the five essential ingredients of an agent loop?
According to phases/14-agent-engineering/01-the-agent-loop/docs/en.md, the five essential ingredients are: a planner to generate actions, a tool registry with JSON schemas, a model driver to interface with the LLM, a state store for persistent JSONL logging, and a stop condition to handle termination criteria like max iterations or budget limits.
How do you prevent an agent loop from running infinitely?
Implement cost governors that enforce max_turns (iteration caps) and max_budget_usd (spending limits) as detailed in phases/15-autonomous-systems/13-cost-governors/docs/en.md. These hard limits cause the loop to abort cleanly with a refusal message when exceeded, preventing both infinite loops and unexpected API charges.
What is the best way to debug a failing agent loop?
Use structured JSONL logging to record every turn's state, combined with OpenTelemetry tracing to visualize the flow from LLM calls to tool executions. The curriculum provides mini synthetic loops in phases/19-capstone-projects/25-verification-gates-observation-budget/code/main.py that allow you to reproduce failures deterministically without consuming real API credits.
How can you run an agent loop on a server without exposing API keys?
Use MCP sampling as implemented in phases/13-tools-and-protocols/11-mcp-sampling/docs/en.md. This pattern allows a server to host and execute the full agent loop while the client retains the LLM API key, supplying the model and tool list via the Model Context Protocol. The server performs the computation without ever accessing the credentials directly.
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 →