{"record":{"id":"a6afdffe8f52a724","repo":"JuliusBrussee/caveman","slug":"native-executor-registration-changed","errorCode":null,"errorMessage":"Native executor registration changed","messagePattern":"Native executor registration changed","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/llama_index.py","lineNumber":179,"sourceCode":"\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\n            and self.runtime.owns_binding(self.binding, self.scope)\n            and self.runtime.owns_binding(self.async_binding, self.scope)\n            and self.tool.fn is self.sync and self.tool.async_fn is self.async_\n            and self.tool.metadata is self.metadata and not self.tool.partial_params and not 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    def invocation(self):\n        return _Invocation(self, self.scope, self.tools, self.successful, True)\n\n    def selected(self, call: ToolSelection):\n        if self.binding is not None and not self.registered(self.tools, self.invocation()):\n            raise ValueError(\"Native executor registration changed\")\n        if not isinstance(call, ToolSelection) or not isinstance(call.tool_id, str) or not call.tool_id or not isinstance(call.tool_kwargs, dict):\n            raise TypeError(\"Expected a native ToolSelection\")\n        matches = [tool for tool in self.tools if tool.metadata.name == call.tool_name]\n        if len(matches) != 1:\n            raise ValueError(\"Native tool is not registered\")\n        self.successful.pop(call.tool_id, None)\n        return matches[0]\n\n    def completed(self, call, result):\n        if not isinstance(result, ToolOutput):\n            raise TypeError(\"Native FunctionTool returned an unexpected output\")\n        if not result.is_error and call.tool_name != \"caveman_retrieve\" and len(self.successful) < 4096:\n            self.successful[call.tool_id] = call.tool_name\n        return result\n\n\n@dataclass(frozen=True)\nclass CavemanLLMTools:","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/llama_index.py#L161-L197","documentation":"On each selected() call the executor re-checks that its tool list still registers identically (via registered()). If the tool set or invocation signature changed since construction, ValueError is raised because the cached recovery binding no longer matches the live tools.","triggerScenarios":"Mutating the tools tuple/list after constructing the executor (adding/removing tools, changing names) and then dispatching a ToolSelection through selected().","commonSituations":"Dynamic tool registries that add tools mid-session, code that mutates shared tool lists between turns, or replacing a tool's metadata name in place.","solutions":["Keep the tools list immutable after registration; build a new executor when tools change","Pass a copy (tuple(tools)) so accidental external mutation is detected rather than corrupting state","Re-create the wrapped LLM/executor with the updated tool list instead of editing the old one"],"exampleFix":"// before\ntools.append(new_tool)  # then reuse existing executor\n// after\nexecutor = make_executor(runtime, scope, tools + [new_tool])","handlingStrategy":"validation","validationCode":"snapshot = tuple(tools)\n# before each dispatch\nif snapshot != tuple(tools):\n    executor = rebuild_executor(runtime, scope, tools)","typeGuard":"def registration_stable(executor, tools) -> bool:\n    return tuple(executor.tools) == tuple(tools)","tryCatchPattern":"try:\n    tool = executor.selected(call)\nexcept ValueError as e:\n    executor = rebuild_executor(runtime, scope, current_tools)\n    tool = executor.selected(call)","preventionTips":["Keep the tools list immutable for the executor's lifetime","Pass tuple(tools) copies into registration","Rebuild the executor whenever the tool set changes"],"tags":["python","llamaindex","state-invalidation"],"backgroundTag":"invalid-state-transition","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"}