{"record":{"id":"ca67f739852d5523","repo":"JuliusBrussee/caveman","slug":"retry-loop-interrupted-tool-call-signature-r-re","errorCode":null,"errorMessage":"retry loop interrupted: tool call {signature!r} repeated {repeats} times (threshold {threshold})","messagePattern":"retry loop interrupted: tool call (.+?) repeated (.+?) times \\(threshold (.+?)\\)","errorType":"exception","errorClass":"RetryLoopError","httpStatus":null,"severity":"error","filePath":"packages/sdk/python/caveman_cloud/core.py","lineNumber":82,"sourceCode":"        \"\"\"Canonical signature for a tool call (name + sorted-key JSON args).\"\"\"\n        try:\n            args = json.dumps(arguments, sort_keys=True, separators=(\",\", \":\"))\n        except (TypeError, ValueError):\n            args = repr(arguments)\n        return f\"{name}({args})\"\n\n    def record(self, name: str, arguments: Any) -> None:\n        \"\"\"Record a tool call. Raises :class:`RetryLoopError` once an identical\n        call has repeated past the threshold. A different call resets the streak.\n        \"\"\"\n        sig = self.signature(name, arguments)\n        if sig == self._last_signature:\n            self._repeats += 1\n        else:\n            self._last_signature = sig\n            self._repeats = 1\n        if self._repeats > self.threshold:\n            raise RetryLoopError(sig, self._repeats, self.threshold)\n\n    def guard(self, name: str, arguments: Any, fn: Callable[[], Any]) -> Any:\n        \"\"\"Record the call (may raise) then invoke ``fn``.\"\"\"\n        self.record(name, arguments)\n        return fn()\n\n    def reset(self) -> None:\n        \"\"\"Clear the streak (e.g. when starting a new task).\"\"\"\n        self._last_signature = None\n        self._repeats = 0\n\n\n@dataclass\nclass Job:\n    \"\"\"Reserved result shape for future durable async-job execution.\"\"\"\n\n    id: str\n    state: str","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/sdk/python/caveman_cloud/core.py#L64-L100","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nbreaker = cave.retry_loop_breaker()\nfor _ in range(5):\n    result = breaker.guard(\"search\", {\"query\": \"same\"}, lambda: do_search(\"same\"))\n# after\nbreaker = cave.retry_loop_breaker()\nfor attempt in range(5):\n    result = breaker.guard(\"search\", {\"query\": \"same\", \"attempt\": attempt},\n                           lambda a=attempt: do_search(\"same\", attempt=a))","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"from caveman_cloud.core import RetryLoopError\n\ntry:\n    breaker.record(tool_name, args)\n    result = run_tool(args)\nexcept RetryLoopError:\n    # the loop is stuck on an identical call — change strategy, don't retry as-is\n    breaker.reset()\n    result = escalate_to_model(tool_name, args, hint=\"previous identical call repeated\")","preventionTips":["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."],"tags":["python","sdk","retry","agent-loop","runtime"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}