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

  1. Create a new Caveman Strands bundle (factory call) for each agent.
  2. Route each agent through its own wrapper instance rather than sharing a singleton.
  3. 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

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


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)