Home / Articles

Eleven ways to break AI agent infinite loops

Detect stuck tool cycles, add stop conditions, and recover with checkpoints instead of more tokens.

Publisher: REACTAPP.TOP11 min read2555 words

This article is published in English.

Sources: **Your AI Agent Is Stuck in a Loop. Here Are 11 Ways to Fix It**

Agent
  ↓
Tool
  ↓
Result
  ↓
Agent
  ↓
Same Tool
  ↓
Same Tool
  ↓
Same Tool
  ↓
💸

1. Why do AI agents get stuck in loops?

Search customer
→ Find customer
→ Check order
→ Check order again
→ Search customer again
→ Check order again
Bad tool selection
Ambiguous tool results
State not changing
Weak stopping conditions
Repeated failed attempts
Model choosing the same strategy again
track_package("12345")
Status: In transit
Estimated delivery: Tomorrow

2. The naive agent loop

Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.

while (!done) {
  const action = await model.decide(context);
  const result = await executeTool(action);
  context.push(result);
}
Model
 ↓
Tool
 ↓
Result
 ↓
Model
 ↓
Tool
 ↓
Result
 ↓
...

Real-world example

For Real-world example, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For Real-world example, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

get_refund_status()
Refund pending
get_refund_status()

3. Give the agent a budget

When working through 3. Give the agent a budget, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

const MAX_STEPS = 12;
const MAX_TOOL_CALLS = 20;
const MAX_RUNTIME_MS = 30_000;

Why use multiple limits?

When working through Why use multiple limits?, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

Steps        → reasoning budget
Tool calls   → external API budget
Time         → latency budget
Tokens       → model cost

Examples

When working through Examples, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through Examples, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

Search restaurant
→ Search restaurant
→ Search restaurant
→ Check table
→ Check table
→ Check table
→ Search restaurant again
Maximum 8 actions
Maximum 15 seconds
Maximum 5 reservation checks

4. Detect repeated actions

  1. Detect repeated actions works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
search_users(...)
search_users(...)
search_users(...)
const signature = `${toolName}:${JSON.stringify(args)}`;

if (seenActions.has(signature)) {
  throw new Error("Repeated tool invocation detected");
}
seenActions.add(signature);

But exact matching isn’t enough

But exact matching isn’t enough works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.

search("Node.js latency")
search("nodejs latency")
search("Node latency")

5. Detect lack of progress

  1. Detect lack of progress works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
  2. Detect lack of progress works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
Step 1 → Search customer
Step 2 → Search customer by email
Step 3 → Search customer by name
Step 4 → Search customer by phone
Step 1
Find customer → ✅
Step 2
Find order → ✅
Step 3
Check shipment → ✅
Step 4
Check shipment → same result
Step 5
Check shipment → same result

A useful mental model

For A useful mental model, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Prefer structured outputs with schema validation over free-form prose when the next step is code or a tool call.

How many actions did the agent take?
What changed after each action?

6. Don’t retry blindly

For 6. Don’t retry blindly, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.

charge_card()
→ timeout

The same principle applies to agents

For The same principle applies to agents, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For The same principle applies to agents, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

for (let i = 0; i < 10; i++) {
  await callTool();
}
Timeout
→ Retry may make sense

Invalid input
→ Retry with same arguments makes no sense
Permission denied
→ Try a different strategy
Same successful result
→ Stop

7. Give the agent a recovery strategy

When working through 7. Give the agent a recovery strategy, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

search_invoices(customer_id)
→ No result
search_invoices(customer_id)
→ No result
→ search_invoices(customer_id)
→ No result
→ search_invoices(customer_id)
Search by customer ID
        ↓
No result
        ↓
Search by email
        ↓
Still no result
        ↓
Search archived invoices
if (isStuck(state)) {
  return replan({
    reason: "No progress detected",
    previousActions
  });
}

8. Make “done” explicit

When working through 8. Make “done” explicit, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

I found an order.
Done.
Task:
Find latest order

Done when:
- customer identified
- orders retrieved
- orders sorted by date
- latest order selected
- result returned

Real-world analogy

When working through Real-world analogy, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through Real-world analogy, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

Destination reached

9. Add observability before you need it

  1. Add observability before you need it works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
run_id
step
tool
arguments
duration
result size
retry count
token usage
termination reason
Run: abc123

Step 1
search_users
210ms

Step 2
get_user
130ms

Step 3
get_orders
180ms

Step 4
get_orders
175ms

Step 5
get_orders
172ms

Step 6
LOOP DETECTED
Reason: No state change

10. A safer agent loop

  1. A safer agent loop works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
while (!done) {
  if (steps >= MAX_STEPS) {
    return terminate("Step budget exceeded");
  }
  if (Date.now() - startTime > MAX_RUNTIME_MS) {
    return terminate("Time budget exceeded");
  }
  const action = await model.decide(context);
  if (isRepeated(action)) {
    return replan("Repeated action detected");
  }
  const previousState = snapshot(state);
  const result = await executeTool(action);
  state = updateState(state, result);
  if (!hasProgress(previousState, state)) {
    markNoProgress();
  }
  if (tooMuchNoProgress()) {
    return replan("No meaningful progress");
  }
  if (isComplete(state)) {
    done = true;
  }
}

11. The model decides. The harness enforces.

  1. The model decides. The harness enforces. works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Budget tokens per turn and per session. Agentic tools expand context aggressively; hard caps keep demos from becoming surprise invoices.
  2. The model decides. The harness enforces. works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
What should I do next?
How many times can you act?
How long can you run?
Have you repeated yourself?
Are you making progress?
Did the state change?
Are you allowed to call this tool?
When should execution stop?
Speed limiter
Brake system
Fuel gauge
Navigation system
Collision warning

The goal isn’t to eliminate loops

Write code
→ Run tests
→ See failure
→ Fix code
→ Run tests
→ See failure
→ Fix code
→ Tests pass
→ Stop
Did the agent loop?
Did the agent make progress?

The production checklist

☑ Is there a maximum step count?
☑ Is there a runtime limit?
☑ Is there a tool-call budget?
☑ Can repeated actions be detected?
☑ Can we detect lack of progress?
☑ Are retries failure-aware?
☑ Can the agent re-plan?
☑ Is "done" explicitly defined?
☑ Can we see every tool call in production?
☑ Do we know why an agent terminated?

Final thought

Thank you for being a part of the community

Operational checklist

Rewrite marker 1 for 3372f6d9932d: rephrase surrounding claims with operator language, keep [[CODE_n]] slots intact, and avoid echoing source sentences.

Rewrite marker 2 for 3372f6d9932d: rephrase surrounding claims with operator language, keep [[CODE_n]] slots intact, and avoid echoing source sentences.

Rewrite marker 3 for 3372f6d9932d: rephrase surrounding claims with operator language, keep [[CODE_n]] slots intact, and avoid echoing source sentences.

Rewrite marker 4 for 3372f6d9932d: rephrase surrounding claims with operator language, keep [[CODE_n]] slots intact, and avoid echoing source sentences.

Rewrite marker 5 for 3372f6d9932d: rephrase surrounding claims with operator language, keep [[CODE_n]] slots intact, and avoid echoing source sentences.

Rewrite marker 6 for 3372f6d9932d: rephrase surrounding claims with operator language, keep [[CODE_n]] slots intact, and avoid echoing source sentences.

Rewrite marker 7 for 3372f6d9932d: rephrase surrounding claims with operator language, keep [[CODE_n]] slots intact, and avoid echoing source sentences.