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
- Create a fresh bundle (model + recovery tool bundle) per agent inside the construction loop
- If the same config is reused, factor a factory function that returns a new bundle per agent
- 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
- Create a new bundle per agent — never share one across Agent instances
- Wrap bundle creation in a factory called inside your per-agent/per-request loop
- Remember the binding is a weakref: a live previous agent still blocks rebinding
- Add an ownership assertion in tests that create multiple agents
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
- A dynamic Strands scope requires the native agent…
- closed
- closed
- Create a separate Caveman Strands bundle for each agent
- Expected a native Strands Model
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)