JuliusBrussee/caveman · error

A dynamic Strands scope requires the native agent…

Error message

A dynamic Strands scope requires the native agent registration

What it means

When a Strands bundle is configured with a dynamic `scope` (a function), the model wrapper must resolve that function against the native Strands agent instance it is registered with. `private scope()` in packages/middleware/typescript/src/strands.ts:33 dereferences a stored WeakRef; if the agent was never registered via `initAgent` or has been garbage-collected, it throws this Error instead of calling the scope callback with undefined.

Solutions

  1. Call `initAgent(agent)` on the bundle with the native Strands agent before streaming.
  2. Keep a strong reference to the agent for the wrapper's lifetime so the WeakRef is not collected.
  3. Use one bundle per agent (a static scope avoids the registration requirement entirely).

Example fix

// before
const model = cavemanStrands({ runtime, scope: (agent) => `agent:${agent.id}` });
await model.stream(...); // throws
// after
const model = cavemanStrands({ runtime, scope: (agent) => `agent:${agent.id}` } as any);
model.initAgent(myStrandsAgent);
await model.stream(...);
Defensive patterns

Strategy: type-guard

Validate before calling

// Before streaming with a dynamic scope:
const bundle = createCavemanStrandsModel(options);
bundle.initAgent(agent); // must precede any stream that resolves a function-valued scope
if (typeof options.scope === 'function' && !bundle.isBound()) throw new Error('call initAgent before streaming');

Type guard

const canResolveDynamicScope = (b: { bound(o?: unknown): boolean }, opts: { scope: unknown }) =>
  typeof opts.scope !== 'function' || b.bound();

Try / catch

try {
  await model.stream(messages, opts);
} catch (e) {
  if (e instanceof Error && /native agent registration/.test(e.message)) {
    console.error('Register the wrapper with initAgent(agent) before using a dynamic scope.');
    throw e;
  }
  throw e;
}

Prevention

When it happens

Trigger: Using a function-valued `options.scope` while streaming before `initAgent(agent)` has been called, or after the WeakRef target was collected; constructing a wrapper and using it detached from any agent.

Common situations: Dynamic scopes driven by agent context but the wrapper is shared/reused across agents without registration; long-lived wrappers where the agent became unreachable; forgetting to wire `initAgent` in a custom host.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at packages/middleware/typescript/src/strands.ts:33

    cache_read_tokens:count(value.cacheReadInputTokens),cache_write_tokens:count(value.cacheWriteInputTokens),reasoning_tokens:null};
}

/** Native streamAggregated and agent orchestration remain in Strands. */
export class CavemanStrandsModel<T extends BaseModelConfig=BaseModelConfig> extends Model<T>{
  registration:StrandsRegistration|null=null;
  private readonly versionSupported:boolean;
  constructor(readonly inner:Model<T>,readonly options:StrandsOptions){
    super();this.versionSupported=adapterCompatible('strands');
    if(!this.versionSupported&&options.runtime.mode!=='off')options.runtime.decline('unsupported_version');
  }
  override get stateful(){return this.inner.stateful;}
  updateConfig(config:T){this.inner.updateConfig(config);}
  getConfig():T{return this.inner.getConfig();}
  override countTokens(messages:Message[],options?:CountTokensOptions){return this.inner.countTokens(messages,options);}
  private scope():Scope{
    if(typeof this.options.scope!=='function')return this.options.scope;
    const agent=this.registration?.agent?.deref();
    if(!agent)throw new Error('A dynamic Strands scope requires the native agent registration');
    return this.options.scope(agent);
  }

  private async prepare(messages:Message[],options:StreamOptions):Promise<{messages:Message[];attempt:Attempt|null}>{
    if(options.cancelSignal?.aborted&&!currentOwner())this.options.runtime.report(null,{reason:'cancelled',adapter:adapter.id});
    options.cancelSignal?.throwIfAborted();
    if(currentOwner())return {messages,attempt:null};
    const passive=(reason:string)=>({messages,attempt:{runtime:this.options.runtime,
      scope:{namespace:'caveman-report',session_id:'passive',branch_id:'main',cache_epoch:'0'},
      logicalCallId:crypto.randomUUID(),attemptId:crypto.randomUUID(),optimization:null,wireSHA256:null,passive:true,reason,adapter:adapter.id}});
    if(this.options.runtime.mode==='off')return passive('disabled');
    if(!this.versionSupported)return passive('unsupported_version');
    if(this.stateful)return passive('opaque_context');
    const context=await manifest([{system:options.systemPrompt??null},...messages.map(m=>m.toJSON())]);
    if(!context)return passive('unsupported_shape');
    const names=new Map<string,string>();
    for(const message of messages)for(const block of message.content)if(block.type==='toolUseBlock')names.set(block.toolUseId,block.name);
    const candidates:Candidate[]=[],paths=new Map<string,{mi:number;bi:number;pi:number}>();

View on GitHub (pinned to 3ee70a1026)