openai SDK pointing at a local Ollama server. View Claude version → · OS Track Index →
TRACK 4 · MODULE 12
The ReAct Pattern
Reason → Act → Observe: the thinking/action cycle that transforms the model from a responder into an autonomous problem-solver.
Learning Objectives
By the end of this module you will be able to:
- Explain the difference between a chatbot and an agent, and when each is appropriate
- Describe the four phases of the ReAct loop (Reason → Act → Observe → Repeat) and what happens in each
- Implement a working ReAct agent using the Chat Completions API and tool-calling loop
- Use thought traces to improve reasoning quality and debuggability
- Define correct stop conditions — without relying on arbitrary iteration caps
- Compare the manual implementation to the Agent SDK approach side-by-side
Chatbot vs. Agent — The Fundamental Difference
Before agents: calling a help desk where the operator can only answer questions from a script. You ask "is my order shipped?" and they look up one field and read it back. One question in, one answer out, every time.
The pain: If your order is late and needs rerouting through a different carrier and the new tracking number needs emailing to you — the operator says "I can't help with that, please call three separate departments." Each department has their own single-answer script.
The mapping: A chatbot is that operator — one message in, one response out, no memory between calls, no ability to take actions. An agent is a project manager who gets your request, decides what steps to take, calls the right departments in sequence, waits for results, and comes back with a complete solution. That autonomy to loop through multiple steps is exactly what ReAct gives the model.
🤖 Chatbot
- One message → one response
- No tools, no actions
- Can only use training knowledge
- Done in a single API call
- Great for: Q&A, summaries, generation
⚙️ Agent (ReAct)
- Goal → multiple reasoning/action cycles
- Uses real tools (search, code, APIs)
- Observes results and adapts its plan
- Runs for as many turns as needed
- Great for: research, automation, multi-step workflows
What Is ReAct?
ReAct (Reasoning + Acting) is a prompting and execution pattern where the model alternates between producing explicit reasoning steps ("Thought: ...") and taking tool actions ("Act: ..."), observing the results of those actions, and repeating until the task is complete.
The name comes from the 2022 academic paper "ReAct: Synergizing Reasoning and Acting in Language Models" by Yao et al. The key insight: models that reason out loud before acting make significantly fewer errors than models that just output answers or call tools silently. The thought step is not decoration — it is the mechanism that improves quality.
Before ReAct, there were two extreme approaches to using language models: pure chain-of-thought (all reasoning, no external tools) or pure tool use (call tools without reasoning about which ones or why). Both had obvious failure modes. Chain-of-thought hallucinated facts because it had no way to verify. Silent tool use called the wrong tool because it had no reasoning step to narrow down the right action.
ReAct fuses both. The "Thought:" step is where the model plans, notices gaps, and decides what action would fill them. The "Act:" step is the actual tool call. The "Observe:" step is reading the tool result. Then the loop continues with another thought — informed by what was just learned — until the model has enough information to give a final answer.
Studies on ReAct showed a 10–40% improvement in task completion on knowledge-intensive benchmarks compared to silent tool use alone. The cost: a few hundred tokens per turn for the thought traces. The benefit: dramatically fewer dead-end tool calls, fewer hallucinated facts, and traces you can actually debug. For production agents handling real tasks, that reliability delta is the difference between something you can ship and something you can only demo.
The ReAct Loop: Reason → Act → Observe → Repeat
The loop has four phases. Let's walk through each one carefully, because understanding what happens at each step is what lets you debug, optimize, and extend ReAct agents.
Phase 1 — Reason
This is where the model thinks out loud. Before calling any tool, it produces a "Thought:" statement describing what it knows, what's missing, and what action would be most useful next. Think of it as the model's internal monologue — except you make it external and visible in the conversation history.
Why force the reasoning to be explicit? Because when the thought is written out, it becomes part of the conversation context. That means subsequent reasoning steps can reference earlier thoughts. The model is essentially leaving notes for itself. Without this, complex multi-step tasks collapse because each tool call happens in a kind of vacuum — the model has already "forgotten" what it was trying to accomplish.
Phase 2 — Act
The Act phase is the model tool call — one of the tool calls you met in M05. the model picks a tool from the available set (web search, code execution, database query, whatever you've given it) and calls it with specific parameters. This is the moment the model interacts with the real world: it reads a file, fetches a URL, runs code, queries an API. Anything can go wrong here (network errors, empty results, access denied), so your agent infrastructure must handle errors gracefully and return them to the model as structured tool results.
Phase 3 — Observe
After the tool executes, your code (not the model — the model doesn't run the tools, it just requests them) appends the tool result to the conversation history as a role: "tool" message. the model reads this result in the next turn — that reading is the "Observe" phase. This is where the model updates its understanding: "The search returned three papers, but none of them have the pricing data I need." That updated understanding feeds directly into the next Reason phase.
Phase 4 — Repeat or Stop
After observing, the model has a choice: call another tool (continue the loop) or produce a final answer (stop). The correct way to detect which it chose is by checking finish_reason in the API response. If finish_reason == "tool_calls", the model requested more tools — keep going. If finish_reason == "stop", the model is done and produced a final text response.
The correct way to determine loop termination is checking finish_reason: 'tool_calls' means continue, 'stop' means done. CRITICAL anti-pattern: parsing the model's natural language response to see if it says "I'm done" or "task complete." Text parsing is fragile — the model may phrase completion differently every time, and any mismatch causes the loop to hang or terminate early.
Implementing ReAct with the model's Tool Use
ReAct is not a library or framework — it's a pattern you implement directly using the Chat Completions API you already know from M05 and M06. The key is the agentic loop: a while loop that keeps calling client.chat.completions.create() until finish_reason == "stop", with each iteration appending both the model's tool calls and your tool results to the running conversation history.
The loop works like this: send the user's request to the model → check if the model wants to use tools → if yes, execute those tools and append results → loop back to the model with the updated history → if finish_reason == "stop", extract and return the final text response.
The conversation history array is the agent's memory — it grows with every turn. the model's "reasoning" happens because each new call includes all previous thoughts, actions, and observations. The stateless API call becomes stateful because your code maintains the history list.
Chunk 1: Tool Definitions
Before the agent loop, define what tools are available. Each tool needs a name, a clear description (the model uses this to decide when to call it — bad descriptions mean wrong tool selections), and a JSON schema for inputs.
# Tool definitions — what the agent can do
# The 'description' field is critical: the model reads it to decide WHEN to call this tool
TOOLS = [
{
"name": "web_search",
"description": "Search the web for current information. Use for: facts that may have changed since training, recent events, prices, statistics. Do NOT use for: well-known stable facts (capitals, basic science), things you already know.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query. Be specific — 'Mistral-7B context window size 2025' is better than 'Mistral context window'."
}
},
"required": ["query"]
}
},
{
"name": "read_url",
"description": "Fetch and return the text content of a web page. Use after web_search when you need the full article, not just the snippet.",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "The full URL to fetch (must start with https://)"}
},
"required": ["url"]
}
}
]
import OpenAI from 'openai';
// Tool definitions — what the agent can do
const TOOLS: object[] = [
{
name: "web_search",
description: "Search the web for current information. Use for: facts that may have changed since training, recent events, prices, statistics. Do NOT use for: well-known stable facts, things you already know.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query. Be specific."
}
},
required: ["query"]
}
},
{
name: "read_url",
description: "Fetch and return the text content of a web page. Use after web_search when you need the full article.",
parameters: {
type: "object",
properties: {
url: { type: "string", description: "The full URL to fetch (must start with https://)" }
},
required: ["url"]
}
}
];
Chunk 2: The Agent Loop
This is the heart of ReAct. Notice three things: (1) the loop condition checks finish_reason, not a counter; (2) every tool call and its result is appended to messages before looping; (3) the system prompt explicitly instructs the model to reason before acting.
from openai import OpenAI
import json
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
SYSTEM_PROMPT = """You are a research assistant with access to web search tools.
Before calling ANY tool, write a Thought: explaining:
1. What you currently know about the question
2. What specific information you still need
3. Why this particular tool call will fill that gap
After receiving a tool result, write another Thought: summarizing what you learned
and whether you need more information or can now answer the question.
This reasoning process makes your answers more accurate and your actions more focused."""
def run_react_agent(user_question: str, max_turns: int = 20) -> str:
"""
Run the ReAct loop until the model finishes (finish_reason == 'stop').
max_turns is a SAFETY NET — the agent terminates via finish_reason, not via this cap.
"""
messages = [{"role": "user", "content": user_question}]
turn_count = 0
while turn_count < max_turns:
turn_count += 1
# --- REASON + ACT phase ---
# the model produces a thought, then optionally requests tool calls
response = client.chat.completions.create(
model="mistral",
tools=TOOLS,
messages=messages
)
msg = response.choices[0].message
# --- STOP CHECK ---
# finish_reason == "stop" → model is done, extract final answer
# finish_reason == "tool_calls" → model wants to call tools, continue loop
if response.choices[0].finish_reason == "stop":
return msg.content or "Agent finished without a text response."
# Append the assistant's message (includes tool_calls) to history
messages.append({
"role": "assistant",
"content": msg.content,
"tool_calls": msg.tool_calls
})
# --- OBSERVE phase ---
# Execute each tool call and append results as tool-role messages
for tc in (msg.tool_calls or []):
result = execute_tool(tc.function.name, json.loads(tc.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
return f"Safety limit reached after {max_turns} turns."
def execute_tool(name: str, inputs: dict) -> str:
"""Dispatch tool calls to their implementations."""
try:
if name == "web_search":
return web_search(inputs["query"])
elif name == "read_url":
return read_url(inputs["url"])
else:
return json.dumps({"error": f"Unknown tool: {name}", "isError": True})
except Exception as e:
# Always return structured errors — never raise. The agent decides what to do next.
return json.dumps({"error": str(e), "tool": name, "isError": True})
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
const SYSTEM_PROMPT = `You are a research assistant with access to web search tools.
Before calling ANY tool, write a Thought: explaining:
1. What you currently know about the question
2. What specific information you still need
3. Why this particular tool call will fill that gap
After receiving a tool result, write another Thought: summarizing what you learned.`;
async function runReactAgent(userQuestion: string, maxTurns = 20): Promise {
const messages: object[] = [
{ role: "user", content: userQuestion }
];
let turnCount = 0;
while (turnCount < maxTurns) {
turnCount++;
// REASON + ACT: the model produces thought + optional tool call
const response = await client.chat.completions.create({
model: "mistral",
tools: TOOLS,
messages,
});
const msg = response.choices[0].message;
// STOP CHECK: finish_reason "stop" means done; "tool_calls" means continue
if (response.choices[0].finish_reason === "stop") {
return msg.content ?? "Agent finished without a text response.";
}
// Append the assistant's message (with tool_calls) to history
messages.push({ role: "assistant", content: msg.content, tool_calls: msg.tool_calls });
// OBSERVE: execute each tool call, append results as tool-role messages
for (const tc of (msg.tool_calls ?? [])) {
const result = await executeTool(tc.function.name, JSON.parse(tc.function.arguments));
messages.push({
role: "tool",
tool_call_id: tc.id,
content: result,
});
}
}
return `Safety limit reached after ${maxTurns} turns.`;
}
while loop runs until the model sets finish_reason == "stop". Each iteration: the model reasons + requests tools → you execute them → you append results → repeat. The messages list is the shared memory that makes all the "reasoning" work — every thought, action, and observation accumulates there.
Thought Traces: The Agent's Working Memory
Thought traces are the "Thought: ..." paragraphs the model writes before each tool call. They look optional but they aren't — they're the mechanism that makes multi-step reasoning coherent. Here's why they matter concretely: imagine the model is researching a question that requires three tool calls. Without thought traces, each call happens in a kind of cognitive vacuum. With thought traces, the second call starts with the model reading its own first thought, the tool result, and its second thought — all of which anchor it to the original intent.
Before thought traces: imagine a researcher who opens a paper, reads a paragraph, closes the paper, opens a second paper, reads a paragraph, closes it — with no notes, no annotations, no connection between what they've read. By the time they finish the third paper, they've forgotten what the first one said.
The pain: They can't synthesize across sources because nothing they read got connected to anything else. The final report is either a copy of the last thing they read or a collection of disconnected fragments.
The mapping: Thought traces are the researcher's notebook. "After reading paper 1, my current understanding is X and I still need to find Y." That note travels into every subsequent step. The agent's final answer can synthesize because the notes are always in context.
Debugging: When an agent produces a wrong answer, the thought traces tell you exactly where it went wrong — was it the wrong tool choice? Wrong search query? Misinterpreted result? Without traces, you have inputs and outputs with a black box between them.
Logging: Thought traces are your audit trail. Every tool call has a "why" recorded immediately before it. This matters in production when someone asks "why did the agent delete that file?" — you can show them the thought that preceded the action.
Stop Conditions: When Should the Agent Stop?
This is where many ReAct implementations go wrong. There are two common anti-patterns and one correct pattern.
Do NOT check if the model's text contains phrases like "I'm done," "task complete," or "I have everything I need." This fails because: (1) the model may phrase completion differently on every run; (2) those phrases may appear inside a thought trace before the model actually finishes; (3) any phrasing mismatch causes the loop to run forever or stop too early.
Do NOT use if turn_count >= 10: break as your main stopping mechanism. A cap of 10 might be too few for a complex research task (agent stops before finishing) and too many for a simple lookup (wastes 9 turns). More critically: if the agent hits the cap on a real task, you get a half-finished result with no indication it's incomplete.
The correct primary stop condition is finish_reason == "stop" — the model decides it has enough information and stops calling tools. The iteration cap (max_turns) is a safety net, not a control mechanism. Set it high enough that legitimate tasks finish (20-50 depending on your use case) and handle the cap-exceeded case explicitly (log it, notify the user, do not silently return partial results).
maxTurns / iteration caps are a SAFETY NET, not a control mechanism. Anti-pattern: using an arbitrary cap (e.g., 10 iterations) as the primary stopping logic. Let the agent terminate naturally via finish_reason. If you're hitting the cap regularly, your agent design has a problem (missing "enough information" signal, tools that never return complete data, or goals that are too open-ended).
Manual Loop vs. Agent SDK — Side by Side Tier 2
M12 is your first Tier 2 module — you've just implemented the ReAct loop by hand, and now you'll see the same logic expressed using the claude-agent-sdk. The goal isn't to replace one with the other — it's to understand what the SDK abstracts away so you can debug it when it breaks.
# Manual ReAct — you own every piece
from openai import OpenAI
import json
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
messages = [{"role": "user", "content": question}]
while True:
resp = client.chat.completions.create(
model="mistral",
tools=TOOLS,
messages=messages
)
msg = resp.choices[0].message
if resp.choices[0].finish_reason == "stop":
return msg.content
messages.append({
"role": "assistant",
"content": msg.content,
"tool_calls": msg.tool_calls
})
for tc in (msg.tool_calls or []):
result = execute_tool(tc.function.name, json.loads(tc.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
# YOU control: history management, tool dispatch,
# error handling, stop logic, logging
// Manual ReAct — you own every piece
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
const messages: object[] = [
{ role: "user", content: question }
];
while (true) {
const resp = await client.chat.completions.create({
model: "mistral",
tools: TOOLS,
messages
});
const msg = resp.choices[0].message;
if (resp.choices[0].finish_reason === "stop") {
return msg.content ?? "";
}
messages.push({ role: "assistant", content: msg.content, tool_calls: msg.tool_calls });
for (const tc of (msg.tool_calls ?? [])) {
const res = await executeTool(tc.function.name, JSON.parse(tc.function.arguments));
messages.push({ role: "tool", tool_call_id: tc.id, content: res });
}
}
// YOU control: everything
# Agent SDK — the loop is handled for you
from claude_agent_sdk import query, ClaudeAgentOptions, tool
@tool
async def web_search(search_query: str) -> dict:
"""Search the web for current information.
Args:
search_query: The search query string
Returns:
dict with search results
"""
# Your implementation here (call real search API)
results = await call_search_api(search_query)
return {"content": [{"type": "text", "text": results}]}
@tool
async def read_url(url: str) -> dict:
"""Fetch and return text content of a web page.
Args:
url: Full URL to fetch (must start with https://)
Returns:
dict with page content
"""
content = await fetch_url(url)
return {"content": [{"type": "text", "text": content}]}
# The SDK handles: loop, history, tool dispatch, stop logic
result = await query(
prompt=question,
options=ClaudeAgentOptions(
model="mistral",
system_prompt=SYSTEM_PROMPT,
tools=[web_search, read_url],
max_turns=20
)
)
print(result.final_text)
// Agent SDK — the loop is handled for you
import { query, ClaudeAgentOptions, tool } from 'claude-agent-sdk';
const webSearch = tool({
name: "web_search",
description: "Search the web for current information.",
parameters: {
search_query: { type: "string", description: "The search query" }
},
execute: async ({ search_query }) => {
const results = await callSearchApi(search_query);
return { content: [{ type: "text", text: results }] };
}
});
const readUrl = tool({
name: "read_url",
description: "Fetch and return text content of a web page.",
parameters: {
url: { type: "string", description: "Full URL to fetch" }
},
execute: async ({ url }) => {
const content = await fetchUrl(url);
return { content: [{ type: "text", text: content }] };
}
});
// SDK handles: loop, history, tool dispatch, stop logic
const result = await query(question, {
model: "mistral",
systemPrompt: SYSTEM_PROMPT,
tools: [webSearch, readUrl],
maxTurns: 20,
} as ClaudeAgentOptions);
console.log(result.finalText);
SDK handles: the while loop, appending messages to history, routing tool calls to your decorated functions, collecting tool results, checking finish_reason, surfacing the final text.
You still control: tool implementations (the actual API calls happen in your @tool functions), the system prompt, model selection, max_turns safety cap, and error handling within each tool.
Why learn both: When something goes wrong in the SDK (wrong tool called, loop terminates early, unexpected output) you need to understand the manual loop to know where to look. The SDK is a productivity layer, not a black box you trust blindly.
Hands-On Lab: Build a ReAct Research Agent
ollama serve)📄
agent.py · tools.py · mock_search.pyEnvironment Setup
mkdir react-agent && cd react-agent
python -m venv venv
# Windows: venv\Scripts\activate
# Mac/Linux: source venv/bin/activate
pip install openai httpx
# Verify Ollama is running and mistral is available
# ollama serve ← start if not running
# ollama pull mistral ← download model if needed
# ollama list ← should show mistral in the list
Step 1: Create the Mock Search Tool
What & Why: We'll use a mock search implementation so the lab works without a real search API key. The mock returns realistic-looking results for a fixed set of queries. When you deploy to production, you swap this for a real search API (SerpAPI, Brave Search, Tavily) — the agent loop code doesn't change at all.
Create mock_search.py:
"""
Mock search tool — simulates web_search results for development.
Swap for httpx + real search API in production.
"""
MOCK_RESULTS = {
"python ai frameworks 2025": """
Top Python AI/Agent Frameworks 2025:
1. LangChain — most ecosystem integrations, complex but powerful
2. LlamaIndex — best for RAG and document processing
3. CrewAI — multi-agent orchestration, growing fast
4. claude-agent-sdk — Anthropic's first-party SDK for Claude agents
5. AutoGen (Microsoft) — enterprise-focused, code execution focus
Trend: First-party SDKs (claude-agent-sdk, OpenAI Agents SDK) are gaining vs wrapper frameworks.
""",
"claude agent sdk features": """
claude-agent-sdk v1.2 Features (May 2025):
- @tool decorator for typed async tool functions
- query() for single-agent invocation
- ClaudeAgentOptions: model, system_prompt, tools, max_turns, hooks
- Built-in: can_use_tool hook for fine-grained control
- create_sdk_mcp_server() for MCP server creation
- Native subagent support via .claude/agents/ directory
- Streaming support with async generators
""",
"react pattern llm agents": """
ReAct (Reasoning + Acting) — Yao et al. 2022:
- Interleaves reasoning traces with tool calls
- Shows 10-40% improvement on knowledge-intensive benchmarks vs silent tool use
- Key principle: thought before action improves tool selection accuracy
- Adopted by: LangChain, LlamaIndex, AutoGen as default agent pattern
- Works with any LLM supporting tool use (Mistral, GPT-4, Gemini, Claude)
"""
}
def mock_search(query: str) -> str:
"""Return mock search results for the given query."""
query_lower = query.lower()
# Find best matching mock result
for key, result in MOCK_RESULTS.items():
if any(word in query_lower for word in key.split()):
return f"Search results for '{query}':\n{result}"
return f"Search results for '{query}':\nNo specific results found. General information: {query} is an active research area in AI. Recent developments include improved tooling and better benchmark performance."
Test it:
python -c "from mock_search import mock_search; print(mock_search('python ai frameworks 2025'))"
Search results for 'python ai frameworks 2025': Top Python AI/Agent Frameworks 2025: 1. LangChain — most ecosystem integrations, complex but powerful ...
react-agent/ directory.Step 2: Build the Complete ReAct Agent
What & Why: This is the full agent combining the system prompt, tool definitions, tool dispatcher, and the ReAct loop. Critically, the agent also extracts and prints thought traces so you can see the reasoning in real time.
Create agent.py:
"""
ReAct Research Agent — M12 hands-on lab
Demonstrates the Reason → Act → Observe loop using Mistral's tool use via Ollama.
"""
from openai import OpenAI
import json
import os
from mock_search import mock_search
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
SYSTEM_PROMPT = """You are a research assistant that compiles accurate, well-sourced reports.
IMPORTANT: Before EVERY tool call, write:
Thought: [your reasoning — what you know, what's missing, why THIS tool call]
After EVERY tool result, write:
Thought: [what you learned and whether you need more information]
When you have enough information, produce a structured report with:
- Summary (2-3 sentences)
- Key Findings (bullet points)
- Sources Used (list the searches you ran)"""
TOOLS = [
{
"name": "web_search",
"description": "Search the web for current information about AI, technology, or research topics. Use specific queries for better results.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query — be specific (e.g., 'claude agent sdk features 2025' not 'claude')"
}
},
"required": ["query"]
}
}
]
def execute_tool(name: str, inputs: dict) -> str:
"""Dispatch a tool call to its implementation."""
if name == "web_search":
return mock_search(inputs.get("query", ""))
return json.dumps({"error": f"Unknown tool: {name}", "isError": True})
def run_agent(question: str, max_turns: int = 20, verbose: bool = True) -> str:
"""
Run the ReAct loop until finish_reason == 'stop'.
Args:
question: The user's research question
max_turns: Safety cap (loop terminates via finish_reason, not this cap)
verbose: Print thought traces and tool calls to stdout
"""
messages = [{"role": "user", "content": question}]
turn = 0
if verbose:
print(f"\n{'='*60}")
print(f"QUESTION: {question}")
print('='*60)
while turn < max_turns:
turn += 1
response = client.chat.completions.create(
model="mistral",
tools=TOOLS,
messages=messages
)
msg = response.choices[0].message
# Print verbose trace
if verbose:
print(f"\n--- Turn {turn} (finish_reason: {response.choices[0].finish_reason}) ---")
if msg.content:
print(f"💭 {msg.content[:300]}{'...' if len(msg.content) > 300 else ''}")
for tc in (msg.tool_calls or []):
print(f"🔧 {tc.function.name}({tc.function.arguments[:100]})")
# STOP: the model has produced its final answer
if response.choices[0].finish_reason == "stop":
return msg.content or "Agent completed without final text."
# Append the assistant's message (with tool_calls) to history
messages.append({
"role": "assistant",
"content": msg.content,
"tool_calls": msg.tool_calls
})
# OBSERVE: Execute each tool call, append results as tool-role messages
for tc in (msg.tool_calls or []):
result = execute_tool(tc.function.name, json.loads(tc.function.arguments))
if verbose:
print(f"👁 Result preview: {result[:150]}...")
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
return f"[Safety cap reached after {max_turns} turns — partial result]"
if __name__ == "__main__":
question = "What are the main Python frameworks for building AI agents in 2025, and how does the claude-agent-sdk compare to them?"
answer = run_agent(question, verbose=True)
print("\n" + "="*60)
print("FINAL REPORT:")
print("="*60)
print(answer)
Step 3: Run the Agent
python agent.py
============================================================
QUESTION: What are the main Python frameworks for building AI agents in 2025...
============================================================
--- Turn 1 (finish_reason: tool_calls) ---
💭 Thought: I need to find current information about Python AI frameworks...
🔧 web_search({"query": "python ai frameworks 2025"})
👁 Result preview: Search results for 'python ai frameworks 2025': ...
--- Turn 2 (finish_reason: tool_calls) ---
💭 Thought: I have a list of frameworks. Now let me search specifically for open-source agent SDKs...
🔧 web_search({"query": "ollama mistral agent sdk features"})
👁 Result preview: Search results for 'ollama mistral agent sdk features': ...
--- Turn 3 (finish_reason: stop) ---
💭 [Final report text]
============================================================
FINAL REPORT:
============================================================
## Summary
Python's agent framework ecosystem has matured significantly in 2025...
## Key Findings
- Five major frameworks dominate: LangChain, LlamaIndex, CrewAI, claude-agent-sdk, AutoGen
- claude-agent-sdk differentiates through first-party Claude integration...
## Sources Used
- web_search("python ai frameworks 2025")
- web_search("claude agent sdk features")
ollama serve.Troubleshooting
- Connection error / refused: Ollama must be running. Start it with
ollama serveand confirm withcurl http://localhost:11434/v1/models. - ModuleNotFoundError: openai: Run
pip install openai— make sure your virtual environment is activated. - Agent stops after 1 turn with no tool calls: Check your system prompt is being passed. Also verify TOOLS list is non-empty.
- Loop never terminates: Usually means tool results are not being appended correctly. Add a
print(messages[-1])after the tool_results append to verify.
Knowledge Check
1. What is the correct way to determine when a ReAct agent should stop looping?
finish_reason == "stop" in the API response
response.choices[0].message.tool_calls is None
2. What does "ReAct" stand for, and why does the pattern improve accuracy?
3. In the ReAct loop implementation, where should tool results be appended in the messages array?
role: "tool" messages — one per tool call — each with a matching tool_call_id, appended after the assistant's message
4. Why are thought traces valuable in a production ReAct agent? (Select all that apply — click the best single answer)
5. You're using the Agent SDK's query() function and the agent terminates after 1 turn without calling any tools. What should you check first?
6. This module (M12) is a Tier 2 module. What does that mean for the lab?
Module Summary
- ReAct = Reasoning + Acting interleaved
- Loop phases: Reason → Act → Observe → Repeat
finish_reason == "stop"= correct stop signal- Thought traces = working memory + audit trail
- Iteration cap = safety net, not control mechanism
- Tool descriptions = the model's selection signal
- ✅ ReAct research agent with verbose trace output
- ✅ Tool definitions with quality descriptions
- ✅ Correct stop-reason loop termination
- ✅ Structured error handling in tool dispatch
- ✅ Manual + SDK side-by-side comparison
ReAct handles sequential reasoning well, but what about tasks so complex that they need a plan before any action is taken? M13 builds on today's loop by adding an explicit planning phase: the model decomposes a goal into a DAG of sub-tasks, then executes each node — potentially in parallel — using the ReAct pattern you just learned.