{"record":{"id":"a80f738dbf669ec1","repo":"JuliusBrussee/caveman","slug":"expected-distinct-native-tools-without-caveman-retrieve","errorCode":null,"errorMessage":"Expected distinct native tools without caveman_retrieve","messagePattern":"Expected distinct native tools without caveman_retrieve","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/llama_index.py","lineNumber":148,"sourceCode":"        if (invocation is None or invocation.registration is not self or not invocation.recovery_allowed\n                or len(tools) != len(invocation.tools) or any(a is not b for a, b in zip(tools, invocation.tools))):\n            return False\n        offered = [tool for tool in tools if tool.metadata.name == \"caveman_retrieve\"]\n        return (len(offered) == 1 and offered[0] is self.tool and self.tool.fn is self.sync\n                and self.tool.async_fn is self.async_ and self.tool.metadata is self.metadata\n                and not self.tool.partial_params and self.tool.requires_context\n                and self.metadata.description == RECOVERY_DESCRIPTION and not self.metadata.return_direct\n                and self.metadata.get_parameters_dict() == RECOVERY_SCHEMA)\n\n\nclass _ApplicationTools:\n    \"\"\"Executor registration for a caller-owned loop of native LLM calls.\"\"\"\n    def __init__(self, runtime, scope, tools, *, enabled=True):\n        self.runtime, self.scope, self.successful = runtime, scope, {}\n        selected = tuple(tool if isinstance(tool, FunctionTool) else FunctionTool.from_defaults(tool) for tool in tools)\n        names = [tool.metadata.name for tool in selected]\n        if any(not isinstance(name, str) or not name for name in names) or len(set(names)) != len(names) or \"caveman_retrieve\" in names:\n            raise ValueError(\"Expected distinct native tools without caveman_retrieve\")\n        self.binding = runtime.recovery(scope) if enabled else None\n        self.async_binding = _async_runtime(runtime).recovery(scope) if enabled else None\n        if enabled:\n            def recover(handle: str, offset: int = 0, limit: int = 262144, query: str = \"\"):\n                return json.dumps(self.binding.execute(dict(handle=handle, offset=offset, limit=limit, query=query)), ensure_ascii=False)\n            async def arecover(handle: str, offset: int = 0, limit: int = 262144, query: str = \"\"):\n                return json.dumps(await self.async_binding.execute(dict(handle=handle, offset=offset, limit=limit, query=query)), ensure_ascii=False)\n            self.sync, self.async_ = recover, arecover\n            self.metadata = _FrozenRecoveryMetadata(name=\"caveman_retrieve\", description=RECOVERY_DESCRIPTION, fn_schema=None)\n            self.tool = _FrozenFunctionTool(fn=recover, async_fn=arecover, metadata=self.metadata)\n            selected += (self.tool,)\n        self.tools, self.names = selected, tuple(tool.metadata.name for tool in selected)\n\n    def registered(self, tools, invocation):\n        return bool(self.binding is not None and invocation is not None and invocation.registration is self\n            and invocation.recovery_allowed and len(tools) == len(self.tools)\n            and all(left is right for left, right in zip(tools, self.tools))\n            and tuple(tool.metadata.name for tool in tools) == self.names","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/llama_index.py#L130-L166","documentation":"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.","triggerScenarios":"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.","commonSituations":"Manually renaming a tool to caveman_retrieve, wrapping tools twice so names collide, or building tools programmatically with default/empty names.","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"],"exampleFix":"// before\nllm = caveman.wrap(runtime, scope, tools=[get_weather, get_weather])\n// after\nrain = get_weather.model_copy(deep=True); rain.metadata.name = \"get_rain\"\nllm = caveman.wrap(runtime, scope, tools=[get_weather, rain])","handlingStrategy":"validation","validationCode":"names = [getattr(t.metadata, \"name\", None) for t in tools]\nassert all(isinstance(n, str) and n for n in names), \"all tool names must be non-empty strings\"\nassert len(set(names)) == len(names), \"tool names must be distinct\"\nassert \"caveman_retrieve\" not in names, \"caveman_retrieve is reserved\"","typeGuard":"def valid_tool_names(tools) -> bool:\n    names = [getattr(getattr(t, \"metadata\", None), \"name\", None) for t in tools]\n    return all(isinstance(n, str) and n for n in names) and len(set(names)) == len(names) and \"caveman_retrieve\" not in names","tryCatchPattern":"try:\n    executor = NativeExecutor(runtime, scope, tools)\nexcept ValueError as e:\n    log.error(\"tool registration rejected: %s\", e)","preventionTips":["Enforce unique, non-empty tool names at build time","Never name a tool caveman_retrieve","Deduplicate wrapped tools before passing them to the runtime"],"tags":["python","validation","llamaindex","duplicate-tool"],"backgroundTag":"invalid-argument-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}