JuliusBrussee/caveman · error · AttributeError

Caveman recovery tool is immutable

Error message

Caveman recovery tool is immutable

What it means

_FrozenFunctionTool is a FunctionTool subclass frozen after construction; assigning any attribute on it raises AttributeError with this message so the caveman recovery tool cannot be altered once registered.

Solutions

  1. Do not mutate the recovery tool; wrap or replace it with your own FunctionTool if customization is needed
  2. Create a new FunctionTool.from_defaults with the desired settings and use that instead
  3. Bypass with object.__setattr__ only as a deliberate last resort

Example fix

// before
caveman_tool.fn = my_wrapper(caveman_tool.fn)
// after
wrapped_tool = FunctionTool.from_defaults(async_fn=my_wrapper(original_fn), name="caveman_retrieve")
Defensive patterns

Strategy: validation

Validate before calling

if getattr(tool, "_caveman_frozen", False):
    raise RuntimeError("cannot mutate caveman recovery tool; build a new FunctionTool instead")

Type guard

def is_frozen_tool(tool) -> bool:
    return getattr(tool, "_caveman_frozen", False)

Prevention

When it happens

Trigger: Setting any attribute on the caveman_retrieve FunctionTool instance after creation, e.g. tool.metadata = ... or tool.custom_flag = True.

Common situations: Framework or user code that renames tools, decorates them, or mutates metadata of all tools in a registry before dispatch.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

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

class _FrozenRecoveryMetadata(_RecoveryMetadata):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        object.__setattr__(self, "_caveman_frozen", True)

    def __setattr__(self, name, value):
        if getattr(self, "_caveman_frozen", False):
            raise AttributeError("Caveman recovery metadata is immutable")
        super().__setattr__(name, value)


class _FrozenFunctionTool(FunctionTool):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        object.__setattr__(self, "_caveman_frozen", True)

    def __setattr__(self, name, value):
        if getattr(self, "_caveman_frozen", False):
            raise AttributeError("Caveman recovery tool is immutable")
        super().__setattr__(name, value)


class _Recovery:
    def __init__(self, runtime, scope):
        self.runtime, self.scope = runtime, scope

        def recover(ctx: Context, handle: str, offset: int = 0, limit: int = 262144, query: str = ""):
            page = runtime.retrieve(_scope(scope, ctx), handle=handle, offset=offset, limit=limit, query=query)
            return json.dumps(page, ensure_ascii=False)

        async def arecover(ctx: Context, handle: str, offset: int = 0, limit: int = 262144, query: str = ""):
            page = await _async_runtime(runtime).retrieve(_scope(scope, ctx), handle=handle, offset=offset, limit=limit, query=query)
            return json.dumps(page, ensure_ascii=False)

        # FunctionTool inspects runtime annotations for its public context injection.
        recover.__annotations__["ctx"] = Context
        arecover.__annotations__["ctx"] = Context

View on GitHub (pinned to 3ee70a1026)