Spurlock Studios
Contact
Why Agents Loop on Failed Tools: No-Progress Detection Beats Longer Prompts

Your agent keeps calling the same failed tool because the harness treats every model turn as progress. The model sees an error, believes another attempt will help, and you never fingerprint the call as a no-progress repeat. Longer prompts do not fix a missing loop detector.

This spoke belongs to the Agentic Systems Operating Manual. The cage is state machines for agent loops; this post owns no-progress detection inside that cage. Also see tool-use sandboxes for what tools are allowed to do when they run.

The short answer

  • A legitimate retry changes something material: args, backoff, or a different tool. A no-progress loop repeats the same fingerprint.
  • Context summarization often re-triggers loops by dropping the “this already failed” evidence.
  • Fix it in the harness: fingerprint tool calls, honor retryable: false, cap turns, terminate with a reason code, alert on loop-rate spikes.
  • Prompting “don’t retry forever” is a hint, not a control.
  • Golden-case the loop once you kill it — or it returns after the next prompt edit.

No-progress loop vs legitimate retry

Ops needs a crisp distinction. Without it, every retry looks like diligence.

SignalLegitimate retryNo-progress loop
Tool nameSame or alternateSame
ArgumentsChanged (id, page, filter)Identical or equivalent
TimingBackoff / jitterImmediate hammer
Prior resultTransient error (429, timeout)Permanent (404, auth, validation)
Harness viewNew fingerprint or marked retryableSame fingerprint ≥ N times

Rule of thumb: if a human watching the trace would say “it’s doing the same thing again,” the harness should already have stopped it.

Why models loop even when the error is clear

Models optimize for completing the user job. An error message is just more context. Unless the harness injects a hard stop, the next plan often is “try the tool again.” That is rational under incomplete control — and expensive under write tools or paid APIs.

Common fuel for loops:

  1. Vague tool errors"failed" with no retryable flag
  2. Silent empty results[] treated as “search again with same query”
  3. Prompt pressure — “keep going until done” with no terminate authority
  4. Missing memory of failure — see summarization below

Do not moralize the model. Instrument the harness.

Why context summarization re-triggers loops

Long runs compress history. Summarizers keep “goals” and drop “we already called crm.get_contact with id X and got not_found three times.” The model, seeing a fresh window, rediscovers the same plan.

Controls that survive summarization:

  • Persist a failure ledger outside the prompt: (tool, arg_hash, error_code, count, last_at)
  • Inject that ledger into every turn as structured system state, not chat prose
  • Never summarize away terminal tool errors for the current run
  • On summarize, keep the last N distinct failure fingerprints verbatim

If failure evidence only lives in chat tokens, compression will resurrect the loop.

How to fingerprint tool calls in the harness

Fingerprinting is the core no-progress signal. Compute before execute:

fingerprint = hash(tool_name + normalize(args) + side_effect_class)

Normalization rules matter:

  • Sort object keys
  • Strip volatile fields (request_id, timestamps you inject)
  • Canonicalize ids (string trim, lowercase where safe)
  • Exclude auth headers from the hash — they are harness-owned

Store per run:

FieldPurpose
fingerprintIdentity of the attempt
countHow many times this run hit it
first_error_code / last_error_codeStability of failure
retryableFrom tool or classifier
blockedHarness refused further executes

Policy example (illustrative defaults — tune per job):

  1. Same fingerprint + retryable: false → refuse immediately, reason tool_no_progress
  2. Same fingerprint + retryable → allow up to 2 retries with backoff, then refuse
  3. Distinct fingerprints that still share tool + error class → soft warn; escalate after threshold

The model can propose the call. The harness decides whether it runs.

retryable: false semantics tools should return

Tools are part of the control loop. Error payloads should be machine-readable:

{
  "ok": false,
  "error_code": "contact_not_found",
  "retryable": false,
  "message": "No contact for id=…"
}
Error classretryableHarness action
Not found / validationfalseBlock fingerprint; maybe alternate tool once
Auth / permissionfalseTerminate tool_auth_error
Rate limit / timeouttrueBackoff, capped retries
5xx / upstream bliptrueBackoff, then escalate
Unknowndefault false in prodFail closed

Defaulting unknown errors to retryable is how you buy infinite loops. Prefer fail closed; loosen per tool after evidence.

Turn caps and terminating reason codes

Infinite max_turns is a production bug, not a feature. Cap turns and cap no-progress events.

Recommended terminal reason codes for this failure family:

  • tool_no_progress — fingerprint blocked after policy
  • tool_retry_exhausted — retryable path used up
  • max_turns — budget of steps hit
  • escalate — human path with the failure ledger attached

State machines define legal states and transitions (state machines for agent loops). Harness guards define when “act” refuses to execute the proposed tool. You need both: cage + detector.

Alert when loop rate spikes online

Offline golden sets catch known loops. Online you need a rate.

Track per job type:

