π€ The ReAct Loop β the Core of Every AI Agent¶
Author: Dr. Kaikai Liu, Ph.D. Position: Associate Professor, Computer Engineering Institution: San Jose State University Contact: kaikai.liu@sjsu.edu
Class goal. Understand what actually makes a chatbot into an agent: a reason-and-act (ReAct) loop wrapped around a small set of tools. You will read a ~120-line reusable implementation, run it as
sjsujetsontool chat --agent, and see how the same loop powers the Next.js app follow-ups and the CVE-triage labs (12b/12c).ποΈ Overview slides: ReAct Agents βΆ
Companion code β the installable
edge_agentpackage:tools.pyΒ·react_loop.pyΒ·tool_calling.pypip install -e edgeLLM/edge_agent # then: from edge_agent import ReActAgent, Tools
1. π― Chatbot vs. agent¶
A plain chatbot maps one prompt β one answer. It cannot look anything up; it only knows what is in its weights and your prompt.
An agent is a chatbot put in a loop with tools. Given a goal, it repeatedly decides "what should I do next?", takes an action in the world (reads a file, runs a search, calls an API), observes the result, and continues until the goal is met. Two ingredients turn a model into an agent:
- Tools β functions the model may call (here: read/grep/search/write/edit files).
- A control loop β code that runs the chosen tool and feeds the result back.
That loop is the whole idea. Everything else (RAG, multi-agent systems, coding assistants like Claude Code) is a variation on it.
2. π ReAct = Reason + Act¶
ReAct is the most common loop shape. The model is asked to interleave reasoning and actions in a strict text protocol, one step per turn:
Thought: I should look at app.py first
Action: read_file
Action Input: {"path": "app.py", "end": 40}
Our code parses that, runs the tool, and appends the result:
Observation: 1 import requests
2 def fetch_status(url): ...
The model sees the observation and produces the next Thought / Action, looping
until it is confident enough to finish:
Thought: I now know the answer
Final Answer: app.py calls requests.get(url) with a caller-supplied URL.
Why a text protocol? Because it works against any chat endpoint β even a local base model with no "function-calling" feature. The model's reasoning is also right there in the terminal, which makes the agent easy to debug. (When the backend does support structured tool-calling, you can use that instead β see Β§6.)
3. π§° The tools β edge_agent/tools.py¶
We give the agent the same five verbs a human coder uses. Every path is confined to a project root, so the agent cannot wander outside the folder you point it at:
| Tool | Signature | Purpose |
|---|---|---|
read_file |
(path, start=1, end=None) |
read a slice, with line numbers |
grep |
(pattern, path=".", is_regex=False) |
search contents β file:line: text |
search_files |
(glob="*", dir=".") |
find files by name |
write_file |
(path, content) |
create / overwrite a file |
edit_file |
(path, old, new) |
replace one exact, unique snippet |
edit_file is the interesting one β it is a find-and-replace that refuses
to run unless the old text matches exactly once. That forces the agent to
read_file first and quote a unique snippet, which is exactly how real coding
agents avoid clobbering the wrong line:
def edit_file(self, path, old, new):
text = open(self._resolve(path)).read()
count = text.count(old)
if count == 0:
return "ERROR: `old` text not found β read_file first and copy an exact snippet."
if count > 1:
return "ERROR: `old` matches %d places β add surrounding context." % count
open(self._resolve(path), "w").write(text.replace(old, new, 1))
return "edited %s (1 replacement)" % path
The dispatch(name, args) method runs a tool by name and always returns a
string (errors included), so a bad tool call becomes an Observation the
model can recover from rather than a crash.
4. π§ The loop β react_loop.py¶
The whole engine is one class. It is deliberately decoupled from any HTTP
client: you hand it a complete(messages) -> str callable, so the same loop
runs on llama.cpp, NVIDIA Build, OpenAI, or Anthropic.
class ReActAgent:
def __init__(self, complete, tools, *, max_steps=8, log=print):
self.complete, self.tools, self.max_steps, self.log = complete, tools, max_steps, log
def run(self, task):
messages = [{"role": "system", "content": REACT_SYSTEM},
{"role": "user", "content": task}]
for step in range(self.max_steps):
text = self.complete(messages) # 1) the model reasons
messages.append({"role": "assistant", "content": text})
if (m := _FINAL.search(text)): # 2) done?
return m.group(1).strip()
act = _ACTION.search(text) # 3) parse Action + JSON input
name, args = act.group(1).strip(), json.loads(act.group(2))
obs = self.tools.dispatch(name, args) # 4) run the tool
messages.append({"role": "user", "content": "Observation: " + obs}) # 5) feed back
return "(stopped: reached max_steps without a Final Answer)"
Five lines of logic: reason β check-for-done β parse β act β observe, with a
max_steps cap so a confused model can't loop forever (or burn your API quota).
The REACT_SYSTEM prompt tells the model the exact format and lists the tools.
5. βΆοΈ Run it: sjsujetsontool chat --agent¶
Agent mode is built into the chat client. It uses the edge_agent package shipped
in the repo (/Developer/edgeAI/edgeLLM/edge_agent); chat.py finds it on
sys.path automatically β no install required on the Jetson.
sjsujetsontool shell # (keys from ~/.env.local come with you)
sjsujetsontool chat --agent --agent-dir /Developer/edgeAI/edgeLLM/vuln-triage/sample_project
β¦or toggle it inside an ordinary chat session:
/agent on # turn the ReAct loop on
/agent dir ./sample_project # point it at a folder
What does app.py do, and is the requests CVE reachable?
You will watch the trace stream by β each Thought / Action / Observation β
before the final answer:
[step 1]
Thought: read the app to see how requests is used
Action: read_file
Action Input: {"path": "app.py", "end": 40}
Observation: 1 import requests ...
[step 2]
Thought: confirm the URL is caller-supplied
Action: grep
Action Input: {"pattern": "requests.get"}
Observation: app.py:37: response = requests.get(url, timeout=5)
π€ app.py calls requests.get(url) with a caller-supplied URL β the HTTP CVE path is reachable.
Tip. Any backend works, but a tool-following model (NVIDIA Nemotron, a Qwen3.5-Coder, GPT-4o, Claude) follows the ReAct format far more reliably than a tiny base model. Pick one with
/server.
6. π Two ways to call tools β and where each fits¶
react_loop.py uses the text protocol (works everywhere).
tool_calling.py does the same job with the
provider's native tools= field (structured JSON function-calling):
pip install -e "edgeLLM/edge_agent[toolcalling]" # installs the `edge-agent` CLI + openai
edge-agent "Summarize app.py and list its risky calls" \
--dir edgeLLM/vuln-triage/sample_project
ReAct text loop (react_loop.py) |
Native tool-calling (tool_calling.py) |
|
|---|---|---|
| Works on any chat model | β even base / local | β needs a tool-calling model |
| Reasoning visible in terminal | β | partly |
| Parsing robustness | regex (can mis-parse) | provider-enforced JSON |
| Used by | chat --agent, CVE lab 12c |
CVE lab 12b, the Next.js API routes |
They share the same tools.py β only the transport differs. This is
the connective tissue across the curriculum: the
CVE-triage labs are this exact pattern with
security tools, and the Next.js app can add an
agent endpoint by calling the same loop server-side.
7. π οΈ Extend it¶
The design makes extension a one-function change:
- Add a tool. Write a method on
Tools(e.g.run_tests(),http_get(url)), add its name toTOOL_NAMES, and β for native tool-calling β one entry inOPENAI_SCHEMAS. The model can use it immediately. - Swap the brain. Pass any
complete()β point it at a different model with/server, or wrap a different SDK. - Change the policy. Edit
REACT_SYSTEMto require a plan first, or to forbidwrite_file(a read-only "auditor" agent).
8. β Where you are now¶
You should be able to:
- Explain why an agent = LLM + tools + a loop.
- Trace one ReAct turn:
Thought β Action β Action Input β Observation. - Run
sjsujetsontool chat --agentand read the live trace. - Name the trade-off between the ReAct text loop and native tool-calling.
- Add a sixth tool to
edge_agent/tools.py.
Next: see the loop applied to security in Lesson 12b β Basic tool-calling triage.