{"record":{"id":"e2b4168663b91ee3","repo":"JuliusBrussee/caveman","slug":"create-a-separate-caveman-strands-bundle-for-each-agent-e2b416","errorCode":null,"errorMessage":"Create a separate Caveman Strands bundle for each agent","messagePattern":"Create a separate Caveman Strands bundle for each agent","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/strands.py","lineNumber":186,"sourceCode":"class _Registration(Plugin):\n    name = \"caveman:middleware\"\n\n    def __init__(self, model):\n        self.model, self.agent = model, None\n        super().__init__()\n\n        @tool(name=\"caveman_retrieve\", description=RECOVERY_DESCRIPTION, inputSchema={\"json\": RECOVERY_SCHEMA}, context=True)\n        async def recover(handle: str, tool_context: ToolContext, offset: int = 0, limit: int = 262144, query: str = \"\"):\n            if tool_context.cancel_signal.is_set():\n                raise asyncio.CancelledError\n            scope = _scope(model.scope, tool_context.invocation_state)\n            result = await model.runtime.retrieve(scope, handle=handle, offset=offset, limit=limit, query=query)\n            return json.dumps(result, ensure_ascii=False, separators=(\",\", \":\"))\n        self.recovery_tool = recover\n\n    def init_agent(self, agent):\n        if self.agent is not None and self.agent() is not agent:\n            raise ValueError(\"Create a separate Caveman Strands bundle for each agent\")\n        self.agent = weakref.ref(agent)\n\n    def bound(self, specs):\n        agent = self.agent() if self.agent else None\n        return (agent is not None and agent.tool_registry.registry.get(\"caveman_retrieve\") is self.recovery_tool\n                and type(specs) is list and sum(s.get(\"name\") == \"caveman_retrieve\" for s in specs if plain(s)) == 1\n                and any(s == self.recovery_tool.tool_spec for s in specs))\n\n\ndef with_caveman_model(model, *, runtime, scope):\n    return CavemanModel(model, runtime=runtime, scope=scope)\n\n\ndef with_caveman_agent(options: dict, *, runtime, scope) -> dict:\n    \"\"\"Return native Agent constructor options; Strands keeps its own loop.\"\"\"\n    model = CavemanModel(options[\"model\"], runtime=runtime, scope=scope)\n    if runtime.mode == \"off\" or not model.version_supported:\n        return {**options, \"model\": model}","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/strands.py#L168-L204","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nbundle = make_bundle(rt, scope)\nfor tenant in tenants:\n    agent = Agent(model=bundle.model, tools=[bundle.recovery_tool])\n    bundle.init_agent(agent)\n// after\nfor tenant in tenants:\n    bundle = make_bundle(rt, scope)\n    agent = Agent(model=bundle.model, tools=[bundle.recovery_tool])\n    bundle.init_agent(agent)","handlingStrategy":"try-catch","validationCode":"if bundle.agent is not None and bundle.agent() is not None and bundle.agent() is not agent:\n    bundle = make_fresh_bundle(rt, scope_for(agent))","typeGuard":null,"tryCatchPattern":"try:\n    bundle.init_agent(agent)\nexcept ValueError as e:\n    if \"separate Caveman Strands bundle\" in str(e):\n        bundle = make_fresh_bundle(rt, scope_for(agent))\n        bundle.init_agent(agent)\n    else:\n        raise","preventionTips":["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"],"tags":["python","strands","lifecycle","one-to-one-binding"],"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"}