Basic Tool Structure
Tools are defined using the AI SDK’stool() function with Zod schemas:
import { tool } from "ai";
import { z } from "zod";
const myTool = tool({
description: "Clear description of what this tool does",
inputSchema: z.object({
param1: z.string().describe("Description of parameter 1"),
param2: z.number().optional().describe("Optional parameter 2"),
}),
execute: async ({ param1, param2 }) => {
// Tool implementation
const result = await performAction(param1, param2);
return result;
},
});
Complete Examples
Weather Tool
const weatherTool = tool({
description: "Get current weather conditions for a location",
inputSchema: z.object({
location: z.string().describe("City name or address"),
units: z.enum(["celsius", "fahrenheit"]).optional().describe("Temperature units"),
}),
execute: async ({ location, units = "fahrenheit" }) => {
// Real implementation would call a weather API
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${location}`
);
const data = await response.json();
return {
location: data.location.name,
temperature: units === "celsius" ? data.current.temp_c : data.current.temp_f,
conditions: data.current.condition.text,
humidity: data.current.humidity,
windSpeed: data.current.wind_mph,
units,
};
},
});
Database Query Tool
import { db } from "./database";
const searchCustomersTool = tool({
description: "Search for customers by name, email, or phone number",
inputSchema: z.object({
query: z.string().describe("Search query (name, email, or phone)"),
limit: z.number().min(1).max(50).optional().describe("Maximum results to return"),
}),
execute: async ({ query, limit = 10 }) => {
const customers = await db.customers.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ phone: { contains: query } },
],
},
take: limit,
select: {
id: true,
name: true,
email: true,
phone: true,
createdAt: true,
},
});
return {
found: customers.length,
customers,
};
},
});
Calendar Tool
import { google } from "googleapis";
const scheduleEventTool = tool({
description: "Schedule a calendar event",
inputSchema: z.object({
title: z.string().describe("Event title"),
startTime: z.string().describe("Start time in ISO 8601 format"),
duration: z.number().describe("Duration in minutes"),
attendees: z.array(z.string().email()).optional().describe("Email addresses of attendees"),
}),
execute: async ({ title, startTime, duration, attendees = [] }) => {
const calendar = google.calendar({ version: "v3", auth });
const endTime = new Date(
new Date(startTime).getTime() + duration * 60000
).toISOString();
const event = await calendar.events.insert({
calendarId: "primary",
requestBody: {
summary: title,
start: { dateTime: startTime },
end: { dateTime: endTime },
attendees: attendees.map(email => ({ email })),
},
});
return {
success: true,
eventId: event.data.id,
eventLink: event.data.htmlLink,
startTime,
endTime,
};
},
});
API Integration Tool
const sendSlackMessageTool = tool({
description: "Send a message to a Slack channel",
inputSchema: z.object({
channel: z.string().describe("Channel name (e.g., #general)"),
message: z.string().describe("Message content"),
urgent: z.boolean().optional().describe("Whether to send as urgent notification"),
}),
execute: async ({ channel, message, urgent = false }) => {
const response = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLACK_BOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
channel,
text: urgent ? `🚨 ${message}` : message,
...(urgent && { priority: "high" }),
}),
});
const data = await response.json();
if (!data.ok) {
throw new Error(`Slack API error: ${data.error}`);
}
return {
success: true,
channel: data.channel,
timestamp: data.ts,
};
},
});
File System Tool
import { readFile, writeFile } from "fs/promises";
import { join } from "path";
const readNoteTool = tool({
description: "Read the contents of a saved note",
inputSchema: z.object({
noteName: z.string().describe("Name of the note to read"),
}),
execute: async ({ noteName }) => {
const filePath = join(__dirname, "notes", `${noteName}.txt`);
try {
const content = await readFile(filePath, "utf-8");
return {
success: true,
noteName,
content,
length: content.length,
};
} catch (error) {
return {
success: false,
error: "Note not found",
noteName,
};
}
},
});
const saveNoteTool = tool({
description: "Save a note to the file system",
inputSchema: z.object({
noteName: z.string().describe("Name for the note"),
content: z.string().describe("Content to save"),
}),
execute: async ({ noteName, content }) => {
const filePath = join(__dirname, "notes", `${noteName}.txt`);
await writeFile(filePath, content, "utf-8");
return {
success: true,
noteName,
savedAt: new Date().toISOString(),
size: content.length,
};
},
});
Registering Tools
During Initialization
const agent = new VoiceAgent({
model: openai("gpt-4o"),
tools: {
getWeather: weatherTool,
searchCustomers: searchCustomersTool,
scheduleEvent: scheduleEventTool,
sendSlackMessage: sendSlackMessageTool,
readNote: readNoteTool,
saveNote: saveNoteTool,
},
// ...
});
After Initialization
// Add more tools dynamically
agent.registerTools({
newTool1: tool1,
newTool2: tool2,
});
Handling Tool Events
Stream-Level Events
These fire as the LLM streams tool calls:// Fires when LLM decides to call a tool
agent.on("chunk:tool_call", ({ toolName, input }) => {
console.log(`🛠️ Calling ${toolName}`);
console.log(`Input:`, JSON.stringify(input, null, 2));
});
// Fires when tool execution completes
agent.on("chunk:tool_result", ({ name, result }) => {
console.log(`✅ ${name} completed`);
console.log(`Result:`, JSON.stringify(result, null, 2));
});
Response-Level Events
These fire after the entire response completes:// Fires with all tool results at the end
agent.on("tool_result", ({ name, toolCallId, result }) => {
console.log(`Tool: ${name}`);
console.log(`Call ID: ${toolCallId}`);
console.log(`Result:`, result);
// Store in database, log to analytics, etc.
analytics.track("tool_executed", {
toolName: name,
callId: toolCallId,
success: result.success !== false,
});
});
Multi-Step Tool Chains
The agent can automatically chain multiple tools together:const agent = new VoiceAgent({
model: openai("gpt-4o"),
tools: {
searchCustomers: searchCustomersTool,
sendSlackMessage: sendSlackMessageTool,
scheduleEvent: scheduleEventTool,
},
instructions: `You are a helpful assistant.
Use tools when needed and chain them together to complete tasks.`,
});
// User: "Find customers named John and notify the sales team"
// Agent will:
// 1. Call searchCustomers({ query: "John" })
// 2. Analyze results
// 3. Call sendSlackMessage({ channel: "#sales", message: "Found 3 customers named John..." })
Tracking Multi-Step Execution
const toolCalls: Array<{ name: string; input: any; result: any }> = [];
agent.on("chunk:tool_call", ({ toolName, input }) => {
toolCalls.push({ name: toolName, input, result: null });
});
agent.on("chunk:tool_result", ({ name, result }) => {
const call = toolCalls.find(c => c.name === name && c.result === null);
if (call) call.result = result;
});
agent.on("text", ({ role, text }) => {
if (role === "assistant") {
console.log(`\n=== Tool Chain Summary ===");
console.log(`Steps executed: ${toolCalls.length}`);
toolCalls.forEach((call, i) => {
console.log(`${i + 1}. ${call.name}`);
console.log(` Input: ${JSON.stringify(call.input)}`);
console.log(` Result: ${JSON.stringify(call.result)}`);
});
toolCalls.length = 0; // Reset for next interaction
}
});
Error Handling
In Tool Execution
const riskyTool = tool({
description: "Performs a risky operation",
inputSchema: z.object({
action: z.string(),
}),
execute: async ({ action }) => {
try {
const result = await performRiskyOperation(action);
return {
success: true,
result,
};
} catch (error) {
// Return error info so LLM can handle it
return {
success: false,
error: error.message,
suggestion: "Please try again with different parameters",
};
}
},
});
At Agent Level
agent.on("error", (error) => {
console.error("Agent error:", error);
// Check if it's a tool error
if (error.message.includes("Tool execution failed")) {
console.log("Tool error detected - continuing with fallback");
// Could notify user, retry, or use cached data
}
});
Best Practices
1. Clear Descriptions
// ❌ Bad: Vague description
description: "Gets stuff"
// ✅ Good: Specific and actionable
description: "Get current weather conditions for a specific location"
2. Detailed Parameter Descriptions
// ❌ Bad: No context
location: z.string()
// ✅ Good: Clear expectations
location: z.string().describe("City name or full address (e.g., 'San Francisco' or 'New York, NY')")
3. Structured Return Values
// ✅ Good: Consistent structure
return {
success: true,
data: result,
metadata: {
timestamp: new Date().toISOString(),
source: "api",
},
};
4. Validate Inputs
execute: async ({ email }) => {
// Even though Zod validates, add business logic validation
if (!email.endsWith("@company.com")) {
return {
success: false,
error: "Only company email addresses are allowed",
};
}
// ...
}
5. Handle Rate Limits
import { RateLimiter } from "limiter";
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "minute" });
const apiTool = tool({
description: "Call external API",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => {
await limiter.removeTokens(1);
// Make API call
},
});
Testing Tools
import { describe, it, expect } from "vitest";
describe("weatherTool", () => {
it("should return weather data for valid location", async () => {
const result = await weatherTool.execute({
location: "San Francisco",
units: "fahrenheit",
});
expect(result).toHaveProperty("location");
expect(result).toHaveProperty("temperature");
expect(result.units).toBe("fahrenheit");
});
it("should handle invalid location", async () => {
const result = await weatherTool.execute({
location: "InvalidCity123",
});
expect(result.success).toBe(false);
expect(result).toHaveProperty("error");
});
});
Next Steps
- API Reference - Full VoiceAgent API
- Basic Usage - Getting started guide
- WebSocket Server - Production server setup