When I first came across ReAct, my immediate reaction was:
Isn't this just Observe → Think → Act with tool calls?
That is close. What made ReAct click for me was not the shape of the loop itself, but the way an observation from one action can change what the model does next.
I built a small incident-investigation agent twice: first with the OpenAI Responses API where I own the loop, and then with the OpenAI Agents SDK where the runtime owns it. Both investigate the same checkout-latency incident and reach the same evidence-backed conclusion.
First: What Is ReAct?
ReAct stands for Reasoning + Acting.
The basic loop is:
┌───────────┐
│ Reason │
└─────┬─────┘
↓
┌───────────┐
│ Act │
└─────┬─────┘
↓
┌───────────┐
│ Observe │
└─────┬─────┘
│
└──────────→ Reason again
The model does not need to decide the entire path upfront.
It can take one action, inspect the result, and use that observation to decide the next action.
That is the part I find most useful:
Current evidence
↓
Choose next action
↓
Call a tool
↓
Receive observation
↓
Update the investigation
The original ReAct work describes this as combining reasoning traces with actions against an external environment. For a modern production agent, I do not need to expose private chain-of-thought to use the same architectural idea.
The observable loop is enough:
Model decision
↓
Tool call
↓
Tool result
↓
Next model decision
ReAct vs Observe → Think → Act
This was my first confusion because the two loops look almost identical.
Observe → Think → Act is a general control-loop pattern:
Observe
↓
Think
↓
Act
↓
Observe again
ReAct is usually framed around an LLM reasoning and interacting with external tools:
Reason
↓
Act
↓
Observation
↓
Reason again
Operationally, both often become:
Observation
↓
Think / Reason
↓
Action
↓
New Observation
↓
Repeat
I would not treat them as competing architectures.
The distinction I keep in mind is:
- Observe → Think → Act is the broader agent/control-loop idea.
- ReAct emphasizes interleaving LLM reasoning with actions against an external environment.
Why the Observation Matters
Consider this question:
Checkout API latency is high.
Find the likely cause.
A model could guess:
Maybe CPU is saturated.
Maybe the database is slow.
Maybe a recent deployment caused a regression.
Those are hypotheses.
A ReAct-style agent can test them.
Check checkout metrics
↓
CPU and DB look normal
↓
Check deployments
↓
No recent deployment
↓
Check dependencies
↓
payment-service is degraded
↓
Inspect payment-service
↓
fraud-service is degraded
↓
Inspect logs
↓
payment is timing out on fraud
The next action depends on what the previous action returned.
That is more important than simply having tools.
A tool-using model becomes an agent when the feedback from the environment meaningfully affects what it does next.
Let's Build a ReAct Agent Ourselves
I used a small production-incident scenario because it makes the feedback loop easy to see.
The agent has four deterministic mock tools:
get_service_metrics(service)
get_recent_deployments(service)
get_dependency_health(service)
get_logs(service, query)
There are no real production systems behind them. The tools return fixed observations so I can focus on the orchestration.
The dependency path hidden in the mock environment is:
checkout-service
↓
payment-service
↓
fraud-service
The agent is not told that this is the answer. It has to discover the path through tool calls.
The project looks like this:
react/
│
├── manual_react.py
├── agents_sdk.py
├── tools.py
├── terminal_ui.py
├── requirements.txt
│
└── tests/
I implemented the same investigation twice.
Version 1: Own the Loop Manually
The first version uses the OpenAI Responses API directly.
At a high level, the application owns this loop:
while not finished:
response = call_model()
if response_has_tool_call(response):
result = execute_tool()
add_result_back_to_context(result)
else:
return final_answer
Conceptually:
MANUAL LOOP
┌─────────────┐
│ Model │
└──────┬──────┘
│ tool call
↓
┌─────────────┐
│ Our Code │
│ runs tool │
└──────┬──────┘
│ observation
↓
┌─────────────┐
│ Add result │
│ to context │
└──────┬──────┘
│
└──────────→ Model again
My code is responsible for:
- calling the model
- detecting tool calls
- executing the matching Python function
- adding the tool result back to the conversation
- calling the model again
- stopping when the model returns a final answer
- enforcing a maximum number of turns
This is the version that made the mechanics clearest to me.
What Actually Happened?
Here is the investigation the manual loop produced.
Model Turn 1: Check Checkout Metrics
Action:
get_service_metrics
Target:
checkout-service
Observation:
p95 1850 ms (baseline 240 ms)
CPU 42%
DB 18 ms
Checkout is definitely slow.
But CPU is not obviously saturated and the local database latency is low. That makes a purely local checkout bottleneck less convincing.
Model Turn 2: Check Recent Deployments
Action:
get_recent_deployments
Target:
checkout-service
Observation:
0 deployments in the last 24h
A recent checkout deployment was a reasonable hypothesis.
The observation does not support it.
This is the first course correction:
Hypothesis
↓
Check environment
↓
Observation does not support it
↓
Move elsewhere
Model Turn 3: Check Checkout Dependencies
Action:
get_dependency_health
Target:
checkout-service
Observation:
inventory-service: 55 ms (healthy)
payment-service: 1720 ms (degraded)
Now there is a much stronger direction.
The checkout service itself is waiting on a degraded downstream dependency.
Model Turn 4: Inspect Payment Dependencies
Action:
get_dependency_health
Target:
payment-service
Observation:
payment-db: 21 ms (healthy)
fraud-service: 1680 ms (degraded)
The investigation moves one hop further downstream.
checkout-service
↓
payment-service
↓
fraud-service
Model Turn 5: Inspect Payment Logs
Action:
get_logs
Target:
payment-service
Query:
timeout fraud-service
Observation:
fraud-service request timed out after 1500ms
payment authorization delayed waiting for fraud check
Now the dependency latency is backed by application-level evidence.
Payment is not merely slow at the same time as fraud. Its logs explicitly show requests waiting on fraud-service.
Model Turn 6: Check Fraud Metrics
Action:
get_service_metrics
Target:
fraud-service
Observation:
p95 1680 ms (baseline 120 ms)
CPU 76%
DB 16 ms
That is enough evidence to support the propagation path:
fraud-service degradation
↓
payment-service waits / times out
↓
checkout-service latency rises
The final verdict was:
fraud-service is the likely downstream bottleneck.
Its elevated latency propagates through payment-service
to checkout-service.
The available evidence does not establish why
fraud-service itself is slow.
That last sentence matters.
Observation Is Not the Same as Root Cause
The fraud-service metrics show:
CPU = 76%
It would be easy for a model to turn that into:
CPU saturation caused the incident.
But the evidence does not prove that.
What I actually know is:
Observed:
fraud-service latency is degraded.
Observed:
payment-service times out while calling fraud-service.
Supported conclusion:
fraud-service is the likely downstream bottleneck.
Unknown:
why fraud-service itself became slow.
That distinction is useful far beyond this demo.
A production agent should separate:
Facts
Hypotheses
Supported conclusions
Unknowns
instead of turning every correlation into a root cause.
Phase 2: Validate the Evidence in Code
After the model-controlled investigation finishes, my application performs a deterministic evidence check.
This is intentionally separate from the ReAct loop.
Phase 1
Agent investigation
↓
Model chooses tools
↓
Observations collected
Phase 2
Evidence validation
↓
Did the agent gather enough evidence?
For this scenario, the validator checks that the agent established:
checkout degradation
payment degradation
fraud degradation
payment → fraud timeout evidence
The run produced:
Evidence validation: PASS
All required observations support
the dependency path.
This is an application-level guarantee, not another reasoning step.
I like this separation because it lets the model decide how to investigate while keeping an important quality check deterministic.
Version 2: Let the Agents SDK Own the Loop
The second implementation uses the OpenAI Agents SDK.
The tools and incident are the same.
The difference is who owns the orchestration.
Conceptually:
agent = Agent(
name="Incident Investigator",
instructions="""
Investigate using the available tools.
Follow observed evidence rather than assumptions.
If evidence contradicts a hypothesis, change direction.
Do not claim a deeper root cause unless evidence supports it.
""",
tools=[
get_service_metrics,
get_recent_deployments,
get_dependency_health,
get_logs,
],
)
result = Runner.run_sync(
agent,
"Checkout API latency is high. Find the likely cause."
)
I no longer write the repeated model/tool/model loop myself.
The runtime handles it:
AGENTS SDK
┌─────────────┐
│ Model │
└──────┬──────┘
│
↓
┌─────────────┐
│ Tool │
└──────┬──────┘
│ observation
↓
┌─────────────┐
│ Model │
└──────┬──────┘
│
↓
...
My application still provides:
Tools
Instructions
Business rules
Validation
Guardrails
Observability
But the SDK manages the repetitive turn orchestration.
The SDK Run
The Agents SDK version followed essentially the same path:
Model turn 1
get_service_metrics(checkout-service)
Model turn 2
get_recent_deployments(checkout-service)
Model turn 3
get_dependency_health(checkout-service)
Model turn 4
get_dependency_health(payment-service)
Model turn 5
get_logs(payment-service, "timeout fraud")
Model turn 6
get_service_metrics(fraud-service)
The evidence validation also passed, and the final verdict stayed appropriately bounded:
fraud-service is the likely downstream bottleneck.
Its elevated latency propagates through payment-service
to checkout-service.
The available evidence does not establish why
fraud-service itself is slow.
The behavior is similar.
The abstraction boundary is different.
Manual Loop vs Agents SDK
This is the comparison I wanted from the experiment:
MANUAL RESPONSES API
Our application owns:
Model call
↓
Tool call
↓
Execute tool
↓
Append observation
↓
Next model call
↓
Repeat
With the Agents SDK:
AGENTS SDK
Runtime owns:
Model
↓
Tool
↓
Observation
↓
Model
↓
Repeat
Our application still owns the actual tools, domain behavior, validation, and safety boundaries.
The SDK is not removing the agent loop.
It is abstracting it.
Is ReAct Built Into Codex and Claude Code?
Coding agents already behave like iterative model-action-observation systems.
A bug-fixing session can look like:
Inspect repository
↓
Read files
↓
Search references
↓
Edit code
↓
Run tests
↓
Observe failure
↓
Edit again
↓
Run tests
↓
Finish
That is a ReAct-style feedback loop.
I would not say:
Codex = ReAct
A coding agent is much larger than one loop. It also needs things such as:
- context management
- file editing
- shell execution
- permissions
- sandboxing
- retries
- compaction
- termination logic
The useful relationship is:
ReAct-style loop
⊂
Coding-agent architecture
Do I Need to Write ReAct Manually in Production?
Usually, I would not.
The manual implementation was useful because it exposed exactly what happens between model turns.
Once I understand that, an agent runtime is the more practical default for normal production work.
I would consider owning the orchestration myself when the workflow has requirements the runtime does not express cleanly, for example:
Run 3 tools in parallel
↓
Aggregate results
↓
Call a critic model
↓
Retry exactly twice
↓
Require human approval
↓
Continue with another model
or when I need precise control over state, retries, latency, provider independence, or deterministic branching.
My current rule is:
Learn the manual loop once.
Use an agent runtime when it removes
orchestration I do not need to own.
Prompt vs Code
Another lesson from this experiment is that instructions and hard guarantees are different things.
The prompt can guide behavior:
Follow observed evidence.
Use tools instead of guessing.
Change direction when evidence contradicts a hypothesis.
Do not overstate the root cause.
But important constraints should still live in code.
For example, I would not rely only on:
Never refund more than ₹10,000.
I would enforce the boundary:
if amount > 10_000:
raise ApprovalRequired()
The split I keep in mind is:
Prompt
→ how the agent should behave
Code
→ what the system is actually allowed to do
What I Took Away
I started by wondering whether ReAct was materially different from Observe → Think → Act.
Building it made the useful part clearer: the observation is feedback, not just output.
The checkout investigation did not need one perfect plan at the beginning. The model checked an assumption, saw that the evidence did not support it, moved downstream, found a degraded dependency, inspected that dependency, and stopped when the evidence was strong enough.
The manual version made that loop visible:
Model → Tool → Observation → Model
The Agents SDK version showed what a production abstraction can remove:
same loop
different owner
That is the mental model I will carry forward.
Source Code
The complete runnable implementation contains both the manual OpenAI Responses API loop and the OpenAI Agents SDK version, along with deterministic mock tools, terminal output, tests, and evidence validation.
GITHUB / SOURCE CODEExplore the ReAct incident-investigation implementationsriraman-10 / applied-ai-concepts / reactThe implementation intentionally keeps the scenario small so the model → tool → observation → model cycle remains visible.