{"record":{"id":"50f262184631935b","repo":"JuliusBrussee/caveman","slug":"invalid-recovery-arguments","errorCode":"invalid_recovery_arguments","errorMessage":"invalid_recovery_arguments","messagePattern":"invalid_recovery_arguments","errorType":"error_code","errorClass":"MiddlewareError","httpStatus":null,"severity":"warning","filePath":"packages/middleware/python/caveman_middleware/autogen.py","lineNumber":412,"sourceCode":"        return [*tools, self.recovery_schema] if self.recovery_enabled else tools\n\n    async def call_tool(self, name, arguments=None, cancellation_token=None, call_id=None):\n        # Refresh dynamic registries before deciding which implementation owns\n        # the name. A collision always belongs to the original workbench.\n        await self.list_tools()\n        if name != \"caveman_retrieve\" or not self.recovery_enabled:\n            for workbench in self.delegates:\n                if any(tool[\"name\"] == name for tool in await workbench.list_tools()):\n                    return await workbench.call_tool(name, arguments, cancellation_token, call_id)\n            if self.delegates:\n                return await self.delegates[0].call_tool(name, arguments, cancellation_token, call_id)\n            return ToolResult(name=name, result=[TextResultContent(content=f\"Tool {name} not found.\")], is_error=True)\n        cancellation = _Cancellation(cancellation_token)\n        cancellation.check()\n        try:\n            args = dict(arguments or {})\n            if set(args) - set(RECOVERY_SCHEMA[\"properties\"]) or \"handle\" not in args:\n                raise MiddlewareError(\"invalid_recovery_arguments\")\n            result = await cancellation.wait(self.binding.execute(args))\n            return ToolResult(name=name, result=[TextResultContent(content=json.dumps(result, ensure_ascii=False, separators=(\",\", \":\")))])\n        except MiddlewareError as error:\n            return ToolResult(name=name, result=[TextResultContent(content=json.dumps({\"error\": {\"code\": error.code}}))], is_error=True)\n        except (TypeError, ValueError):\n            return ToolResult(name=name, result=[TextResultContent(content='{\"error\":{\"code\":\"invalid_recovery_arguments\"}}')], is_error=True)\n\n    async def call_tool_stream(self, name, arguments=None, cancellation_token=None, call_id=None):\n        await self.list_tools()\n        workbench = None\n        if name != \"caveman_retrieve\" or not self.recovery_enabled:\n            for candidate in self.delegates:\n                if any(tool[\"name\"] == name for tool in await candidate.list_tools()):\n                    workbench = candidate\n                    break\n        if not isinstance(workbench, StaticStreamWorkbench):\n            yield await self.call_tool(name, arguments, cancellation_token, call_id)\n            return","sourceCodeStart":394,"sourceCodeEnd":430,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/autogen.py#L394-L430","documentation":"call_tool executes a recovery action on the middleware binding using arguments supplied by the agent through the recovery tool schema. If the argument dict contains keys outside RECOVERY_SCHEMA['properties'] or lacks the required 'handle' key, the middleware raises MiddlewareError('invalid_recovery_arguments'); the handler converts it into a ToolResult with is_error=True rather than crashing the agent loop.","triggerScenarios":"An LLM hallucinating extra/unknown parameters for the recovery tool; omitting the required 'handle' field; supplying malformed values that make binding.execute raise TypeError/ValueError; calling call_tool directly with hand-built arguments missing 'handle'.","commonSituations":"Small/weak models inventing parameters not in the JSON schema; stale recovery handles after a restart; agents reusing recovery args from a previous turn; hand-written tool calls in tests.","solutions":["Inspect the returned ToolResult's error JSON and re-prompt the model to call the tool with only schema-defined parameters plus a valid 'handle'.","Ensure the agent is bound to the RECOVERY_SCHEMA (set via _schema()); mis-bound or custom schemas cause key mismatches.","Refresh the recovery handle by triggering a new recovery session; old handles may be invalid after middleware restart.","In tests, validate your args dict against RECOVERY_SCHEMA['properties'] before calling call_tool."],"exampleFix":"// before\nawait workbench.call_tool(\"caveman_recovery\", {\"action\": \"retry\"})  # missing handle\n// after\nawait workbench.call_tool(\"caveman_recovery\", {\"handle\": handle_id, \"action\": \"retry\"})","handlingStrategy":"validation","validationCode":"def valid_recovery_args(args) -> bool:\n    keys = set(args or {})\n    return \"handle\" in keys and keys <= set(RECOVERY_SCHEMA[\"properties\"])","typeGuard":null,"tryCatchPattern":"result = await workbench.call_tool(\"caveman_recovery\", args)\nif result.is_error:\n    err = json.loads(result.result[0].content).get(\"error\", {})\n    if err.get(\"code\") == \"invalid_recovery_arguments\":\n        # re-prompt model or refresh handle and retry with schema-valid args\n        retry_args = {k: v for k, v in args.items() if k in RECOVERY_SCHEMA[\"properties\"]} | {\"handle\": fresh_handle}\n        result = await workbench.call_tool(\"caveman_recovery\", retry_args)","preventionTips":["Always validate tool arguments against RECOVERY_SCHEMA before invoking.","Refresh recovery handles after middleware restarts; stale handles cause schema/execute failures.","Use structured output (strict schema) so models cannot invent extra parameters.","In tests, assert valid_recovery_args(args) before calling call_tool."],"tags":["python","autogen","tool-call","recovery","validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}