JuliusBrussee/caveman · error · ValueError

Expected distinct native tools without caveman_retrieve

Error message

Expected distinct native tools without caveman_retrieve

What it means

The native LlamaIndex executor registration validates that the user-supplied tool list has unique, non-empty string names and does not contain the reserved name caveman_retrieve; otherwise ValueError is raised.

Solutions

  1. Rename duplicate tools so each metadata.name is unique and non-empty
  2. Remove or rename any tool named caveman_retrieve (it is reserved for the recovery binding)
  3. Verify names with [t.metadata.name for t in tools] before registration

Example fix

// before
llm = caveman.wrap(runtime, scope, tools=[get_weather, get_weather])
// after
rain = get_weather.model_copy(deep=True); rain.metadata.name = "get_rain"
llm = caveman.wrap(runtime, scope, tools=[get_weather, rain])
Defensive patterns

Strategy: validation

Validate before calling

names = [getattr(t.metadata, "name", None) for t in tools]
assert all(isinstance(n, str) and n for n in names), "all tool names must be non-empty strings"
assert len(set(names)) == len(names), "tool names must be distinct"
assert "caveman_retrieve" not in names, "caveman_retrieve is reserved"

Type guard

def valid_tool_names(tools) -> bool:
    names = [getattr(getattr(t, "metadata", None), "name", None) for t in tools]
    return all(isinstance(n, str) and n for n in names) and len(set(names)) == len(names) and "caveman_retrieve" not in names

Try / catch

try:
    executor = NativeExecutor(runtime, scope, tools)
except ValueError as e:
    log.error("tool registration rejected: %s", e)

Prevention

When it happens

Trigger: Passing two tools with the same metadata.name, a tool whose metadata.name is empty/non-string, or a tool explicitly named caveman_retrieve to the executor registration.

Common situations: Manually renaming a tool to caveman_retrieve, wrapping tools twice so names collide, or building tools programmatically with default/empty names.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

        if (invocation is None or invocation.registration is not self or not invocation.recovery_allowed
                or len(tools) != len(invocation.tools) or any(a is not b for a, b in zip(tools, invocation.tools))):
            return False
        offered = [tool for tool in tools if tool.metadata.name == "caveman_retrieve"]
        return (len(offered) == 1 and offered[0] is self.tool 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 self.tool.requires_context
                and self.metadata.description == RECOVERY_DESCRIPTION and not self.metadata.return_direct
                and self.metadata.get_parameters_dict() == RECOVERY_SCHEMA)


class _ApplicationTools:
    """Executor registration for a caller-owned loop of native LLM calls."""
    def __init__(self, runtime, scope, tools, *, enabled=True):
        self.runtime, self.scope, self.successful = runtime, scope, {}
        selected = tuple(tool if isinstance(tool, FunctionTool) else FunctionTool.from_defaults(tool) for tool in tools)
        names = [tool.metadata.name for tool in selected]
        if any(not isinstance(name, str) or not name for name in names) or len(set(names)) != len(names) or "caveman_retrieve" in names:
            raise ValueError("Expected distinct native tools without caveman_retrieve")
        self.binding = runtime.recovery(scope) if enabled else None
        self.async_binding = _async_runtime(runtime).recovery(scope) if enabled else None
        if enabled:
            def recover(handle: str, offset: int = 0, limit: int = 262144, query: str = ""):
                return json.dumps(self.binding.execute(dict(handle=handle, offset=offset, limit=limit, query=query)), ensure_ascii=False)
            async def arecover(handle: str, offset: int = 0, limit: int = 262144, query: str = ""):
                return json.dumps(await self.async_binding.execute(dict(handle=handle, offset=offset, limit=limit, query=query)), ensure_ascii=False)
            self.sync, self.async_ = recover, arecover
            self.metadata = _FrozenRecoveryMetadata(name="caveman_retrieve", description=RECOVERY_DESCRIPTION, fn_schema=None)
            self.tool = _FrozenFunctionTool(fn=recover, async_fn=arecover, metadata=self.metadata)
            selected += (self.tool,)
        self.tools, self.names = selected, tuple(tool.metadata.name for tool in selected)

    def registered(self, tools, invocation):
        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

View on GitHub (pinned to 3ee70a1026)