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: [],
  },
};

Step 2: Implement the Function

Implement the function associated with the tool
function getTime() {
  return new Date().toLocaleTimeString();
}
Important: 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

Tool Schema Format

Each tool schema must follow this structure:
{
  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:
// ✅ Good: Return actual data
function getWeather({ city }: { city: string }) {
  return "72°F and sunny in San Francisco";
}

// ✅ Good: Return success confirmation
function sendEmail({ to, subject }: { to: string; subject: string }) {
  // ... send email logic
  return `Email sent to ${to}`;
}

// ✅ Good: Return error details
function uploadFile({ filename }: { filename: string }) {
  try {
    // ... upload logic
    return "File uploaded successfully";
  } catch (error) {
    return `Upload failed: ${error.message}`;
  }
}

// ❌ Bad: Don't return undefined/null
function badTool() {
  // The AI gets nothing to work with
  return null;
}
Handle errors gracefully:
function robustTool({ param }: { param: string }) {
  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:
async function fetchData({ query }: { query: string }) {
  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 },
  ],
};