JuliusBrussee/caveman · error · ValueError

Create a separate Caveman Strands bundle for each agent

Error message

Create a separate Caveman Strands bundle for each agent

What it means

A Caveman Strands bundle binds itself to exactly one agent via a weakref; init_agent raises ValueError if the bundle is already attached to a different live agent, because one bundle's session scope, recovery tool, and registration cannot serve two agents safely.

Solutions

  1. Create a fresh bundle (model + recovery tool bundle) per agent inside the construction loop
  2. If the same config is reused, factor a factory function that returns a new bundle per agent
  3. If intentionally rebinding, drop the old bundle and build a new one rather than reusing it

Example fix

// before
bundle = make_bundle(rt, scope)
for tenant in tenants:
    agent = Agent(model=bundle.model, tools=[bundle.recovery_tool])
    bundle.init_agent(agent)
// after
for tenant in tenants:
    bundle = make_bundle(rt, scope)
    agent = Agent(model=bundle.model, tools=[bundle.recovery_tool])
    bundle.init_agent(agent)
Defensive patterns

Strategy: try-catch

Validate before calling

if bundle.agent is not None and bundle.agent() is not None and bundle.agent() is not agent:
    bundle = make_fresh_bundle(rt, scope_for(agent))

Try / catch

try:
    bundle.init_agent(agent)
except ValueError as e:
    if "separate Caveman Strands bundle" in str(e):
        bundle = make_fresh_bundle(rt, scope_for(agent))
        bundle.init_agent(agent)
    else:
        raise

Prevention

When it happens

Trigger: Reusing a single bundle/model+tool bundle object across two Strands Agent instances, e.g. constructing agents in a loop while sharing one bundle; re-init after an agent was garbage collected only clears the weakref if the old agent died.

Common situations: Multi-tenant servers creating one agent per request but building the bundle once at startup; tests that reuse a module-level bundle across agent fixtures.

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


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

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/strands.py:186

class _Registration(Plugin):
    name = "caveman:middleware"

    def __init__(self, model):
        self.model, self.agent = model, None
        super().__init__()

        @tool(name="caveman_retrieve", description=RECOVERY_DESCRIPTION, inputSchema={"json": RECOVERY_SCHEMA}, context=True)
        async def recover(handle: str, tool_context: ToolContext, offset: int = 0, limit: int = 262144, query: str = ""):
            if tool_context.cancel_signal.is_set():
                raise asyncio.CancelledError
            scope = _scope(model.scope, tool_context.invocation_state)
            result = await model.runtime.retrieve(scope, handle=handle, offset=offset, limit=limit, query=query)
            return json.dumps(result, ensure_ascii=False, separators=(",", ":"))
        self.recovery_tool = recover

    def init_agent(self, agent):
        if self.agent is not None and self.agent() is not agent:
            raise ValueError("Create a separate Caveman Strands bundle for each agent")
        self.agent = weakref.ref(agent)

    def bound(self, specs):
        agent = self.agent() if self.agent else None
        return (agent is not None and agent.tool_registry.registry.get("caveman_retrieve") is self.recovery_tool
                and type(specs) is list and sum(s.get("name") == "caveman_retrieve" for s in specs if plain(s)) == 1
                and any(s == self.recovery_tool.tool_spec for s in specs))


def with_caveman_model(model, *, runtime, scope):
    return CavemanModel(model, runtime=runtime, scope=scope)


def with_caveman_agent(options: dict, *, runtime, scope) -> dict:
    """Return native Agent constructor options; Strands keeps its own loop."""
    model = CavemanModel(options["model"], runtime=runtime, scope=scope)
    if runtime.mode == "off" or not model.version_supported:
        return {**options, "model": model}

View on GitHub (pinned to 3ee70a1026)