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
- Rename duplicate tools so each metadata.name is unique and non-empty
- Remove or rename any tool named caveman_retrieve (it is reserved for the recovery binding)
- 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
- Enforce unique, non-empty tool names at build time
- Never name a tool caveman_retrieve
- Deduplicate wrapped tools before passing them to the runtime
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
- 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/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.namesView on GitHub (pinned to 3ee70a1026)