JuliusBrussee/caveman · warning · MiddlewareError
invalid_recovery_arguments
invalid_recovery_arguments
Error message
invalid_recovery_arguments
What it means
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.
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.
Example fix
// before
await workbench.call_tool("caveman_recovery", {"action": "retry"}) # missing handle
// after
await workbench.call_tool("caveman_recovery", {"handle": handle_id, "action": "retry"}) Defensive patterns
Strategy: validation
Validate before calling
def valid_recovery_args(args) -> bool:
keys = set(args or {})
return "handle" in keys and keys <= set(RECOVERY_SCHEMA["properties"]) Try / catch
result = await workbench.call_tool("caveman_recovery", args)
if result.is_error:
err = json.loads(result.result[0].content).get("error", {})
if err.get("code") == "invalid_recovery_arguments":
# re-prompt model or refresh handle and retry with schema-valid args
retry_args = {k: v for k, v in args.items() if k in RECOVERY_SCHEMA["properties"]} | {"handle": fresh_handle}
result = await workbench.call_tool("caveman_recovery", retry_args) Prevention
- 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.
When it happens
Trigger: 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'.
Common situations: 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.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- artifact_id is required
- ASGI context resolver or request bounds are invalid
- assemble requires model and session_id
- assembly slot id must be non-empty and unique
- assembly slot has unknown stability
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/50f262184631935b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/autogen.py:412
return [*tools, self.recovery_schema] if self.recovery_enabled else tools
async def call_tool(self, name, arguments=None, cancellation_token=None, call_id=None):
# Refresh dynamic registries before deciding which implementation owns
# the name. A collision always belongs to the original workbench.
await self.list_tools()
if name != "caveman_retrieve" or not self.recovery_enabled:
for workbench in self.delegates:
if any(tool["name"] == name for tool in await workbench.list_tools()):
return await workbench.call_tool(name, arguments, cancellation_token, call_id)
if self.delegates:
return await self.delegates[0].call_tool(name, arguments, cancellation_token, call_id)
return ToolResult(name=name, result=[TextResultContent(content=f"Tool {name} not found.")], is_error=True)
cancellation = _Cancellation(cancellation_token)
cancellation.check()
try:
args = dict(arguments or {})
if set(args) - set(RECOVERY_SCHEMA["properties"]) or "handle" not in args:
raise MiddlewareError("invalid_recovery_arguments")
result = await cancellation.wait(self.binding.execute(args))
return ToolResult(name=name, result=[TextResultContent(content=json.dumps(result, ensure_ascii=False, separators=(",", ":")))])
except MiddlewareError as error:
return ToolResult(name=name, result=[TextResultContent(content=json.dumps({"error": {"code": error.code}}))], is_error=True)
except (TypeError, ValueError):
return ToolResult(name=name, result=[TextResultContent(content='{"error":{"code":"invalid_recovery_arguments"}}')], is_error=True)
async def call_tool_stream(self, name, arguments=None, cancellation_token=None, call_id=None):
await self.list_tools()
workbench = None
if name != "caveman_retrieve" or not self.recovery_enabled:
for candidate in self.delegates:
if any(tool["name"] == name for tool in await candidate.list_tools()):
workbench = candidate
break
if not isinstance(workbench, StaticStreamWorkbench):
yield await self.call_tool(name, arguments, cancellation_token, call_id)
returnView on GitHub (pinned to 3ee70a1026)