JuliusBrussee/caveman · error
Create a separate Caveman Strands bundle for each agent
Error message
Create a separate Caveman Strands bundle for each agent
What it means
Each Caveman Strands bundle keeps a WeakRef to exactly one native agent and registers its recovery tool on that agent. `initAgent` in packages/middleware/typescript/src/strands.ts:110 throws this Error if the bundle is re-initialized with a different agent than the one it already holds, since recovery tool registration and dynamic scope resolution are per-agent.
Solutions
- Create a new Caveman Strands bundle (factory call) for each agent.
- Route each agent through its own wrapper instance rather than sharing a singleton.
- If the same agent object is intentionally re-registered, ensure identity matches (deref comparison passes).
Example fix
// before
const shared = cavemanStrands({...});
agentA.model = shared; agentB.model = shared; // throws on second initAgent
// after
agentA.model = cavemanStrands({...});
agentB.model = cavemanStrands({...}); Defensive patterns
Strategy: validation
Validate before calling
const bundles = new WeakMap<object, Bundle>();
function bundleFor(agent: LocalAgent, make: () => Bundle): Bundle {
let b = bundles.get(agent);
if (!b) { b = make(); b.initAgent(agent); bundles.set(agent, b); }
return b;
} Type guard
const isFreshBundle = (b: Bundle, agent: LocalAgent) => (b as any).agent === undefined || (b as any).agent.deref() === agent;
Try / catch
try {
bundle.initAgent(agent);
} catch (e) {
if (e instanceof Error && /separate Caveman Strands bundle/.test(e.message)) {
bundle = cavemanStrands(options); // create a per-agent bundle
bundle.initAgent(agent);
} else throw e;
} Prevention
- Never share a Strands model wrapper across agents; use a factory per agent.
- Avoid module-level singleton wrappers in multi-agent apps.
- Track bundle ownership in a WeakMap keyed by the agent.
When it happens
Trigger: Calling `bundle.initAgent(agentB)` after `bundle.initAgent(agentA)` where agentA !== agentB.
Common situations: A shared wrapper instance passed to multiple agents in an agent-pool or multi-agent app; a caching layer reusing one bundle across sessions; accidentally reusing a module-level singleton bundle.
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…
- Create a separate Caveman Strands bundle for each agent
- cave_budget_cap_breached
- cave_budget_controller_in_use
- cave_budget_controller_unbound
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/242812b7d7c554dd.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/typescript/src/strands.ts:110
}
}catch(error){done=true;observe(attempt,options.cancelSignal?.aborted?'cancelled':'failed');throw error;}
finally{if(!done)observe(attempt,'cancelled');await withOwner(attempt,()=>iterator.return?.());}
}
}
class StrandsRegistration implements Plugin{
readonly name='caveman:middleware';
agent:WeakRef<LocalAgent>|null=null;
readonly recoveryTool:FunctionTool;
constructor(readonly options:StrandsOptions){
this.recoveryTool=new FunctionTool({name:'caveman_retrieve',description:recoveryToolDescription,inputSchema:{...structuredClone(recoveryInputSchema),required:[...recoveryInputSchema.required]} as NonNullable<FunctionToolConfig['inputSchema']>,
callback:async(input,context)=>{
const scope=typeof options.scope==='function'?options.scope(context.agent):options.scope;
return JSON.stringify(await options.runtime.retrieve(scope,input as RetrieveArgs,context.cancelSignal));
}});
}
initAgent(agent:LocalAgent){
if(this.agent&&this.agent.deref()!==agent)throw new Error('Create a separate Caveman Strands bundle for each agent');
this.agent=new WeakRef(agent);
// Registration runs after the host discovers its own tools. A collision
// keeps the host's tool and prevents this model wrapper from using lossiness.
if(this.options.runtime.mode==='compress'&&!agent.toolRegistry.get('caveman_retrieve'))agent.toolRegistry.add(this.recoveryTool);
}
bound(options:StreamOptions):boolean{
const agent=this.agent?.deref(),matches=options.toolSpecs?.filter(t=>t.name==='caveman_retrieve')??[];
return agent?.toolRegistry.get('caveman_retrieve')===this.recoveryTool&&matches.length===1&&JSON.stringify(matches[0])===JSON.stringify(this.recoveryTool.toolSpec);
}
}
export function withCavemanStrandsModel<T extends BaseModelConfig>(model:Model<T>,options:StrandsOptions):Model<T>{
return new CavemanStrandsModel(model,options);
}
export function withCavemanStrands(input:AgentConfig&{model:Model},options:StrandsOptions):AgentConfig{
const model=new CavemanStrandsModel(input.model,options);
if(options.runtime.mode==='off'||!adapterCompatible('strands'))return {...input,model};View on GitHub (pinned to 3ee70a1026)