JuliusBrussee/caveman · error · ValueError

Native tool is not registered

Error message

Native tool is not registered

What it means

After validation, selected() looks up exactly one registered tool whose metadata.name matches call.tool_name; zero or multiple matches raise ValueError because the selection cannot be resolved unambiguously.

Solutions

  1. Only dispatch tool calls whose tool_name matches a registered tool's metadata.name
  2. Re-register/re-create the executor if the tool set changed
  3. Check membership first: any(t.metadata.name == call.tool_name for t in tools)

Example fix

// before
executor.selected(call)  # call.tool_name='get_weather' not registered
// after
names = {t.metadata.name for t in executor.tools}
if call.tool_name in names:
    executor.selected(call)
Defensive patterns

Strategy: validation

Validate before calling

registered = {t.metadata.name for t in executor.tools}
if call.tool_name not in registered:
    raise KeyError(f"tool {call.tool_name!r} is not registered")

Type guard

def is_registered(executor, tool_name: str) -> bool:
    return sum(t.metadata.name == tool_name for t in executor.tools) == 1

Try / catch

try:
    tool = executor.selected(call)
except ValueError as e:
    log.warning("dispatch skipped: %s", e)
    return error_tool_output(call)

Prevention

When it happens

Trigger: Dispatching a tool_id/name that is not in the executor's tools list, or after tools were changed so the name no longer matches.

Common situations: LLM hallucinating a tool name not registered, tools renamed after registration, or reusing an executor across sessions with different tool sets.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/2c3ce02e79842481. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/llama_index.py:184

            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
    tools: tuple[FunctionTool, ...]
    _registration: Any

View on GitHub (pinned to 3ee70a1026)