ReAct Tool-Calling Agent Loop

A minimal, provider-agnostic reason-act-observe loop with a step budget, tool registry, and safe termination.

reacttool-uselooppython
Setup time30-60 minutes (estimate)
Estimated costOne model call per loop step; set a step budget.
ToolsPython
VerificationNot yet independently verified

The ReAct pattern (Yao et al., 2022) interleaves model reasoning with tool calls. Modern APIs expose this as native tool calling; the loop below works with any chat API that returns tool calls.

Steps

  1. Define tools with a name, a JSON Schema for arguments, and a Python function.
  2. Send the conversation plus tool schemas to the model.
  3. If the model returns tool calls, execute each, append the results as tool messages, and repeat.
  4. Stop when the model returns a plain answer or the step budget is exhausted.
  5. Log every step (input, tool, args, result, latency) for debugging.

Code

import json

MAX_STEPS = 8
TOOLS = {}

def tool(schema):
    def wrap(fn):
        TOOLS[fn.__name__] = {"fn": fn, "schema": schema}
        return fn
    return wrap

@tool({"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]})
def get_weather(city: str) -> str:
    return json.dumps({"city": city, "forecast": "replace with a real API call"})

def run(llm_call, user_msg: str) -> str:
    messages = [{"role": "system", "content": "Use tools when needed. Answer concisely."},
                {"role": "user", "content": user_msg}]
    specs = [{"type": "function", "function": {"name": n, "parameters": t["schema"]}} for n, t in TOOLS.items()]
    for _ in range(MAX_STEPS):
        reply = llm_call(messages, specs)          # returns an assistant message dict
        messages.append(reply)
        calls = reply.get("tool_calls") or []
        if not calls:
            return reply["content"]
        for c in calls:
            name = c["function"]["name"]
            try:
                args = json.loads(c["function"]["arguments"])
                out = TOOLS[name]["fn"](**args)
            except Exception as e:                  # feed errors back so the model can recover
                out = f"ERROR: {e}"
            messages.append({"role": "tool", "tool_call_id": c["id"], "content": out})
    return "Stopped: step budget exhausted."

Notes

Failure modes

Markdown version: /blueprints/react-agent-loop.md

Want a deeper build of this?

Vote for an extended, fully tested version (more tools, a full workflow file). Votes decide what we build next.

– votes

Get one fully built blueprint a week

A weekly email: one blueprint with config, cost, setup time and failure modes, plus a vote on what we build next.

Related blueprints