Overview

Client tools are custom functions that the voice agent can use during the conversation. They are defined and implemented on the client side.

Simple Example

Here’s a complete example with one tool:

Step 1: Define the Tool

const getTimeSchema = {
  name: "get_time",
  type: "function",
  description: "Get the current time",
  parameters: {
    type: "object",
    properties: {},
    required: [],
  },
};
Tool schemas are OpenAI compatible. See the OpenAI Function Calling guide for more details on defining functions.

Step 2: Implement the Function

import { type ClientTool } from "@outspeed/client";

// params & context can be skipped here since this tool doesn't use them
const getTime: ClientTool<{}> = (params, context) => {
  return new Date().toLocaleTimeString();
};
The return value from your function is sent directly to the AI model, which uses it to generate its response to the user.
Always return a value - even for action-based tools:
  • Data tools (weather, calculations): Return the actual data
  • Action tools (generate image, open browser): Return acknowledgment like “Image generated successfully” or “Browser tab opened”
  • On failure: Return error description like “Failed to generate image: rate limit error”
This tells the AI whether your tool succeeded or failed. See the implementation section for more details.

Step 3: Configure Session

const sessionConfig = {
  // rest of config...
  tools: [getTimeSchema],
};

const conversation = useConversation({
  clientTools: {
    get_time: getTime,
  },
});
That’s it! When the user asks “What time is it?”, the agent will:
  1. Call your getTime() function
  2. Receive the return value (e.g., “2:30:45 PM”)
  3. Use that information to respond to the user

Advanced Example with Context

Here’s a more complex tool with typed parameters and context usage:
import { type ClientTool } from "@outspeed/client";

const setTimer: ClientTool<{ time: number; prompt: string }> = ({ time, prompt }, context) => {
  setTimeout(() => {
    // show a toast if you want to
    // toast.info("Timer completed!");

    // we let the model know that the timer is done so that it can respond to the user
    context.sendText(
      `🔔 TIMER ALERT: The timer you set ${time} seconds ago has finished.
  
Timer prompt: "${prompt}"
Completed at: ${new Date().toLocaleString()}
  
This is an automated system notification. Please proceed with any actions related to this timer.`,
    );
  }, time * 1000);

  // we set the timer and let the model know that the timer is set
  return "Timer set";
};
The context parameter provides access to conversation methods like sendText() for sending messages back to the AI after your tool completes.

Tool Schema Format

Tool schemas are OpenAI compatible. See the OpenAI Function Calling guide for more details on defining functions.
{
  name: string,           // Unique tool identifier
  type: "function",       // Always "function" for client tools
  description: string,    // Clear description for the AI
  parameters: {
    type: "object",
    properties: {
      [paramName]: {
        type: string,       // "string", "number", "boolean", "array", "object"
        description: string // Parameter description
      }
    },
    required: string[]      // Required parameter names
  }
}

Best Practices

Tool Design

  • Clear descriptions: Help the AI understand when and how to use each tool
  • Specific parameters: Define precise parameter types and descriptions
  • Single purpose: Each tool should do one thing well
  • Predictable naming: Use descriptive, consistent naming conventions

Implementation

Always return meaningful values - the AI uses your return value to respond to the user:
import { type ClientTool } from "@outspeed/client";

// ✅ Good: Return actual data
const getWeather: ClientTool<{ city: string }> = ({ city }, context) => {
  return "72°F and sunny in San Francisco";
};

// ✅ Good: Return success confirmation
const sendEmail: ClientTool<{ to: string; subject: string }> = ({ to, subject }, context) => {
  // ... send email logic
  return `Email sent to ${to}`;
};

// ✅ Good: Return error details
const uploadFile: ClientTool<{ filename: string }> = ({ filename }, context) => {
  try {
    // ... upload logic
    return "File uploaded successfully";
  } catch (error) {
    return `Upload failed: ${error.message}`;
  }
};

// ❌ Bad: Don't return undefined/null
const badTool: ClientTool<{}> = (params, context) => {
  // The AI gets nothing to work with
  return null;
};
Handle errors gracefully:
const robustTool: ClientTool<{ param: string }> = ({ param }, context) => {
  try {
    if (!param?.trim()) {
      return "Parameter is required";
    }

    const result = performOperation(param);

    // notice that we're returning something that model can use to respond to the user
    return result || "Operation completed but no data returned";
  } catch (error) {
    return `Error: ${error.message}`; // for the model to understand the error
  }
};
Use async/await for API calls:
const fetchData: ClientTool<{ query: string }> = async ({ query }, context) => {
  try {
    const response = await fetch(`/api/search?q=${query}`);
    const data = await response.json();
    return `Found ${data.results.length} results for "${query}"`;
  } catch (error) {
    return "Search service unavailable"; // again, for the model to understand what went wrong
  }
};

Performance

  • Cache results: Cache API responses when appropriate
  • Timeout handling: Set reasonable timeouts for external calls
  • Rate limiting: Respect API rate limits
  • Graceful degradation: Provide fallbacks when tools fail

Error Handling

export async function robustToolFunction({ param }: { param: string }) {
  try {
    // Validate input
    if (!param || param.trim() === "") {
      return "Parameter is required";
    }

    // Perform operation
    const result = await someApiCall(param);

    // Validate result
    if (!result) {
      return "No data available";
    }

    return result;
  } catch (error) {
    console.error("Tool error:", error);

    // Return user-friendly error message
    if (error instanceof Error) {
      return `Error: ${error.message}`;
    }

    return "An unexpected error occurred";
  }
}

Combining with System Tools

You can use client tools alongside system tools:
const sessionConfig = {
  // rest of config...
  tools: [getTimeSchema],
  system_tools: [
    { name: "end_call", enabled: true },
    { name: "skip_turn", enabled: true },
  ],
};