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: str

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Make the repeated call different — change the arguments (pagination cursor, retry backoff marker, added context) so the signature changes.
  2. If the repetition is legitimate (e.g. deliberate retries with backoff), construct Cave.retry_loop_breaker(threshold=N) with a higher threshold.
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/ca67f739852d5523. Report an issue: GitHub.