Tool Calling
Let the model call functions you provide — fetch data, take actions — and use the results to answer, all on-device with generateText().
Overview
Where structured output gives you JSON as the final answer, tool calling is a loop: the model proposes a function call, your code runs it, the result is fed back, and the model continues — until it produces a plain-text answer. This is how you connect an on-device model to live data (weather, a database, search) or actions.
How It Works
generateText() drives the loop in JavaScript over sendMessage(), so it works on every backend and inherits the single-flight guard, AbortSignal, and systemPrompt semantics:
- The tool descriptions are added to the prompt.
- The model replies with a tool call, or with a plain-text answer.
- The proposed arguments are validated against the tool's schema.
- Your
executeruns; its result is fed back into the conversation. - Repeat until the model answers in plain text, or
maxSteps(default 5) is reached.
Defining Tools
Each tool has a description (how the model decides when to use it), a JSON-Schema parameters object, and an execute function that receives the validated arguments.
import { generateText } from 'expo-ai-kit';
const { text } = await generateText(
[{ role: 'user', content: 'What should I wear in Paris today?' }],
{
tools: {
getWeather: {
description: 'Get the current weather for a city.',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
execute: async ({ city }: { city: string }) => {
const w = await fetchWeather(city);
return { tempC: w.tempC, conditions: w.conditions };
},
},
},
maxSteps: 5,
},
);
console.log(text); // "Bring a jacket and an umbrella — it's 12°C and raining."The Result
generateText() returns the final text plus a full trace — every step, all toolCalls and toolResults, and a finishReason.
const res = await generateText(messages, { tools });
res.text; // final assistant answer
res.steps; // each model round-trip (text + toolCalls + toolResults)
res.toolCalls; // every call across all steps, flattened
res.toolResults; // every result, flattened
res.finishReason; // 'stop' | 'tool-calls' | 'max-steps''stop'— the model produced a final text answer.'tool-calls'— stopped because a tool has noexecute(see below).'max-steps'— hit themaxStepscap while still calling tools; raise it.
Human in the Loop
Omit a tool's execute and the loop stops the moment the model wants to call it, returning the proposed call instead of running anything. Use this to confirm or gate sensitive actions.
const res = await generateText(messages, {
tools: {
deleteAccount: {
description: 'Permanently delete the user account.',
parameters: { type: 'object', properties: { userId: { type: 'string' } }, required: ['userId'] },
// no execute — we want to confirm first
},
},
});
if (res.finishReason === 'tool-calls') {
const call = res.toolCalls[0]; // { toolName, args }
const ok = await confirmWithUser(call);
if (ok) await reallyDelete(call.args);
}Reliability
On-device models are weaker at tool selection than frontier cloud models, so the loop is defensive. A malformed call, an unknown tool name, or arguments that fail schema validation are re-prompted with the error up to maxRepairAttempts times (default 2). If the model still can't comply, generateText() throws rather than executing bad input. An execute that throws is caught and fed back to the model as { error } so it can recover.
Keep tool sets small
Fewer tools with sharp, action-oriented descriptions and flat parameters dramatically improve tool selection on-device. Prefer a focused set over a large toolbox.
Error Handling
import { generateText, ModelError } from 'expo-ai-kit';
try {
const { text } = await generateText(messages, { tools });
} catch (e) {
if (e instanceof ModelError && e.code === 'INFERENCE_FAILED') {
// Model kept proposing an unknown tool or invalid args after repairs.
}
}Tips
- Set
maxStepshigh enough for the model to call tools and then answer (a typical task is 2 steps). - Return small, plain results from
execute— strings or shallow objects the model can read easily. - With no
tools,generateText()is just a single text generation. - Validate again inside
executefor anything destructive — schema validation guards shape, not intent.
Stable by design
Like structured output, the protocol is parsed out of the model's text today, keeping the signature stable so native guided generation (Apple Tool protocol / LiteRT-LM) can slot in behind generateText() later.