The ReAct loop in plain language — thought, action, observation — and how an agent figures out which tool to reach for.
When I first started using AI coding agents, they felt a bit like magic: you type "fix the failing test," and the thing goes off, reads files, runs commands, and comes back with an answer. Under the hood, though, the core mechanism is surprisingly simple — simple enough to fit in one page. These are my notes on how it works, written the way I wish someone had explained it to me.
A language model, by itself, can only do one thing: read text in, write text out. It can't run a command, open a file, or search the web. What turns a model into an agent is a thin program wrapped around it that does three things, over and over:
The most common shape of this loop is called ReAct — short for Reason + Act. Each turn, the model produces a Thought (its reasoning about what to do next), an Action (which tool to call, with what input), and then the loop feeds back an Observation (what the tool returned). Thought, action, observation. Repeat until the model decides it has enough to answer.
Here's a tiny example — an agent asked to gather two facts. Step through it and watch what the loop actually does. The key thing to watch: the conversation history grows every turn, and each new model call sees all of it.
Three things this little run demonstrates, which generalize to every real agent:
1. Memory is just the transcript. The model has no memory between calls — none. On every iteration it re-reads the entire history from scratch. If the loop ever forgets to append an observation, the model genuinely doesn't know that step happened, so it will reason its way to doing it again. Most "my agent is stuck in a loop" bugs are exactly this: the model isn't confused, it's uninformed — the loop dropped part of the record.
2. The loop has to know when to stop. The model signals it's done by emitting a final answer instead of another action. The wrapper must detect that and exit — and also enforce a maximum number of iterations as a safety net, because a model that never converges would otherwise burn tokens forever. Every real agent framework has both: a termination signal and an iteration cap.
3. Everything the model "does" is text. The action is just a formatted string the wrapper parses. This is why agents can fail in mundane ways — a slightly malformed action line and the parser misses it. Modern APIs solve this with structured tool-calling, where the model outputs the tool name and arguments as typed JSON rather than free text. Same loop; sturdier plumbing.
Here's a minimal, honest version of the loop — stripped of error handling, but structurally the same thing every agent framework is doing underneath:
import re
class ReActAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = {t.name: t for t in tools}
def run(self, task, max_iterations=5):
history = ""
for _ in range(max_iterations): # safety net: never loop forever
prompt = f"Task: {task}\nHistory: {history}\nNext Step:"
response = self.llm.generate(prompt)
if "Final Answer:" in response: # termination signal → exit
return response
action, action_input = self.parse_action(response)
if action in self.tools:
observation = self.tools[action].execute(action_input)
# append — never overwrite. The transcript IS the memory.
history += f"\n{response}\nObservation: {observation}"
else:
history += f"\n{response}\nObservation: unknown tool"
return "Stopped: hit iteration limit."
def parse_action(self, text):
action = re.search(r"Action:\s*(\w+)", text)
arg = re.search(r"Action Input:\s*(.*)", text)
return (action.group(1), arg.group(1).strip()) if action and arg else (None, None)
Every line maps to something from the previous section. The history += is point one — append-only, because the model re-reads it all each call and anything dropped never happened. The Final Answer check and max_iterations are point two — the termination signal and the safety cap. And parse_action pulling structure out of free text with a regex is point three — the fragile part that structured tool-calling replaces in production systems.
This is the part that felt most mysterious to me at first. Nobody writes if "weather" in task: use_search(). So how does the model know?
The unglamorous answer: the tools are described to it in the prompt, and it reads the descriptions. Every tool the agent has comes with a name, a natural-language description ("Search the web for current information"), and a schema of its parameters. All of that goes into the context before your task does. Choosing a tool is then just next-word prediction doing what it does — the model has seen millions of examples of matching a need to a described capability, and it picks the tool whose description fits the situation. Which means:
Tool selection quality is mostly a documentation problem. A tool with a vague description gets misused or ignored; a tool with a precise description of what it does and when to use it gets picked correctly. You improve an agent's tool choice the same way you'd improve a new teammate's — by writing better docs.
At small scale, that's the whole story. At larger scale, two problems appear, and the solutions to both are worth knowing:
Too many tools to fit in context. An agent with hundreds of available tools can't carry every description on every call — that's tokens spent on 195 tools it won't use. The fix is tool retrieval: keep full definitions out of context, give the agent a search-over-tools capability, and load a tool's definition only when a task looks like it needs it. The same "small context, deliberately chosen" principle that applies to conversation history applies to tool inventories.
Tools need instructions, not just schemas. Knowing a tool's parameters isn't the same as knowing how to use it well — the workflows, the gotchas, the order of operations. That's where skills come in: files of instructions the agent reads when a matching task appears, essentially runbooks written for a model instead of a human. (This is the pattern behind my own skills-for-microservices — the hard part of working across microservices isn't calling tools, it's knowing which service owns what, and a skill can carry that knowledge in.) And for connecting agents to external systems in a standard way, MCP (Model Context Protocol) has become the common plug: a server describes its tools once, and any MCP-speaking agent can discover and call them.
Once you see the loop, a lot of everyday agent behavior stops being mysterious:
The loop is simple. Everything hard about agents lives in what you feed it.
Personal notes from learning how this works — simplified on purpose. If I've gotten something wrong, tell me.