JuliusBrussee/caveman · error · RetryLoopError
retry loop interrupted: tool call {signature!r} repeated {re
Error message
retry loop interrupted: tool call {signature!r} repeated {repeats} times (threshold {threshold}) What it means
RetryLoopError from RetryLoopBreaker in packages/sdk/python/caveman_cloud/core.py (obtain via Cave.retry_loop_breaker(threshold=3)). The breaker watches every tool call you record; when the identical signature — tool name plus sorted-key JSON of the arguments — repeats more than `threshold` times consecutively, it raises to interrupt a stuck agent loop. Any different call resets the streak, and the breaker fires on the call that would be the (threshold+1)-th consecutive duplicate.
Source
Thrown at packages/sdk/python/caveman_cloud/core.py:82
"""Canonical signature for a tool call (name + sorted-key JSON args)."""
try:
args = json.dumps(arguments, sort_keys=True, separators=(",", ":"))
except (TypeError, ValueError):
args = repr(arguments)
return f"{name}({args})"
def record(self, name: str, arguments: Any) -> None:
"""Record a tool call. Raises :class:`RetryLoopError` once an identical
call has repeated past the threshold. A different call resets the streak.
"""
sig = self.signature(name, arguments)
if sig == self._last_signature:
self._repeats += 1
else:
self._last_signature = sig
self._repeats = 1
if self._repeats > self.threshold:
raise RetryLoopError(sig, self._repeats, self.threshold)
def guard(self, name: str, arguments: Any, fn: Callable[[], Any]) -> Any:
"""Record the call (may raise) then invoke ``fn``."""
self.record(name, arguments)
return fn()
def reset(self) -> None:
"""Clear the streak (e.g. when starting a new task)."""
self._last_signature = None
self._repeats = 0
@dataclass
class Job:
"""Reserved result shape for future durable async-job execution."""
id: str
state: strView on GitHub (pinned to 27d5a3981a)
Solutions
- Make the repeated call different — change the arguments (pagination cursor, retry backoff marker, added context) so the signature changes.
- If the repetition is legitimate (e.g. deliberate retries with backoff), construct Cave.retry_loop_breaker(threshold=N) with a higher threshold.
- Call breaker.reset() when starting a new task or after a successful different call so old streaks do not carry over.
Example fix
# before
breaker = cave.retry_loop_breaker()
for _ in range(5):
result = breaker.guard("search", {"query": "same"}, lambda: do_search("same"))
# after
breaker = cave.retry_loop_breaker()
for attempt in range(5):
result = breaker.guard("search", {"query": "same", "attempt": attempt},
lambda a=attempt: do_search("same", attempt=a)) Defensive patterns
Strategy: try-catch
Try / catch
from caveman_cloud.core import RetryLoopError
try:
breaker.record(tool_name, args)
result = run_tool(args)
except RetryLoopError:
# the loop is stuck on an identical call — change strategy, don't retry as-is
breaker.reset()
result = escalate_to_model(tool_name, args, hint="previous identical call repeated") Prevention
- Vary arguments on every retry (attempt counter, cursor, backoff marker) so signatures differ.
- Call breaker.reset() at task boundaries so legitimate repeats in a new task are not accumulated into the streak.
- Tune threshold via Cave.retry_loop_breaker(threshold=N) when deliberate identical retries are expected.
When it happens
Trigger: Calling breaker.record(name, args) or breaker.guard(name, args, fn) with the exact same name and arguments 4+ times in a row at the default threshold of 3 — typically an agent retrying a failing tool with unchanged arguments, or a polling loop whose query never changes.
Common situations: Agent loops that retry on error without changing the request; a tool returning 'not found' while the caller keeps asking identically; forgetting reset() between logically distinct tasks that happen to issue the same first call.
Related errors
- {name} must not contain surrounding whitespace
- {name} must be an absolute http(s) URL
- {name} must be an absolute http(s) URL without credentials
- cave_breaker_retry_requires_budget
- cave_breaker_retry_spend_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/ca67f739852d5523.
Report an issue: GitHub.