# Best Practices for Agent Loop Implementation and Debugging: ReAct Architecture from First Principles

> Implement and debug agent loops effectively with this guide. Discover best practices for ReAct architecture, planner, tool registry, state, and stop conditions using JSON schema and OpenTelemetry.

- Repository: [Rohit Ghumare/ai-engineering-from-scratch](https://github.com/rohitg00/ai-engineering-from-scratch)
- Tags: best-practices
- Published: 2026-07-26

---

**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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/14-agent-engineering/01-the-agent-loop/code/main.py) demonstrates the five ingredients in a toy ReAct loop:

```python

# 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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/14-agent-engineering/01-the-agent-loop/code/main.ts) mirrors the Python design, illustrating language-agnostic best practices:

```ts
// 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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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.

```python

# 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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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_turns`** and **`max_budget_usd`** to 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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/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.