01 · Problem
What is it for?
A model can request work, but application code must validate arguments, resolve a registered function and return its result.
02 · Mental model
How should you think about it?
Treat the model as a planner whose output is untrusted input. A function schema describes a contract; a handler performs the work.
Runs a bounded, validated function-call conversation with an injectable provider. Provider calls are mocked in CI; flat schemas are supported. Real provider compatibility and application authorization remain integration concerns.
03 · Control flow
What happens in which order?
- Append the user message to conversation history.
- Send public function schemas to the model.
- Resolve requested names through a registry and validate arguments.
- Execute the handler and append a function result.
- Stop at a final answer or a strict step limit.
04 · Building blocks
The important contracts
query(message, signal?)Execute a bounded conversation and validated local handlers.AIFunctionCallerOptionsInject the provider, model and step limit.05 · Build it yourself
Implement a small core
Start with this small independent implementation, then use the steps below to make its contract explicit.
- Append the user message to conversation history.
- Send public function schemas to the model.
- Resolve requested names through a registry and validate arguments.
- Execute the handler and append a function result.
- Stop at a final answer or a strict step limit.
type Tool = { schema: object; run(input: unknown): Promise<unknown> };
async function runAgent(input: string, signal: AbortSignal) {
const messages = [{ role: "user", content: input }];
for (let step = 0; step < 6; step++) {
const reply = await model.complete(messages, signal);
messages.push(reply.message);
if (!reply.toolCalls.length) return reply.message.content;
for (const call of reply.toolCalls) {
const tool = tools.get(call.name);
if (!tool) throw new Error("Unknown tool: " + call.name);
const args = validate(tool.schema, JSON.parse(call.arguments));
const value = await tool.run(args);
messages.push({ role: "tool", content: JSON.stringify(value) });
}
}
throw new Error("Tool-call limit reached");
}06 · Verify
What should you test?
- Reject unknown function names without executing application code.
- Reject malformed JSON and invalid arguments.
- Test an endless sequence of tool requests against a step limit.
Try it yourself
Turn the first verification claim into a failing test, implement the smallest fix, then add a boundary case.
Reject unknown function names without executing application code.
Compare your approach
Your check should fail when the stated invariant is deliberately broken, and pass again after the fix. Keep setup deterministic and assert an observable result.
07 · Limits
Trade-offs and failure modes
- Keep provider secrets on the server.
- Bound conversation size and execution time.
- Provider SDKs evolve; isolate their message formats in an adapter.
Provider calls are mocked in CI; flat schemas are supported. Real provider compatibility and application authorization remain integration concerns.
08 · Keep exploring
Source code and related ideas
yarn tsx packages/ai-function-caller/examples/basic.ts