JuliusBrussee/caveman · error · TypeError
Expected a native ToolSelection
Error message
Expected a native ToolSelection
What it means
TypeError from the LlamaIndex recovery registration check in selected(): the object passed as the tool call is not a native llama_index ToolSelection (or its tool_id/tool_kwargs have the wrong type/shape). Only genuine native selections can drive recovery, so anything else is rejected before dispatch.
Solutions
- Pass the actual ToolSelection object produced by the LlamaIndex agent loop
- Ensure tool_id is a non-empty string and tool_kwargs is a dict before calling
- If bridging frameworks, construct a proper ToolSelection from the foreign call
Example fix
// before
executor.selected({"tool_id": None, "tool_kwargs": None})
// after
executor.selected(ToolSelection(tool_id=call.id, tool_name=call.name, tool_kwargs=call.arguments or {})) Defensive patterns
Strategy: type-guard
Type guard
from llama_index.core.tools.types import ToolSelection
def is_native_selection(call) -> bool:
return (isinstance(call, ToolSelection)
and isinstance(getattr(call, "tool_id", None), str)
and bool(call.tool_id)
and isinstance(getattr(call, "tool_kwargs", None), dict)) Try / catch
try:
tool = executor.selected(call)
except TypeError as e:
log.error("bad ToolSelection: %s", e)
raise Prevention
- Only pass ToolSelection objects produced by the LlamaIndex agent loop
- Validate tool_id and tool_kwargs when bridging from other frameworks
- Avoid hand-rolled dict stand-ins for ToolSelection
When it happens
Trigger: Calling selected() with None, a raw dict, a ToolCall of another framework, a tool_id that is empty or not a str, or tool_kwargs that is None/list.
Common situations: Bridging from another agent framework's tool-call shape, hand-constructing ToolSelection objects, or deserialization yielding wrong types.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Expected an existing native LlamaIndex LLM
- Native FunctionTool returned an unexpected output
- Agno scope resolver must return a Caveman Scope
- AutoGen requires a stable Caveman Scope for each agent or…
- Caveman delegates through public chat_with_tools methods
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/1712d364c1dc3652.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/llama_index.py:181
return bool(self.binding is not None and invocation is not None and invocation.registration is self
and invocation.recovery_allowed and len(tools) == len(self.tools)
and all(left is right for left, right in zip(tools, self.tools))
and tuple(tool.metadata.name for tool in tools) == self.names
and self.runtime.owns_binding(self.binding, self.scope)
and self.runtime.owns_binding(self.async_binding, self.scope)
and self.tool.fn is self.sync and self.tool.async_fn is self.async_
and self.tool.metadata is self.metadata and not self.tool.partial_params and not self.tool.requires_context
and self.metadata.description == RECOVERY_DESCRIPTION and not self.metadata.return_direct
and self.metadata.get_parameters_dict() == RECOVERY_SCHEMA)
def invocation(self):
return _Invocation(self, self.scope, self.tools, self.successful, True)
def selected(self, call: ToolSelection):
if self.binding is not None and not self.registered(self.tools, self.invocation()):
raise ValueError("Native executor registration changed")
if not isinstance(call, ToolSelection) or not isinstance(call.tool_id, str) or not call.tool_id or not isinstance(call.tool_kwargs, dict):
raise TypeError("Expected a native ToolSelection")
matches = [tool for tool in self.tools if tool.metadata.name == call.tool_name]
if len(matches) != 1:
raise ValueError("Native tool is not registered")
self.successful.pop(call.tool_id, None)
return matches[0]
def completed(self, call, result):
if not isinstance(result, ToolOutput):
raise TypeError("Native FunctionTool returned an unexpected output")
if not result.is_error and call.tool_name != "caveman_retrieve" and len(self.successful) < 4096:
self.successful[call.tool_id] = call.tool_name
return result
@dataclass(frozen=True)
class CavemanLLMTools:
"""Native model and tools for an application-owned loop; no scheduler."""
model: LLMView on GitHub (pinned to 3ee70a1026)