Agents From First Principles 05: AI Agent Gets Stuck in a Loop? Add State, Feedback and Stopping Conditions
An AI agent that keeps calling the same tool, revisiting the same page, rewriting the same file, or repeatedly saying “I’ll try again” is not displaying persistence.
It is displaying a control-flow bug.
This is one of the most common failure modes in agent software because the basic loop is deceptively simple:
observe
↓
decide
↓
act
↓
observe
↓
repeat
The problem is hidden inside the final word.
Repeat until when?
A useful agent needs more than a loop. It needs:
state
feedback
progress signals
budgets
stopping conditions
termination reasons
Without those, an agent can continue making perfectly valid decisions that collectively go nowhere.
This post builds loop control from first principles and then maps it to the software systems where it matters most: coding agents, browser agents, support automation, research agents, data workflows, and long-running task runners.
The core idea is simple:
An agent should not merely know what it did. It should know whether what it did changed the situation in a useful way.
Where This Fits in the Series
We have built the agent stack progressively:
00 agent loop
↓
01 structured actions + validation
↓
02 Best-of-N generation + ranking
↓
03 critique + revision
↓
04 planning + execution
↓
05 state + feedback + stopping
The previous post made plans explicit.
Now we make progress explicit.
Planning answers:
What should happen next?
Loop control answers:
Is what is happening actually moving us toward completion?
The Smallest Broken Agent
Consider this agent:
while True:
action = model(observation)
observation = execute(action)
This contains no definition of success.
It contains no definition of failure.
It contains no budget.
It contains no idea of whether two observations are meaningfully different.
The model is therefore asked to solve a problem the runtime should own:
Should I keep going?
That is dangerous because continuation is not only a reasoning question.
It is also a systems question.
The runtime knows things the model may not know reliably:
- how many steps have executed,
- how many model calls have been made,
- how much money has been spent,
- whether the same action has repeated,
- whether state changed,
- whether a tool failed,
- whether the goal verifier passed,
- whether a time budget expired.
Those are deterministic signals.
They belong in code.
Search Problem: Why Does My AI Agent Keep Calling the Same Tool?
A common loop looks like this:
search("PyTorch DataLoader deadlock")
↓
read results
↓
search("PyTorch DataLoader deadlock")
↓
read results
↓
search("PyTorch DataLoader deadlock")
Every individual tool call is valid.
The failure is temporal.
The agent is not recognizing that it has already performed the action and obtained no new useful information.
The first fix is explicit action history.
history = []
while True:
action = model(observation)
if action in history:
raise RuntimeError("Repeated action detected")
history.append(action)
observation = execute(action)
That is crude, but it reveals the mechanism.
An agent needs memory of its own trajectory.
State Is More Than Conversation History
When developers first add state, they often store only messages:
messages = [
{"role": "user", "content": task},
{"role": "assistant", "content": response},
]
That is useful, but agent state usually needs to be more structured.
For example:
from dataclasses import dataclass, field
@dataclass
class AgentState:
goal: str
step: int = 0
observations: list[str] = field(default_factory=list)
actions: list[dict] = field(default_factory=list)
completed_items: set[str] = field(default_factory=set)
failures: list[str] = field(default_factory=list)
status: str = "running"
Now the runtime can reason deterministically about the trajectory.
conversation history
≠
agent state
Conversation history is mostly model context.
Agent state is runtime truth.
Search Problem: Why Does My Agent Repeat the Same Failed Action?
Suppose a coding agent tries:
run_tests
and receives:
ImportError: missing dependency X
If the state presented to the next model call says only:
Tests failed.
then repeating run_tests is not unreasonable.
The observation needs to contain enough information for adaptation.
observation = {
"action": "run_tests",
"success": False,
"error_type": "ImportError",
"error": "No module named X",
"changed_state": False,
}
This gives us a general rule:
Feedback should describe what happened, not merely whether something happened.
Action → Observation Is a Contract
An action should produce a structured observation.
@dataclass
class Observation:
success: bool
summary: str
changed_state: bool
error_code: str | None = None
artifacts: list[str] = field(default_factory=list)
Then:
observation = executor.execute(action)
The model may receive a textual rendering of this structure.
But the runtime can inspect it directly.
That enables deterministic loop control.
Progress Must Be Measured
A productive agent loop should change something relevant.
For a coding agent, progress might mean:
fewer failing tests
For a browser agent:
reached new page
completed form field
submitted transaction
For a research agent:
new source discovered
new claim verified
coverage increased
For a support agent:
required customer data collected
issue classified
known remediation completed
For a data pipeline:
more rows processed
schema validated
output artifact produced
This is why generic loop controls are not enough.
Every application needs a notion of useful state transition.
A Simple Progress Function
We can make that explicit.
def progress(previous_state, current_state) -> float:
score = 0.0
if current_state.completed_items > previous_state.completed_items:
score += 1.0
if len(current_state.failures) < len(previous_state.failures):
score += 1.0
return score
Real systems will use domain-specific signals.
The important thing is architectural:
state_t
↓
action_t
↓
state_t+1
↓
progress(state_t, state_t+1)
Now the runtime can ask:
Did anything improve?
without asking the model to narrate whether it believes it improved.
Search Problem: AI Agent Stuck in Infinite Loop
The first defense is a hard step limit.
MAX_STEPS = 20
for step in range(MAX_STEPS):
...
This is not sophisticated.
It is still essential.
Every agent loop should have a deterministic outer bound unless the system is explicitly designed as a daemon or persistent service.
Even then, each task episode should usually have its own budget.
A step limit protects against:
- repeated actions,
- parser recovery loops,
- tool failures,
- bad replanning,
- evaluator disagreement,
- model indecision,
- hidden state bugs.
But a maximum step count only stops the failure.
It does not detect it early.
Add a No-Progress Window
Suppose we track a progress score after every action.
progress_history = []
Then:
NO_PROGRESS_LIMIT = 3
if len(progress_history) >= NO_PROGRESS_LIMIT:
recent = progress_history[-NO_PROGRESS_LIMIT:]
if all(score <= 0 for score in recent):
stop("no_progress")
Now the system can terminate after three useless steps instead of twenty.
This matters enormously for expensive models and tools.
Detect Exact Repetition
A simple fingerprint can detect identical actions.
import json
import hashlib
def fingerprint(action: dict) -> str:
payload = json.dumps(action, sort_keys=True)
return hashlib.sha256(payload.encode()).hexdigest()
Then:
seen_actions = set()
fp = fingerprint(action)
if fp in seen_actions:
stop("repeated_action")
seen_actions.add(fp)
This catches:
search(query=X)
search(query=X)
but not:
search(query=X)
search(query=X with slightly different wording)
That requires semantic repetition detection, which belongs later in the sophistication ladder.
Start exact.
Measure how much it catches.
Only then add complexity.
Repetition Is Sometimes Correct
Be careful.
Repeated actions are not always loops.
Examples:
poll_build_status
poll_build_status
poll_build_status
may be valid.
Likewise:
scroll_down
scroll_down
scroll_down
may be necessary in browser automation.
So action definitions can encode repetition policy.
TOOL_POLICY = {
"search": {"repeatable": False},
"poll_build_status": {"repeatable": True},
"scroll_down": {"repeatable": True},
}
This is another recurring principle:
Agent runtime policy should reflect the semantics of the software application.
Detect Cycles, Not Only Repetition
A more subtle loop is:
A → B → A → B → A → B
For example:
open_settings
change_setting
open_settings
change_setting
or:
edit_file
run_tests
edit_file
run_tests
The second example may be productive.
The first may not be.
Again we need progress.
A cycle alone is not enough.
A cycle with no improvement is much more informative.
if repeated_cycle(history) and no_progress(progress_history):
stop("nonproductive_cycle")
Application: Coding Agents
Coding agents are one of the clearest places to apply this pattern.
A typical loop is:
inspect code
↓
edit
↓
run tests
↓
inspect failure
↓
edit
↓
run tests
This is intentionally cyclic.
The loop becomes pathological when the objective metrics do not improve.
Useful progress signals include:
number of failing tests
compiler error count
lint error count
type-check error count
changed failing-test identities
coverage delta
benchmark delta
A coding-agent progress function might be:
def coding_progress(before, after):
return before.failing_tests - after.failing_tests
Then:
12 failures → 8 failures → 3 failures → 0
is clearly productive.
But:
12 → 12 → 12 → 12
should trigger a strategy change or stop.
This is much stronger than asking:
Do you think your fix worked?
Application: Browser Agents
Browser agents often fail through navigational cycles.
Example:
homepage
↓
login
↓
homepage
↓
login
Or:
search results
↓
product page
↓
back
↓
search results
↓
same product page
Useful state includes:
BrowserState(
url=current_url,
page_hash=dom_hash,
completed_fields={...},
submitted=False,
)
Progress signals might be:
- new URL reached,
- new form field completed,
- required data extracted,
- cart changed,
- submission confirmation observed.
A browser agent should not rely solely on visual novelty.
A page may visually change without meaningful task progress.
Application: Research Agents
Research agents have a different loop problem.
They often keep searching indefinitely because there is always another query.
search
↓
read
↓
extract
↓
search again
What constitutes completion?
Possible criteria:
all required claims sourced
minimum independent source count reached
contradictions resolved
coverage threshold reached
budget exhausted
A useful research state might track:
@dataclass
class ResearchState:
claims_required: set[str]
claims_supported: set[str]
sources_seen: set[str]
contradictions: list[str]
Then progress is not “found another webpage.”
Progress is something closer to:
coverage increased
Application: Customer Support Agents
Support agents can get trapped in conversational loops:
Can you provide your order number?
User provides order number.
Can you provide your order number?
That is usually a state-update failure.
The value was present in conversation text but never promoted into structured state.
state.order_number = extracted_order_number
Then the runtime can enforce:
if state.order_number:
disable_action("ask_for_order_number")
This is a powerful design pattern:
Once a prerequisite is satisfied, remove actions whose purpose was to satisfy it.
Action-space reduction is a form of loop prevention.
Application: Data and ETL Agents
Suppose an agent manages a data-cleaning task.
inspect schema
↓
repair data
↓
validate
↓
repair again
Useful progress signals might include:
invalid rows decreasing
schema violations decreasing
missing-value rate decreasing
output artifact produced
If the agent repeatedly transforms the dataset but validation does not improve, the runtime should stop or escalate.
This applies to:
- data ingestion,
- schema migration,
- automated cleanup,
- reconciliation systems,
- report generation.
Application: Long-Running Task Runners
Not every agent is interactive.
Some run for minutes or hours.
These systems need more than max_steps.
They often need multiple budgets:
@dataclass
class Budget:
max_steps: int
max_model_calls: int
max_tool_calls: int
max_seconds: float
max_cost: float
Then continuation becomes a runtime policy.
if budget.exhausted():
stop("budget_exhausted")
This is especially useful for:
- repository analysis,
- autonomous debugging,
- batch research,
- code migration,
- dataset processing,
- automated QA.
Stopping Conditions Should Be Explicit
A serious agent should stop for named reasons.
For example:
class StopReason:
SUCCESS = "success"
MAX_STEPS = "max_steps"
NO_PROGRESS = "no_progress"
REPEATED_ACTION = "repeated_action"
NONPRODUCTIVE_CYCLE = "nonproductive_cycle"
TOOL_FAILURE = "tool_failure"
BUDGET_EXHAUSTED = "budget_exhausted"
USER_INPUT_REQUIRED = "user_input_required"
This matters operationally.
Compare:
agent stopped
with:
agent stopped: no_progress after 3 consecutive non-improving steps
The second is debuggable.
Success Must Be Verified
The most important stopping condition is success.
But success should preferably come from an external verifier.
For a coding agent:
success = tests_pass()
For a browser agent:
success = confirmation_page_visible()
For a data agent:
success = schema_valid(output)
For a research agent:
success = required_claims <= supported_claims
The weakest version is:
success = model_says_done
Use that only when no stronger signal exists.
A Complete Minimal Loop Controller
Here is a small standalone implementation.
from dataclasses import dataclass, field
from typing import Callable, Any
import time
@dataclass
class LoopState:
step: int = 0
action_history: list[dict] = field(default_factory=list)
progress_history: list[float] = field(default_factory=list)
observations: list[Any] = field(default_factory=list)
stop_reason: str | None = None
@dataclass
class LoopBudget:
max_steps: int = 12
max_seconds: float = 60.0
no_progress_limit: int = 3
class AgentLoop:
def __init__(
self,
decide: Callable,
execute: Callable,
measure_progress: Callable,
verify_success: Callable,
budget: LoopBudget,
):
self.decide = decide
self.execute = execute
self.measure_progress = measure_progress
self.verify_success = verify_success
self.budget = budget
def run(self, initial_observation):
state = LoopState(observations=[initial_observation])
started = time.monotonic()
while True:
if self.verify_success(state):
state.stop_reason = "success"
return state
if state.step >= self.budget.max_steps:
state.stop_reason = "max_steps"
return state
if time.monotonic() - started >= self.budget.max_seconds:
state.stop_reason = "time_budget"
return state
action = self.decide(state)
if action in state.action_history:
state.stop_reason = "repeated_action"
return state
before = state.observations[-1]
after = self.execute(action)
state.action_history.append(action)
state.observations.append(after)
state.step += 1
progress = self.measure_progress(before, after)
state.progress_history.append(progress)
limit = self.budget.no_progress_limit
if len(state.progress_history) >= limit:
recent = state.progress_history[-limit:]
if all(value <= 0 for value in recent):
state.stop_reason = "no_progress"
return state
This is already dramatically safer than:
while True:
action = model(...)
execute(action)
But Should We Stop or Recover?
Detecting a loop does not always mean giving up.
We have three broad responses:
stop
change strategy
escalate
For example:
if reason == "no_progress":
strategy = "replan"
or:
if reason == "repeated_action":
strategy = "ask_for_alternative"
or:
if reason == "tool_failure":
strategy = "fallback_tool"
This turns loop detection into feedback.
That gives us:
act
↓
observe
↓
measure progress
↓
productive?
├─ yes → continue
└─ no
↓
diagnose
↓
stop / change strategy / escalate
That is the beginning of adaptive control.
Do Not Let the Recovery Path Become Another Loop
A common mistake is:
main loop stuck
↓
replan
↓
main loop stuck
↓
replan
↓
main loop stuck
So recovery actions need budgets too.
MAX_REPLANS = 2
MAX_TOOL_RETRIES = 2
MAX_CRITIQUE_PASSES = 1
Nested loops need nested limits.
Search Problem: My Agent Never Stops Even After Finishing
This usually means the runtime has no independent completion signal.
The model completes the task, but the loop asks:
What next?
So the model invents another action.
The fix is not necessarily a better prompt.
It is often an explicit verifier.
if goal_verifier(state):
return result
Examples:
all tests pass
required file exists
transaction confirmation received
all requested fields populated
all target claims sourced
Search Problem: My Agent Stops Too Early
The opposite failure happens when the model emits something like:
{"action": "finish"}
before the task is actually complete.
The runtime should validate finish actions too.
if action["name"] == "finish":
if verify_success(state):
stop("success")
else:
reject_action("goal_not_complete")
This preserves the distinction established earlier in the series:
The model proposes. The runtime validates.
Search Problem: My Agent Burns Too Many Tokens
Loop control is also cost control.
Suppose each step consumes roughly:
8,000 input tokens
2,000 output tokens
A twenty-step runaway loop is not just slow.
It can be extremely expensive.
Useful telemetry includes:
model_calls
input_tokens
output_tokens
tool_calls
wall_clock_seconds
cost
steps_to_success
Then benchmark:
cost per successful task
not simply:
average cost per run
A cheaper run that fails is not necessarily cheaper software.
Application Matrix
Here is how this pattern maps into real systems.
| Software | Agent loop | Useful progress signal | Typical stop condition |
|---|---|---|---|
| Coding agent | edit → test → inspect | failing tests decrease | tests pass / no progress |
| Browser agent | navigate → interact → observe | required page/form state changes | confirmation reached |
| Research agent | search → read → extract | claim coverage increases | coverage target met |
| Support agent | ask → classify → act | required case fields resolved | issue resolved / escalation |
| Data agent | inspect → transform → validate | validation errors decrease | dataset valid |
| CI remediation | inspect logs → patch → rerun | failing checks decrease | CI green |
| Migration agent | inspect → transform → verify | migrated units increase | all units migrated |
| QA agent | test → diagnose → retest | defect count decreases | quality threshold reached |
The loop structure is similar.
The meaning of progress is application-specific.
That is the important engineering lesson.
A Better Agent State Machine
Instead of thinking only in terms of prompts, think in states.
RUNNING
↓
ACTION_SELECTED
↓
ACTION_EXECUTED
↓
OBSERVED
↓
PROGRESS_CHECKED
├→ SUCCESS
├→ CONTINUE
├→ RECOVER
└→ STOPPED
This is ordinary software engineering.
That is exactly why it helps.
Agent systems become easier to reason about when we stop pretending every control decision belongs inside natural language.
Telemetry You Should Record
At minimum:
trace = {
"step": step,
"action": action,
"observation": observation,
"progress": progress,
"elapsed_ms": elapsed_ms,
"stop_reason": stop_reason,
}
For production systems also consider:
action fingerprint
state fingerprint
model name
prompt version
tool version
retry count
replan count
token usage
cost
error category
Without trajectory telemetry, an agent loop failure often looks like:
The AI did something weird.
With telemetry it becomes:
Step 7 repeated the same search action as Step 4 after three zero-progress observations.
That is a software bug you can work with.
How to Test Loop Control
Do not wait for production loops.
Inject them deliberately.
Test 1: repeated action
Force the policy to return the same action twice.
Expected:
stop_reason = repeated_action
Test 2: zero progress
Return valid observations that never improve state.
Expected:
stop_reason = no_progress
Test 3: successful completion
Make the verifier pass on step three.
Expected:
stop_reason = success
steps = 3
Test 4: premature finish
Have the model propose finish before success.
Expected:
finish rejected
loop continues
Test 5: budget exhaustion
Make every action unique but useless.
Expected:
stop_reason = max_steps
Benchmark the Mechanism
Compare:
A: no loop controls
B: max-step only
C: max-step + repetition detection
D: max-step + repetition + progress detection
E: D + adaptive recovery
Measure:
verified success rate
average steps
steps on failed tasks
model calls
latency
tool calls
cost per successful task
false-positive stops
false-negative loops
The last two matter.
A loop detector that constantly stops productive work is not useful.
Failure Mode: Progress Metric Is Wrong
Suppose a coding agent measures:
number of changed files
Then it can appear productive by editing more files.
That metric is badly aligned.
Likewise a research agent measuring:
number of URLs visited
can improve forever without answering the question.
The progress metric should move toward the goal, not toward activity.
activity ≠ progress
This distinction is fundamental to agent engineering.
Failure Mode: The Model Games the Progress Signal
If the agent can manipulate its own progress metric, it may accidentally optimize the metric rather than the goal.
For example, if success is:
no failing tests
then deleting tests is technically effective.
So verifiers need integrity constraints.
For a coding agent:
tests pass
AND
required tests still exist
AND
expected behavior preserved
For a research agent:
claims sourced
AND
sources independent
AND
citations actually support claims
This moves us toward verification, which will become a dedicated post later in the series.
Failure Mode: Too Much State
Agent state can grow without bound.
step 1 observation
step 2 observation
...
step 500 observation
If all of that is fed back to the model, context becomes noisy and expensive.
So distinguish:
runtime state
model-visible state
long-term memory
The runtime may preserve the entire trace.
The model may only need:
current goal
current plan
recent actions
current failures
important completed work
This is another reason structured state is better than blindly appending messages.
Failure Mode: Retry Is Mistaken for Recovery
Retry means:
do the same thing again
Recovery means:
change something relevant
Examples:
retry same API call after timeout
can be correct.
But:
retry same invalid tool call after schema rejection
usually is not.
A useful recovery changes one of:
action
arguments
tool
plan
model
context
strategy
When This Technique Is Useful
Use explicit loop control when software contains repeated model-driven action.
Especially:
- coding agents,
- browser automation,
- research systems,
- autonomous debugging,
- support automation,
- data repair,
- workflow remediation,
- CI repair,
- migration tools,
- multi-step tool-use systems.
The more expensive or consequential each action is, the more important deterministic loop control becomes.
When You Do Not Need It
If your system is:
input
↓
one model call
↓
output
then you do not need an agent loop controller.
Likewise, if the process is a known fixed workflow:
A → B → C
ordinary workflow-engine semantics may be enough.
Do not introduce adaptive loops merely to make the architecture look agentic.
The Deeper Pattern
We started the series with:
observe
↓
decide
↓
act
↓
observe
Now we can refine it:
┌──────────────────────────┐
│ │
↓ │
observe │
↓ │
update state │
↓ │
decide │
↓ │
validate │
↓ │
execute │
↓ │
observe result │
↓ │
measure progress │
↓ │
verify completion │
/ | \ │
success recover continue ───────┘
That is much closer to a production agent.
The LLM still matters.
But the reliability comes increasingly from the software around it.
The Rule to Remember
When an agent gets stuck, do not immediately change the prompt.
First ask:
What state changed?
What feedback was produced?
What counted as progress?
What prevented repetition?
What defined success?
What forced termination?
If those questions have no concrete answers, the loop is underspecified.
Next: Agents With Tools
We now have:
structured actions
selection
revision
planning
state
feedback
stopping
The next step is to look more closely at the interface between an agent and the software it can operate.
Because another extremely common failure is:
Why does my AI agent keep choosing the wrong tool?
In the next post we will build tool interfaces from first principles and look at:
- tool descriptions,
- schema design,
- routing,
- action-space size,
- ambiguous tools,
- tool result design,
- tool errors,
- permission boundaries,
- and how the software application itself should constrain what the model is allowed to do.
The agent loop gives us motion.
The tool interface determines where that motion can go.