JuliusBrussee/caveman · error · ValueError
Native executor registration changed
Error message
Native executor registration changed
What it means
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.
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
Example fix
// before tools.append(new_tool) # then reuse existing executor // after executor = make_executor(runtime, scope, tools + [new_tool])
Defensive patterns
Strategy: validation
Validate before calling
snapshot = tuple(tools)
# before each dispatch
if snapshot != tuple(tools):
executor = rebuild_executor(runtime, scope, tools) Type guard
def registration_stable(executor, tools) -> bool:
return tuple(executor.tools) == tuple(tools) Try / catch
try:
tool = executor.selected(call)
except ValueError as e:
executor = rebuild_executor(runtime, scope, current_tools)
tool = executor.selected(call) Prevention
- Keep the tools list immutable for the executor's lifetime
- Pass tuple(tools) copies into registration
- Rebuild the executor whenever the tool set changes
When it happens
Trigger: Mutating the tools tuple/list after constructing the executor (adding/removing tools, changing names) and then dispatching a ToolSelection through selected().
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Caveman delegates through public chat_with_tools methods
- Caveman recovery metadata is immutable
- Caveman recovery tool is immutable
- Expected a native ToolSelection
- Expected an existing native LlamaIndex LLM
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/a6afdffe8f52a724.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/llama_index.py:179
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
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:View on GitHub (pinned to 3ee70a1026)