Tool Calling

Let the model call functions you provide — fetch data, take actions — and use the results to answer, all on-device with generateText().

iOSAndroid

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:

  1. The tool descriptions are added to the prompt.
  2. The model replies with a tool call, or with a plain-text answer.
  3. The proposed arguments are validated against the tool's schema.
  4. Your execute runs; its result is fed back into the conversation.
  5. 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.

weather.tstypescript
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 no execute (see below).
  • 'max-steps' — hit the maxSteps cap 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 maxSteps high 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 execute for 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.