Skip to main content

Module 2 — The reasoning and acting loop

The core of every agent is a loop with three alternating moves: the model thinks, the model acts by calling a tool, and the runtime feeds it back an observation. This pattern is called ReAct, from Reasoning and Acting, and its power comes not from complexity but from making the model's steps explicit and short.

The three moves, in order

Each iteration produces three artefacts.

The thought is one or two sentences of natural language in which the model tells itself what it plans to do next. It is what previously happened inside the model on a chain-of-thought prompt, now written to the transcript so the runtime can read it too.

The action is a structured decision: a tool name and its arguments, as a JSON object. This is what the runtime executes.

The observation is the string the tool returns, added back into the model's context. On the next iteration, the model sees its own previous thought, its own previous action, and the observation it produced.

That transcript grows monotonically. Iteration seven sees the entire history of iterations one through six. This is why the loop can plan without external memory — every step is visible to the next — and it is also why the cost of one extra iteration is more than a linear increment. Module 4 comes back to it.

The stopping condition, in three checks

An agent without a stopping condition is an agent that stops when your token bill catches your attention. Three checks belong in every ReAct loop and none is optional.

The final answer. The model's action can be a special tool called finish, whose only argument is the answer to return. Encountering it exits the loop successfully.

The iteration limit. A hard cap — six on the running example, adjust to the task. Reaching it exits the loop with the best available answer and a flag saying the agent gave up.

The budget. A tally of tokens consumed and, optionally, of dollars spent. Reaching either quota exits the loop the same way as the iteration limit. Module 7 formalizes this.

Without all three, the failure mode "loops forever" is not a rare bug: it is the default.

Sixty lines of Python, without a framework

Here is the running example in its skeleton form. The call_model and run_tool helpers are placeholders for module 3.

import json

def react(question: str, tools: dict, max_steps: int = 6) -> str:
transcript = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": question},
]
for step in range(max_steps):
# 1. Ask the model for its next thought and action.
reply = call_model(transcript, tools_schema=describe(tools))
transcript.append({"role": "assistant", "content": reply.text})

action = reply.tool_call # None if the model wrote free text
if action is None:
return reply.text # implicit finish

# 2. The explicit finish tool exits the loop.
if action.name == "finish":
return action.args["answer"]

# 3. Execute the chosen tool and feed back its output.
try:
observation = run_tool(tools, action.name, action.args)
except Exception as exc:
observation = f"tool error: {exc}"

transcript.append({
"role": "tool",
"tool_call_id": action.id,
"content": observation[:4000], # cap: module 4 explains why
})

return "gave up after {} steps".format(max_steps)

Six ideas concentrated in thirty runnable lines. The transcript accumulates the history the model needs. The tools_schema is passed on every iteration so the model always knows what it can call. The exception is caught and turned into an observation — a tool that crashes should not crash the loop, because the model can often recover. And the observation is capped at four thousand characters, because a single unbounded HTML page dumped into context is how the seventh iteration runs out of window.

The system prompt matters more than you think

The prompt does two jobs the code does not.

It names the game: "You are a research assistant. On each step, think in one sentence, then call one tool. Use the finish tool once you can answer with citations." Without this, the model tries to answer directly on iteration one and never calls a tool.

It bounds the thought: "Keep each thought to a single line." Without this, the model writes a paragraph per iteration, which triples input tokens for no reasoning gain. Empirically, one-line thoughts on the running example match multi-paragraph thoughts on answer quality and cost three times less.

First run on the running example

We give the agent the question "Has Postgres 17 shipped a native change-data-capture module?" with two tools — a web search and a reader that fetches a URL. A well-behaved run produces something like this trace, condensed:

  • Thought. I need recent official information; I will search first.
  • Action. web_search({"q": "Postgres 17 native CDC"})
  • Observation. Three URLs, one on postgresql.org.
  • Thought. The official page is the primary source.
  • Action. read_page({"url": "..."})
  • Observation. Four paragraphs on logical replication.
  • Thought. The page confirms logical replication but does not call it CDC; I will finish with that nuance.
  • Action. finish({"answer": "Postgres 17 ships logical replication, which covers CDC use cases, but no module is officially named 'CDC' in the release notes."})

Four iterations, one final answer, every step visible on the transcript. That trace is what module 9 turns into a decision log.

An agent is not a chatbot

The role: "assistant" messages in the transcript are addressed to the loop, not to the user. Only the return value of the loop reaches the user. Confusing the two produces agents that "answer" mid-run and then keep working, which is confusing to instrument and impossible to log correctly.

Summary

  • Each iteration produces a thought, an action (a tool call as JSON) and an observation (the tool's output).
  • The transcript grows monotonically; every iteration sees the whole history, which is what enables planning without external memory — and inflates cost.
  • The stopping condition needs three checks: an explicit finish tool, an iteration cap, and a token or dollar budget.
  • The system prompt names the game and bounds each thought; without it, the model either skips tools or writes essays.

Next module: the description of tools, whose quality decides whether the model calls them correctly or invents plausible arguments.