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
- Add an executor function in `functions` for every tool name in `tools`.
- Remove definitions for tools you have no executor for.
- 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
- Generate the executor map from the same array of tool definitions so they never drift.
- Key executor objects by the exact tool name constant shared with the definition.
- Type the functions param as Record<ToolName, Fn> so TypeScript flags missing keys.
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
- azure Foundry api-version
- azure legacy inference path
- azure request missing api-version
- Duplicate or reserved caveman_retrieve tool name
- Every native function definition needs exactly one executor
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)