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

  1. Pass the actual ToolSelection object produced by the LlamaIndex agent loop
  2. Ensure tool_id is a non-empty string and tool_kwargs is a dict before calling
  3. 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

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


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

View on GitHub (pinned to 3ee70a1026)