JuliusBrussee/caveman · error · TypeError

Every native function definition needs exactly one executor

Error message

Every native function definition needs exactly one executor

What it means

This wrapper enforces a strict 1:1 mapping between tool definitions and executor functions. In packages/middleware/typescript/src/openai.ts:33 it throws a TypeError when the number of tool names differs from the number of keys in `options.functions`, or when any name in `tools` has no corresponding function executor (or the executor is not a function).

Solutions

  1. Add an executor function in `functions` for every tool name in `tools`.
  2. Remove definitions for tools you have no executor for.
  3. Generate `functions` from the same source as `tools` so keys stay in sync.

Example fix

// before
withCavemanOpenAITools(client, { protocol: 'openai-chat', tools: [getWeatherDef], functions: {} });
// after
withCavemanOpenAITools(client, { protocol: 'openai-chat', tools: [getWeatherDef], functions: { getWeather: async (args) => fetchWeather(args) } });
Defensive patterns

Strategy: validation

Validate before calling

function assertExecutorsMatch(tools: { name?: string; function?: { name?: string } }[], functions: Record<string, unknown>) {
  const names = tools.map(t => 'function' in t ? t.function?.name : t.name) as string[];
  const missing = names.filter(n => typeof functions[n] !== 'function');
  if (names.length !== Object.keys(functions).length || missing.length) {
    throw new Error(`Executor mismatch; missing: ${missing.join(', ')}`);
  }
}

Type guard

const hasExecutorFor = (functions: Record<string, unknown>, name: string): functions is Record<string, Function> =>
  typeof functions[name] === 'function';

Try / catch

try {
  const loop = withCavemanOpenAITools(client, options);
} catch (e) {
  if (e instanceof TypeError && /exactly one executor/.test(e.message)) {
    console.error('tools and functions must be 1:1; check names and keys.');
    throw e;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `withCavemanOpenAITools` where `Object.keys(options.functions).length !== tools.length`, or `typeof options.functions[someToolName] !== 'function'` (undefined or non-callable).

Common situations: Adding a tool definition but forgetting to add its executor; typos in function keys vs tool names; passing executor maps built for a different protocol variant; refactors renaming a tool but not its key.

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/b7383b148db1ed1b. Report an issue: GitHub.

Appendix: source

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

type Functions = Readonly<Record<string, (input: unknown) => unknown | Promise<unknown>>>;
export interface OpenAIToolLoop<T extends OpenAI, Tool> {
  readonly client: T;
  /** A fresh native definition list; changing it cannot change registration. */
  readonly tools: Tool[];
  /** Use this immutable table for every application-owned function dispatch. */
  readonly functions: Functions;
}
type ChatFunction = OpenAI.Chat.Completions.ChatCompletionFunctionTool;
type ResponseFunction = OpenAI.Responses.FunctionTool;
export function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-chat';tools:ChatFunction[];functions:Functions}):OpenAIToolLoop<T,ChatFunction>;
export function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-responses';tools:ResponseFunction[];functions:Functions}):OpenAIToolLoop<T,ResponseFunction>;
/** Native application-owned Chat/Responses calls, with no added scheduler. */
export function withCavemanOpenAITools<T extends OpenAI>(client:T,options:OpenAIOptions & {protocol:'openai-chat'|'openai-responses';tools:(ChatFunction|ResponseFunction)[];functions:Functions}):OpenAIToolLoop<T,ChatFunction|ResponseFunction>{
  const definitions=structuredClone(options.tools);
  const names=definitions.map(tool=>'function' in tool?tool.function.name:tool.name);
  if(!plain(options.functions)||names.some(name=>typeof name!=='string'||!name)||new Set(names).size!==names.length||names.includes('caveman_retrieve')||'caveman_retrieve' in options.functions)throw new TypeError('Duplicate or reserved caveman_retrieve tool name');
  if(names.length!==Object.keys(options.functions).length||names.some(name=>typeof options.functions[name]!=='function'))throw new TypeError('Every native function definition needs exactly one executor');
  let functions=Object.freeze({...options.functions});
  let context:RecoveryContext|undefined;
  if(options.runtime.mode==='compress'&&frameworkCompatible('openai',VERSION)){
    const binding=options.runtime.recovery(options.scope);
    const schema={name:binding.name,description:binding.description,parameters:binding.inputSchema};
    const definition=options.protocol==='openai-chat'?{type:'function' as const,function:schema}:{type:'function' as const,...schema,strict:false};
    definitions.push(definition);
    const execute=(input:unknown)=>binding.execute(input as never);
    functions=Object.freeze({...functions,[binding.name]:execute});
    context={runtime:options.runtime,scope:options.scope,binding,overhead:JSON.stringify(definition),logicalCallId:crypto.randomUUID(),
      isRegistered:()=>options.runtime.ownsBinding(binding,options.scope)&&functions[binding.name]===execute};
  }
  const serialized=JSON.stringify(definitions);
  return Object.freeze({client:wrapOpenAI(client,options,context,options.protocol),functions,get tools(){return JSON.parse(serialized);}});
}

/** A native withOptions clone; APIPromise, parsers, streams and runners survive. */
export function withCavemanOpenAI<T extends OpenAI>(client:T,options:OpenAIOptions):T{

View on GitHub (pinned to 3ee70a1026)