MetricWhy
% runs with any blocked fingerprintLoop pressure
avg duplicate fingerprints per runSeverity
runs terminated tool_no_progressHard stops working
cost of runs with loop≥1Money on the floor

Alert when blocked-fingerprint rate exceeds a trailing baseline after a deploy or prompt change. That is how you catch “helpful” prompt edits that remove the “stop retrying” language — or, better, prove your harness does not depend on that language.

Illustrative failure: the 404 hammer

Illustrative — not a client result. Agent is told to update contact c_1842. Tool returns 404 contact_not_found, retryable: false. Without fingerprinting, the agent retries the same id twelve times, then tries nearby ids it invents. With fingerprinting: first failure records the fingerprint; second proposal is refused; run terminates tool_no_progress with escalate package for a human to verify the id.

Cost difference is not subtle when the tool is a paid enrichment API.

ABAB handoff oscillations vs same-tool loops

Same-tool loops are one fingerprint repeating. Multi-agent systems add a second species: Agent A hands to Agent B, B hands back to A, neither advances the artifact.

PatternDetectionFix
Same-tool loopFingerprint countBlock tool; reason code
ABAB handoffHandoff graph cycle / identical package hashBreak cycle; merge agents or escalate
Alternate-tool thrashTool set cycles without state changeRequire state checksum progress

Oscillations belong to multi-agent handoffs. Do not stretch tool fingerprinting to cover them — detect package-level no-progress separately.

Where state machines help vs harness guards

ConcernState machineHarness no-progress guard
Legal states (plan/act/eval)Owns
Revision ceilingsOwns
Escalate pathsOwnsTriggers into
Same tool+args againOwns
retryable policyOwns
Turn / budget capsSharedShared

If you only have a state machine, you can still spin inside act. If you only have fingerprinting, you can still wander illegal states. Ship both.

Procedure: add no-progress detection this week

  1. Define fingerprint normalization for each tool.
  2. Add per-run failure ledger (store beside traces).
  3. Enforce retryable from tool payloads; default unknown → false in prod.
  4. Set max_turns and max blocked-fingerprint count per job type.
  5. Emit reason codes on terminate; wire one alert on loop-rate spike.
  6. Add a golden case that expects tool_no_progress for a known bad id.
  7. Confirm summarization preserves the failure ledger.

Checklist for the PR:

  • Fingerprint computed pre-execute
  • Block path refuses model retries
  • Reason code visible in ops dashboard
  • Golden case green
  • Alert stubbed (even if threshold is temporary)

How to add a golden case for a known loop

Capture a production offender once:

  1. Freeze tool stubs that return the permanent error.
  2. Assert the agent proposes the tool (optional).
  3. Assert the harness blocks the second identical fingerprint.
  4. Assert terminal reason is tool_no_progress (or your chosen code).
  5. Assert no write tools ran after the block.

Prompts will change. The golden case keeps the detector honest.

What not to do

Raise temperature and hope. Irrelevant to deterministic 404s.

Add “please don’t loop” to the system prompt as the only fix. It will drift.

Retry all errors three times. Auth and validation errors are not transient.

Log loops without terminating. Observation without a brake is a museum exhibit.

Infinite max_turns “for hard tasks.” Hard tasks need escalate, not eternity.

Pilot minimum

In a Spurlock Studios $1,500 · 5-day agentic pilot, no-progress detection is part of the thin harness: fingerprinting on write-capable tools, turn caps, reason codes, and at least one golden loop case. Full multi-agent oscillation detection can wait; same-tool loops should not.

Start from /agentic. Architecture context: operating manual.

CTA

Stop paying for the same failed tool call.

/agentic · /contact?intent=agentic-pilot

FAQ

What max_turns default is dangerously infinite?

Any default that is null, zero-means-unlimited, or set in the thousands “just in case.” Pick a finite cap per job type that matches real successful trajectories, then escalate — do not let the model grind until the budget burns.

Should tools return retryable: false?

Yes for permanent failures: not found, validation, auth, and business-rule rejects. Transient classes (rate limit, timeout, some 5xx) return true with harness-enforced caps. Unknown errors should default to non-retryable in production.

How do ABAB handoff oscillations differ from same-tool loops?

Same-tool loops repeat one fingerprint. ABAB oscillations bounce work between agents without artifact progress. Detect them with handoff-package hashes and cycle checks, not only tool fingerprints — see multi-agent handoff design.

Where do state machines help vs harness guards?

State machines own legal states, transitions, and revision ceilings. Harness guards own fingerprinting, retryable policy, and refusing duplicate executes inside act. You need the cage and the detector.

What reason code should terminate the run?

Use a dedicated code such as tool_no_progress when a fingerprint is blocked, and tool_retry_exhausted when retryable attempts are spent. Do not overload generic error — ops cannot trend mush.

How do I add a golden case for a known loop?

Stub the permanent tool error, assert the harness blocks the repeated fingerprint, assert the terminal reason code, and assert no further writes. Keep that case in CI so prompt edits cannot delete the brake.

Start a pilot