My App

Defining Tools

Tools (often referred to as function calling) allow your agent to interact with the outside world—fetching data from APIs, writing to databases, or performing calculations.

Fragola provides a simple tool(...) helper function to define these tools securely and with full TypeScript support.

The tool(...) Helper

To create a tool, use the tool function exported by the SDK. A tool requires a name, a description, a schema representing the expected parameters, and a handler function.

import { tool } from "@fragola-ai/agent";
import { z } from "zod";

const getWeatherTool = tool({
    name: "getWeather",
    description: "Fetches the current weather for a specific city.",
    schema: z.object({
        city: z.string().describe("The city to get weather for, e.g., 'London'")
    }),
    handler: async (params, context) => {
        // `params` is fully typed based on the provided schema!
        // params.city is a string
        const response = await fetch(`https://api.weather.com/v1?q=${params.city}`);
        const data = await response.json();
        
        return `The current temperature in ${params.city} is ${data.temp}.`;
    }
});

Tool Metadata

  • name: The exact string name the model will use to invoke the tool. It must conform to OpenAI's naming rules (usually alphanumeric and underscores).
  • description: A highly detailed description of what the tool does and when the model should use it. Good descriptions are critical for the model to choose the right tool at the right time.

Tool Handlers

The handler is the function that executes when the LLM requests the tool. It can be synchronous or asynchronous.

The handler receives two arguments:

  1. params: The parsed parameters provided by the LLM. If you used a Zod schema, this object will be strictly typed and validated before the handler runs.
  2. context: The AgentContext object, giving you access to the agent's stores, state, and message history directly from within the tool.
const logToStoreTool = tool({
    name: "logMetrics",
    description: "Logs metrics to the agent's scoped store.",
    schema: z.object({ value: z.number() }),
    handler: (params, context) => {
        const store = context.getStore("metrics-store");
        store?.update(prev => ({ ...prev, lastValue: params.value }));
        return "Metrics successfully logged.";
    }
});

Return Values

The handler can return almost anything: strings, numbers, booleans, arrays, or JSON objects. Fragola will automatically serialize the return value into a stringified JSON format (if it isn't a string already) to feed back into the LLM context.

Schema & Validation

When defining tools or using agent.json(), you must define the shape of the data you expect the LLM to provide. Fragola supports defining these schemas using either Zod or raw JSON Schema strings.

Zod Schema Validation

The recommended approach is to use Zod. It provides strict runtime validation and automatically infers TypeScript types for your handler functions.

Fragola supports both Zod v3 and Zod v4.

import { z } from "zod";
import { tool } from "@fragola-ai/agent";

const createEventTool = tool({
    name: "createEvent",
    description: "Creates a calendar event.",
    schema: z.object({
        title: z.string(),
        date: z.string().date(),
        attendees: z.array(z.string().email())
    }),
    handler: (params) => {
        // Types are correctly inferred
        // params.attendees is string[]
        return "Event created!";
    }
});

Automatic Error Reporting

When you use a Zod schema, Fragola automatically intercepts the LLM's tool call request and validates the arguments against the schema before running your handler.

If the LLM provides invalid arguments (e.g., passing a number instead of a string, or omitting a required field), the handler is never executed. Instead, Fragola catches the validation error, formats it nicely, and sends an error message back to the LLM automatically so the model can correct its mistake in the next step.

Raw JSON Schema Strings

In some advanced use-cases—such as when schemas are generated dynamically by another system, or you are proxying requests—you can pass a raw JSON Schema string instead of a Zod object.

const genericTool = tool({
    name: "genericAction",
    description: "Performs an action.",
    // A stringified JSON Schema
    schema: '{ "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] }',
    handler: (params) => {
        // Because a raw string was used, `params` is `any` 
        // You are responsible for validating the object manually inside the handler
        if (typeof params.id !== "string") {
            throw new Error("Invalid ID parameter");
        }
        return `Action ${params.id} completed.`;
    }
});

Note: When using a raw JSON schema string, Fragola does not perform automatic validation before executing the handler. You must validate the input manually.

Dynamic Tools & Custom Resolution

Sometimes you want the LLM to call a tool, but you don't actually want Fragola to execute the logic directly.

This happens frequently in proxy servers, remote execution environments, or when delegating the actual execution to a separate microservice. In these scenarios, you want to pause the execution, send the tool call elsewhere, and inject the result later.

The "dynamic" Handler

To tell Fragola that a tool's execution will be handled externally, set the handler property to the string "dynamic" instead of providing a function.

import { tool } from "@fragola-ai/agent";
import { z } from "zod";

const remoteTool = tool({
    name: "remoteAction",
    description: "An action executed on a remote server.",
    schema: z.object({ id: z.string() }),
    handler: "dynamic" // Skips local execution
});

When the LLM calls remoteAction, Fragola will register the tool call in the state but will not attempt to run any function. The agent's loop will pause, and it will be up to your application to provide the result.

Injecting Results via Events

To resolve a dynamic tool, you use the onBeforeToolCall event to intercept the call and inject a hardcoded response.

agent.onBeforeToolCall(async ({ config }) => {
    // Check if the current tool being called is our dynamic tool
    if (config.tool.name === "remoteAction") {
        
        // Send the tool arguments to your external service
        const externalResult = await fetchRemoteService(config.params);
        
        // Inject the response directly into the event configuration.
        // By setting `injectResponse`, Fragola skips execution entirely 
        // and treats this string as the final output of the tool.
        config.injectResponse = JSON.stringify(externalResult);
    }
    
    return config;
});

Using "dynamic" handlers paired with injectResponse allows you to build complex, distributed agent architectures while keeping the core logic predictable and event-driven.

Runtime Tool Management

In complex agents, you don't always want every tool available to the model at all times. Providing too many tools can confuse the model, burn unnecessary tokens, or expose secure actions before they are authorized.

Fragola allows you to dynamically manage which tools are exposed to the LLM during an execution turn using the AgentContext.

Updating Tools Dynamically

You can access the context.updateTools method from within an event listener to change the available tools on the fly.

This is highly effective when paired with state-machine patterns. For instance, if an agent is in an "auth" state, only provide the login tool. Once authenticated, swap them out for the primary tools.

agent.onBeforeModelInvocation(({ context, config }) => {
    const isLoggedIn = context.getStore("auth")?.get().isLoggedIn;

    if (!isLoggedIn) {
        // Only allow the login tool
        context.updateTools([loginTool]);
    } else {
        // Allow the full suite of tools
        context.updateTools([searchTool, databaseQueryTool, logoutTool]);
    }

    return config;
});

When you call context.updateTools(), the underlying SDK client receives the updated tool schemas immediately before making the API call to OpenAI.

Adding and Removing Individual Tools

context.updateTools expects a full array of tools, which completely overwrites the existing list.

If you want to append or remove a specific tool without losing the others, you can read the currently registered tools from context.options.tools, modify the array, and pass it back.

// Example: Removing a specific tool after it has been used once
const oneTimeTool = tool({
    name: "initializeSetup",
    description: "Runs once.",
    schema: z.object({}),
    handler: (params, context) => {
        // Run logic...
        
        // Remove this tool from the agent so the model can't call it again
        const currentTools = context.options.tools || [];
        const filteredTools = currentTools.filter(t => t.name !== "initializeSetup");
        
        context.updateTools(filteredTools);
        
        return "Setup complete.";
    }
});

On this page