AI Agent Tool Loop: ReAct Pattern, Loop Engineering, and Termination Strategies

TL;DR — AI agent tool loop: ReAct pattern and loop engineering. Oracle Blog: "Canonical pattern is ReAct: reasoning interleaved with acting. Model reasons about why tool is appropriate, executes call, processes result, reasons again. Three iterations, three tool calls, one complete response." MindStudio: "ReAct loop is iterative — thought, action, observation, next thought. Standard prompting = single output. ReAct = iterative." Data Science Dojo: "From ReAct to loop engineering. No-progress detection, circuit breakers, termination criteria with verifiable automated checks not agent self-assessment." MindStudio: "Loop engineering replaces single-shot with iterative cycles: act, observe, reason, repeat. Engineering choices — structure, termination, management — vary widely." Learn more with what is AI agent, build agent, function calling, and agent memory.

Oracle Blog describes the core pattern: "The canonical pattern is ReAct: reasoning interleaved with acting. The model does not simply select a tool. It reasons about why that tool is appropriate, executes the call, processes the result, and reasons again. Three iterations, three tool calls, one complete response."

MindStudio frames the engineering discipline: "Loop engineering replaces single-shot prompting with iterative cycles: act, observe, reason, repeat. Most agentic systems use loops as their operating model, but the engineering choices within those loops — how they're structured, terminated, and managed — vary widely."

Tool Loop Architecture

flowchart TD subgraph Init["Initialization"] Goal["User Goal
define objective"] System["System Prompt
role, tools, constraints"] Memory["Conversation Memory
messages list"] Limits["Limits
max_steps, timeout
token_budget"] end subgraph Loop["Agent Loop (while not done)"] Reason["1. Reason
LLM thinks about
current state and goal
selects next action"] Act["2. Act
LLM calls tool
with generated arguments"] Observe["3. Observe
tool returns result
append to messages"] Reflect["4. Reflect
LLM processes result
updates understanding
decides next step"] Check{"5. Termination
Check"} end subgraph Termination["Termination Conditions"] GoalDone["Goal Complete
LLM returns answer
no tool calls"] MaxSteps["Max Steps
hard limit reached
return fallback"] NoProgress["No Progress
output unchanged
for N iterations"] CircuitBreaker["Circuit Breaker
tool failed
allowed_fails times"] Timeout["Timeout
wall-clock limit
exceeded"] TokenBudget["Token Budget
consumption
exceeds limit"] Verifiable["Verifiable Check
automated validation
file exists, test passes"] end subgraph Safety["Safety Mechanisms"] ErrorHandling["Error Handling
try/except per tool
return error as result"] ProgressTrack["Progress Tracking
compare output state
across iterations"] Cooldown["Cooldown
failed tool cooled
for cooldown_time"] Logging["Logging
tool, args, result
step number, tokens"] end Goal --> System System --> Memory Memory --> Limits Limits --> Reason Reason --> Act Act --> Observe Observe --> Reflect Reflect --> Check Check -->|Not done| Reason Check -->|Done| GoalDone Check --> MaxSteps Check --> NoProgress Check --> CircuitBreaker Check --> Timeout Check --> TokenBudget Check --> Verifiable ErrorHandling --> Act ProgressTrack --> Check Cooldown --> Act Logging --> Loop

Loop Engineering Concerns

Concern Description Implementation
Loop structure How iterations are organized while loop with step counter
Termination When and how to stop Goal completion, max steps, timeout
Error handling What happens when tools fail try/except, return error as result
Progress detection Is the agent making progress? Compare output state across iterations
Resource limits Prevent runaway costs max_steps, token_budget, timeout
State management What persists between iterations messages list, tool cooldowns
Circuit breaker Stop retrying failing tools allowed_fails threshold + cooldown
Verifiable checks Automated completion validation File exists, test passes, schema valid

Implementation

from dataclasses import dataclass
from typing import Optional
from enum import Enum
import json
import time

class TerminationReason(Enum):
    GOAL_COMPLETE = "goal_complete"
    MAX_STEPS = "max_steps"
    NO_PROGRESS = "no_progress"
    CIRCUIT_BREAKER = "circuit_breaker"
    TIMEOUT = "timeout"
    TOKEN_BUDGET = "token_budget"

@dataclass
class AgentToolLoopGuide:
    """AI agent tool loop implementation guide."""

    def get_production_loop(self) -> str:
        """Production agent tool loop with
        all termination conditions."""
        return (
            "import json\n"
            "import time\n"
            "from openai import OpenAI\n"
            "\n"
            "client = OpenAI()\n"
            "\n"
            "def run_agent_loop(\n"
            "    goal: str,\n"
            "    tools: list,\n"
            "    tool_functions: dict,\n"
            "    system_prompt: str,\n"
            "    max_steps: int = 10,\n"
            "    timeout: float = 60.0,\n"
            "    token_budget: int = 50000,\n"
            "    no_progress_limit: int = 3,\n"
            "    allowed_fails: int = 3,\n"
            "    cooldown_time: float = 30.0,\n"
            "):\n"
            "    '''Production agent loop\n"
            "    with termination conditions.'''\n"
            "    messages = [\n"
            "        {'role': 'system',\n"
            "         'content': system_prompt},\n"
            "        {'role': 'user',\n"
            "         'content': goal}\n"
            "    ]\n"
            "    \n"
            "    start_time = time.time()\n"
            "    total_tokens = 0\n"
            "    tool_failures = {}\n"
            "    tool_cooldowns = {}\n"
            "    prev_output = None\n"
            "    no_progress_count = 0\n"
            "    \n"
            "    for step in range(max_steps):\n"
            "        # Check timeout\n"
            "        elapsed = time.time() - \\\n"
            "            start_time\n"
            "        if elapsed > timeout:\n"
            "            return {\n"
            "                'result': 'Timeout',\n"
            "                'reason': 'timeout',\n"
            "                'steps': step,\n"
            "                'tokens': total_tokens\n"
            "            }\n"
            "        \n"
            "        # Check token budget\n"
            "        if total_tokens > \\\n"
            "            token_budget:\n"
            "            return {\n"
            "                'result': 'Budget',\n"
            "                'reason':\n"
            "                    'token_budget',\n"
            "                'steps': step,\n"
            "                'tokens': total_tokens\n"
            "            }\n"
            "        \n"
            "        print(f'--- Step {step+1} ---')\n"
            "        \n"
            "        # 1. Reason: call LLM\n"
            "        response = client.chat\\\n"
            "            .completions.create(\n"
            "            model='gpt-4o',\n"
            "            messages=messages,\n"
            "            tools=tools)\n"
            "        \n"
            "        msg = response\\\n"
            "            .choices[0].message\n"
            "        messages.append(msg)\n"
            "        total_tokens += response\\\n"
            "            .usage.total_tokens\n"
            "        \n"
            "        # 2. Check termination:\n"
            "        # goal complete\n"
            "        if not msg.tool_calls:\n"
            "            # Verifiable check\n"
            "            # (not just LLM saying\n"
            "            # 'I am done')\n"
            "            return {\n"
            "                'result': msg.content,\n"
            "                'reason':\n"
            "                    'goal_complete',\n"
            "                'steps': step + 1,\n"
            "                'tokens': total_tokens\n"
            "            }\n"
            "        \n"
            "        # 3. Act: execute tools\n"
            "        current_output = []\n"
            "        for call in msg\\\n"
            "            .tool_calls:\n"
            "            name = call.function\\\n"
            "                .name\n"
            "            args = json.loads(\n"
            "                call.function\n"
            "                .arguments)\n"
            "            \n"
            "            # Circuit breaker\n"
            "            # check\n"
            "            if name in tool_cooldowns:\n"
            "                cd_time, cd_start = \\\n"
            "                    tool_cooldowns[\n"
            "                        name]\n"
            "                if time.time() - \\\n"
            "                    cd_start < \\\n"
            "                    cd_time:\n"
            "                    result = (\n"
            "                        f'Tool {name}'\n"
            "                        f' on cooldown')\n"
            "                    current_output\\\n"
            "                        .append(result)\n"
            "                    messages.append({\n"
            "                        'role': 'tool',\n"
            "                        'tool_call_id':\n"
            "                            call.id,\n"
            "                        'content': result\n"
            "                    })\n"
            "                    continue\n"
            "            \n"
            "            func = tool_functions\\\n"
            "                .get(name)\n"
            "            if func:\n"
            "                try:\n"
            "                    result = func(\n"
            "                        **args)\n"
            "                    current_output\\\n"
            "                        .append(str(\n"
            "                            result))\n"
            "                except Exception as e:\n"
            "                    result = (\n"
            "                        f'Error: {e}')\n"
            "                    current_output\\\n"
            "                        .append(result)\n"
            "                    # Track failures\n"
            "                    tool_failures[\n"
            "                        name] = \\\n"
            "                        tool_failures\\\n"
            "                            .get(name, 0) + 1\n"
            "                    if tool_failures[\n"
            "                        name] >= \\\n"
            "                        allowed_fails:\n"
            "                        tool_cooldowns[\n"
            "                            name] = (\n"
            "                            cooldown_time,\n"
            "                            time.time()\n"
            "                        )\n"
            "                        print(\n"
            "                            f'  Circuit:'\n"
            "                            f' {name} cooled')\n"
            "            else:\n"
            "                result = (\n"
            "                    f'Unknown: {name}')\n"
            "                current_output\\\n"
            "                    .append(result)\n"
            "            \n"
            "            # 4. Observe:\n"
            "            # append result\n"
            "            messages.append({\n"
            "                'role': 'tool',\n"
            "                'tool_call_id':\n"
            "                    call.id,\n"
            "                'content': str(\n"
            "                    result)\n"
            "            })\n"
            "            print(f'  {name}: {args}')\n"
            "            print(f'  → {result[:80]}')\n"
            "        \n"
            "        # 5. Progress detection\n"
            "        output_state = ''.join(\n"
            "            current_output)\n"
            "        if output_state == \\\n"
            "            prev_output:\n"
            "            no_progress_count += 1\n"
            "            if no_progress_count >= \\\n"
            "                no_progress_limit:\n"
            "                return {\n"
            "                    'result':\n"
            "                        'No progress',\n"
            "                    'reason':\n"
            "                        'no_progress',\n"
            "                    'steps': step + 1,\n"
            "                    'tokens':\n"
            "                        total_tokens\n"
            "                }\n"
            "        else:\n"
            "            no_progress_count = 0\n"
            "        prev_output = \\\n"
            "            output_state\n"
            "    \n"
            "    # Max steps reached\n"
            "    return {\n"
            "        'result': 'Max steps',\n"
            "        'reason': 'max_steps',\n"
            "        'steps': max_steps,\n"
            "        'tokens': total_tokens\n"
            "    }"
        )

    def get_react_pattern(self) -> dict:
        """ReAct pattern breakdown."""
        return {
            "reason": (
                "LLM generates a thought "
                "about the current state. "
                "Why is this tool appropriate? "
                "What do I expect to learn? "
                "Oracle: 'The model does not "
                "simply select a tool. It "
                "reasons about why that tool "
                "is appropriate.'"
            ),
            "act": (
                "LLM selects a tool and "
                "generates arguments. "
                "Your code executes the "
                "function. MindStudio: "
                "'The agent produces a "
                "thought, takes an action.'"
            ),
            "observe": (
                "Tool returns result. "
                "Result appended to "
                "conversation as tool "
                "message. MindStudio: "
                "'Receives an observation "
                "from that action.'"
            ),
            "reflect": (
                "LLM processes the result "
                "in next turn. Updates "
                "understanding. Decides "
                "next step or final answer. "
                "MindStudio: 'Uses that "
                "observation to inform the "
                "next thought.'"
            ),
            "loop": (
                "Repeat reason-act-observe-"
                "reflect until goal reached "
                "or termination condition "
                "met. Oracle: 'Three "
                "iterations, three tool "
                "calls, one complete "
                "response.'"
            ),
        }

    def get_termination_strategies(self) -> dict:
        """Termination strategies."""
        return {
            "goal_completion": (
                "LLM returns final answer "
                "without tool_calls. "
                "Verify with automated "
                "checks, not just LLM "
                "saying 'I am done'."
            ),
            "max_steps": (
                "Hard limit on iterations "
                "(e.g., 10). Prevents "
                "infinite loops. Return "
                "fallback when exceeded."
            ),
            "no_progress": (
                "Exit if output state "
                "unchanged across N "
                "iterations. Detects "
                "agent stuck in loop "
                "without advancement."
            ),
            "circuit_breaker": (
                "Stop retrying a failing "
                "tool after allowed_fails "
                "threshold. Cooldown for "
                "cooldown_time seconds."
            ),
            "timeout": (
                "Wall-clock limit (e.g., "
                "60 seconds). Prevents "
                "long-running loops from "
                "consuming resources."
            ),
            "token_budget": (
                "Stop when token "
                "consumption exceeds "
                "budget. Controls cost "
                "of multi-step loops."
            ),
            "verifiable_checks": (
                "Automated validation: "
                "file exists, test passes, "
                "schema valid. Data "
                "Science Dojo: 'Define "
                "what done means using "
                "verifiable automated "
                "checks not agent "
                "self-assessment.'"
            ),
        }

    def get_loop_engineering(self) -> dict:
        """Loop engineering principles."""
        return {
            "structure": (
                "How iterations are "
                "organized. while loop "
                "with step counter. "
                "Clear entry and exit "
                "points."
            ),
            "termination": (
                "When and how to stop. "
                "Multiple conditions: "
                "goal, max_steps, timeout, "
                "no_progress, budget."
            ),
            "error_handling": (
                "What happens when tools "
                "fail. try/except per tool. "
                "Return error as result. "
                "Circuit breaker for "
                "repeated failures."
            ),
            "progress_detection": (
                "Is the agent making "
                "progress? Compare output "
                "state across iterations. "
                "Exit if no change for "
                "N steps."
            ),
            "resource_limits": (
                "max_steps, token_budget, "
                "timeout. Prevent runaway "
                "costs and infinite loops."
            ),
            "state_management": (
                "What persists between "
                "iterations. messages list, "
                "tool cooldowns, failure "
                "counts, progress state."
            ),
        }

AI Agent Tool Loop Checklist

  • [ ] Agent tool loop: core architecture where agent iteratively reasons, acts, observes, reflects until goal reached
  • [ ] ReAct pattern: reasoning interleaved with acting — reason about why tool is appropriate, execute, process, reason again
  • [ ] ReAct: (1) Reason — think about current state and goal. (2) Act — select and call tool. (3) Observe — process result. (4) Reflect — update understanding. (5) Repeat
  • [ ] Standard prompting: single input → single output. ReAct loop: iterative — thought, action, observation, next thought
  • [ ] Oracle: "Three iterations, three tool calls, one complete response"
  • [ ] Loop engineering: practice of designing, structuring, and managing iterative cycles
  • [ ] Loop engineering replaces single-shot prompting with iterative cycles: act, observe, reason, repeat
  • [ ] Loop engineering concerns: structure, termination, error handling, progress detection, resource limits, state management
  • [ ] Loop structure: how iterations are organized — while loop with step counter
  • [ ] Termination: when and how to stop — multiple conditions
  • [ ] Error handling: try/except per tool, return error as result, circuit breaker for repeated failures
  • [ ] Progress detection: compare output state across iterations, exit if no change for N steps
  • [ ] Resource limits: max_steps, token_budget, timeout — prevent runaway costs and infinite loops
  • [ ] State management: messages list, tool cooldowns, failure counts, progress state persist between iterations
  • [ ] Termination: goal completion — LLM returns answer without tool_calls
  • [ ] Termination: max steps — hard limit (e.g., 10) to prevent infinite loops
  • [ ] Termination: no-progress detection — exit if output state unchanged across N iterations
  • [ ] Termination: circuit breaker — stop retrying failing tool after allowed_fails threshold, cooldown for cooldown_time
  • [ ] Termination: timeout — wall-clock limit (e.g., 60 seconds)
  • [ ] Termination: token budget — stop when token consumption exceeds limit
  • [ ] Termination: verifiable checks — automated validation (file exists, test passes, schema valid) not agent self-assessment
  • [ ] Data Science Dojo: "Define what done means before the loop starts, using verifiable automated checks not agent self-assessment"
  • [ ] No-progress detection: exit if output state has not changed across iterations
  • [ ] Circuit breakers: retry limits on tool calls, clear failure reporting after set number of attempts
  • [ ] MindStudio: "Observe, reason, act, evaluate — repeatedly — until goal achieved"
  • [ ] MindStudio: "Act — Execute the appropriate routine. Evaluate — Did the action move things toward the goal?"
  • [ ] /loop, /goal, /routines produce agentic loop: self-directing cycle
  • [ ] ReAct = the pattern (reason-act-observe). Loop engineering = the discipline (structure, termination, error handling, etc.)
  • [ ] Loop engineering builds on ReAct by adding production concerns
  • [ ] Production loop: initialize messages, while not done and step < max_steps, call LLM, check tool_calls, execute, track progress
  • [ ] Production loop: check timeout (elapsed > timeout), token budget (total > budget), no-progress (unchanged for N steps)
  • [ ] Production loop: circuit breaker (tool_failures[name] >= allowed_fails → cooldown), logging (tool, args, result, step)
  • [ ] Production loop: return dict with result, reason, steps, tokens for observability
  • [ ] Verifiable checks: file exists, test passes, schema valid — not just LLM saying "I am done"
  • [ ] Cooldown: failed tool cooled for cooldown_time seconds, skip calls during cooldown
  • [ ] Progress tracking: compare output_state to prev_output each iteration
  • [ ] Read what is AI agent for architecture
  • [ ] Read build agent for implementation
  • [ ] Read function calling for tool schemas
  • [ ] Read agent memory for state persistence
  • [ ] Test: agent completes multi-step task with tool loop
  • [ ] Test: max_steps terminates loop without infinite loop
  • [ ] Test: no-progress detection exits after N unchanged iterations
  • [ ] Test: circuit breaker cools tool after allowed_fails
  • [ ] Test: timeout terminates long-running loop
  • [ ] Test: token budget stops loop when exceeded
  • [ ] Test: verifiable checks validate completion correctly
  • [ ] Test: error handling returns errors as tool results for LLM adaptation
  • [ ] Test: logging captures all steps for debugging
  • [ ] Document max_steps, timeout, token_budget, no_progress_limit, allowed_fails, cooldown_time

FAQ

What is the AI agent tool loop?

The agent tool loop is the core architecture where an AI agent iteratively reasons, acts, observes results, and reflects until a goal is reached. Oracle Blog: "The canonical pattern is ReAct: reasoning interleaved with acting. The model does not simply select a tool. It reasons about why that tool is appropriate, executes the call, processes the result, and reasons again. Three iterations, three tool calls, one complete response." MindStudio: "The ReAct loop is iterative — the agent produces a thought, takes an action, receives an observation from that action, and uses that observation to inform the next thought. Standard prompting gives a model input and gets a single output. The ReAct loop is iterative." Data Science Dojo: "Agentic loops: from ReAct to loop engineering. No-progress detection — exit if output state has not changed across iterations. Circuit breakers — retry limits on tool calls. Termination criteria — define what done means before the loop starts, using verifiable automated checks not agent self-assessment." Loop: (1) Reason: think about current state and goal. (2) Act: select and call a tool. (3) Observe: process tool result. (4) Reflect: update understanding. (5) Repeat until done or max steps.

What is loop engineering for AI agents?

Loop engineering is the practice of designing, structuring, and managing the iterative cycles that agents use to complete tasks. MindStudio: "Loop engineering replaces single-shot prompting with iterative cycles: act, observe, reason, repeat. Most agentic systems use loops as their operating model, but the engineering choices within those loops — how they are structured, terminated, and managed — vary widely." MindStudio: "Put together, /loop, /goal, and /routines produce what is often called an agentic loop: a self-directing cycle where the agent observes, reasons, acts, and evaluates — repeatedly — until the goal is achieved. Act — Execute the appropriate routine. Evaluate — Did the action move things toward the goal?" Loop engineering concerns: (1) Loop structure: how iterations are organized. (2) Termination: when and how to stop. (3) Error handling: what happens when tools fail. (4) Progress detection: is the agent making progress? (5) Resource limits: max steps, max tokens, timeout. (6) State management: what persists between iterations.

How do you terminate an AI agent loop?

Define termination criteria before the loop starts using verifiable automated checks, not agent self-assessment. Data Science Dojo: "No-progress detection — exit if output state has not changed across iterations. Circuit breakers — retry limits on tool calls, clear failure reporting after a set number of attempts. Termination criteria — define what done means before the loop starts, using verifiable automated checks not agent self-assessment." Termination strategies: (1) Goal completion: LLM returns final answer without tool calls. (2) Max steps: hard limit (e.g., 10 iterations) to prevent infinite loops. (3) No-progress detection: exit if output state unchanged across N iterations. (4) Circuit breaker: stop retrying a failing tool after allowed_fails threshold. (5) Timeout: wall-clock limit (e.g., 60 seconds). (6) Token budget: stop when token consumption exceeds limit. (7) Human override: operator can stop the loop manually. (8) Verifiable checks: automated validation (e.g., file exists, test passes) rather than agent saying 'I am done'.

What is the difference between ReAct and loop engineering?

ReAct is a specific pattern (reason-act-observe). Loop engineering is the broader discipline of designing and managing agent loops. Oracle Blog: "The canonical pattern is ReAct: reasoning interleaved with acting. The model reasons about why a tool is appropriate, executes the call, processes the result, and reasons again." MindStudio: "Loop engineering replaces single-shot prompting with iterative cycles: act, observe, reason, repeat. The engineering choices within those loops — how they are structured, terminated, and managed — vary widely." Data Science Dojo: "From ReAct to loop engineering. ReAct is the foundational pattern. Loop engineering adds: no-progress detection, circuit breakers, termination criteria, verifiable checks, resource limits." ReAct = the pattern (reason-act-observe). Loop engineering = the discipline (structure, termination, error handling, progress detection, resource limits, state management). Loop engineering builds on ReAct by adding production concerns.

How do you implement a production agent tool loop in Python?

Implement the ReAct loop with termination conditions, error handling, progress detection, and logging. Oracle Blog: "Three iterations, three tool calls, one complete response. The model reasons, acts, processes result, reasons again." Data Science Dojo: "No-progress detection, circuit breakers, termination criteria with verifiable automated checks." MindStudio: "Observe, reason, act, evaluate — repeatedly — until goal achieved." Implementation: (1) Initialize messages with system prompt and user goal. (2) while not done and step < max_steps: call LLM with tools. (3) If no tool_calls: check termination (verifiable). If done: return. (4) Execute tools with try/except. (5) Track progress: compare output state to previous. (6) If no progress for N steps: break. (7) Circuit breaker: count tool failures, cooldown after threshold. (8) Log each step: tool, args, result, step number. (9) Token budget: track and stop if exceeded. (10) Return final answer or fallback.


Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →