Custom Agent Tools
Create and use custom tools with AI agents for function calling
Custom Agent Tools
Custom tools extend your AI agents with specific capabilities through function calling. All agent providers in ProductReady support the AI SDK tool interface for seamless integration.
Overview
Tools allow AI agents to:
- Execute functions - Call custom code during conversations
- Access external data - Query APIs, databases, or services
- Perform actions - Modify data, send messages, or trigger workflows
- Return structured results - Provide typed responses to the agent
Tool Interface
All tools follow the AI SDK standard interface:
import { tool } from "ai";
import { z } from "zod";
const myTool = tool({
description: "Clear description of what this tool does",
inputSchema: z.object({
// Zod schema defining expected inputs
}),
execute: async (input) => {
// Implementation logic
return result;
},
});Creating Custom Tools
Basic Tool Example
import { tool } from "ai";
import { z } from "zod";
const getWeatherTool = tool({
description: "Get current weather for a specified location",
inputSchema: z.object({
location: z.string().describe("City name or address"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
execute: async ({ location, units }) => {
// Call weather API
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&units=${units}`
);
const data = await response.json();
return {
temperature: data.temp,
condition: data.condition,
humidity: data.humidity,
};
},
});Tool with Database Access
import { tool } from "ai";
import { z } from "zod";
import { db } from "~/db/client";
import { users } from "~/db/schema";
import { eq } from "drizzle-orm";
const getUserInfoTool = tool({
description: "Retrieve user information from database",
inputSchema: z.object({
userId: z.string().describe("User ID to look up"),
}),
execute: async ({ userId }) => {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
if (!user) {
return { error: "User not found" };
}
return {
name: user.name,
email: user.email,
createdAt: user.createdAt,
};
},
});Tool with Error Handling
import { tool } from "ai";
import { z } from "zod";
const calculateTool = tool({
description: "Perform mathematical calculations",
inputSchema: z.object({
expression: z.string().describe("Mathematical expression to evaluate"),
}),
execute: async ({ expression }) => {
try {
// Safe evaluation (use a proper math library in production)
const result = evaluateExpression(expression);
return {
success: true,
result,
expression,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Calculation failed",
expression,
};
}
},
});Using Tools with Providers
AI SDK Provider with Tools
import { AiSdkProvider } from "agentlib/providers";
import { tool } from "ai";
import { z } from "zod";
// Define your tools
const tools = {
get_weather: tool({
description: "Get weather for a location",
inputSchema: z.object({
location: z.string(),
}),
execute: async ({ location }) => {
return { temperature: 72, condition: "sunny" };
},
}),
search_web: tool({
description: "Search the web for information",
inputSchema: z.object({
query: z.string(),
}),
execute: async ({ query }) => {
// Implement web search
return { results: [] };
},
}),
};
// Create provider with tools
const provider = new AiSdkProvider({
model: "gpt-4",
apiKey: process.env.OPENAI_API_KEY,
tools,
});
// Use the provider
const result = await provider.stream(messages);Gaia Agent Provider with Tools
import { GaiaAgentProvider } from "agentlib/providers";
import { tool } from "ai";
import { z } from "zod";
const tools = {
generate_article: tool({
description: "Generate a blog article on a specific topic",
inputSchema: z.object({
title: z.string(),
keywords: z.array(z.string()),
minWords: z.number().default(500),
}),
execute: async ({ title, keywords, minWords }) => {
// Article generation logic
return {
title,
content: "Generated article content...",
wordCount: 750,
};
},
}),
};
const provider = new GaiaAgentProvider({
model: "gpt-4",
apiKey: process.env.OPENAI_API_KEY,
tools,
});Advanced Tool Patterns
Tool with External API Integration
import { tool } from "ai";
import { z } from "zod";
const githubSearchTool = tool({
description: "Search GitHub repositories",
inputSchema: z.object({
query: z.string().describe("Search query"),
language: z.string().optional().describe("Programming language filter"),
stars: z.number().optional().describe("Minimum star count"),
}),
execute: async ({ query, language, stars }) => {
const params = new URLSearchParams({ q: query });
if (language) params.append("language", language);
if (stars) params.append("stars", `>=${stars}`);
const response = await fetch(
`https://api.github.com/search/repositories?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
}
);
const data = await response.json();
return {
total: data.total_count,
repositories: data.items.slice(0, 5).map((repo: any) => ({
name: repo.full_name,
description: repo.description,
stars: repo.stargazers_count,
url: repo.html_url,
})),
};
},
});Tool with File Operations
import { tool } from "ai";
import { z } from "zod";
import { promises as fs } from "fs";
import path from "path";
const saveFileTool = tool({
description: "Save content to a file in the workspace",
inputSchema: z.object({
filename: z.string().describe("Name of the file"),
content: z.string().describe("Content to write"),
directory: z.string().default("output"),
}),
execute: async ({ filename, content, directory }) => {
const dirPath = path.join(process.cwd(), directory);
const filePath = path.join(dirPath, filename);
// Ensure directory exists
await fs.mkdir(dirPath, { recursive: true });
// Write file
await fs.writeFile(filePath, content, "utf-8");
return {
success: true,
path: filePath,
size: content.length,
};
},
});Tool with Async Operations
import { tool } from "ai";
import { z } from "zod";
const processLargeDataTool = tool({
description: "Process large dataset asynchronously",
inputSchema: z.object({
datasetId: z.string(),
operation: z.enum(["analyze", "transform", "export"]),
}),
execute: async ({ datasetId, operation }) => {
// Start async operation
const jobId = await startBackgroundJob(datasetId, operation);
// Poll for completion (simplified)
const result = await waitForJobCompletion(jobId);
return {
jobId,
status: "completed",
result,
};
},
});Tool Organization
Grouping Tools by Feature
// tools/weather.ts
export const weatherTools = {
get_weather: tool({ /* ... */ }),
get_forecast: tool({ /* ... */ }),
};
// tools/search.ts
export const searchTools = {
search_web: tool({ /* ... */ }),
search_docs: tool({ /* ... */ }),
};
// tools/index.ts
import { weatherTools } from "./weather";
import { searchTools } from "./search";
export const allTools = {
...weatherTools,
...searchTools,
};Context-Specific Tools
import { GaiaAgentProvider } from "agentlib/providers";
import { weatherTools } from "./tools/weather";
import { searchTools } from "./tools/search";
import { databaseTools } from "./tools/database";
function getToolsForContext(context: string) {
switch (context) {
case "weather-app":
return weatherTools;
case "search-app":
return searchTools;
case "admin-panel":
return { ...searchTools, ...databaseTools };
default:
return {};
}
}
// Use in provider
const provider = new GaiaAgentProvider({
model: "gpt-4",
apiKey: process.env.OPENAI_API_KEY,
tools: getToolsForContext("weather-app"),
});Best Practices
1. Clear Tool Descriptions
// ✅ Good: Specific, actionable description
const goodTool = tool({
description: "Search GitHub repositories by keyword, language, and minimum stars. Returns top 5 matching repositories with metadata.",
// ...
});
// ❌ Bad: Vague description
const badTool = tool({
description: "Search stuff",
// ...
});2. Detailed Input Schemas
// ✅ Good: Well-documented schema with descriptions
const goodSchema = z.object({
query: z.string().describe("Search query (e.g., 'react hooks')"),
language: z.string().optional().describe("Programming language filter (e.g., 'typescript')"),
minStars: z.number().min(0).optional().describe("Minimum star count (default: 0)"),
});
// ❌ Bad: No descriptions
const badSchema = z.object({
q: z.string(),
lang: z.string().optional(),
stars: z.number().optional(),
});3. Robust Error Handling
const robustTool = tool({
description: "Fetch user profile",
inputSchema: z.object({
userId: z.string(),
}),
execute: async ({ userId }) => {
try {
const user = await fetchUser(userId);
if (!user) {
return {
success: false,
error: "User not found",
userId,
};
}
return {
success: true,
user,
};
} catch (error) {
console.error("Tool execution error:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
},
});4. Return Structured Data
// ✅ Good: Structured, typed response
execute: async ({ location }) => {
return {
location,
temperature: 72,
unit: "fahrenheit",
condition: "sunny",
humidity: 45,
timestamp: new Date().toISOString(),
};
}
// ❌ Bad: Unstructured string
execute: async ({ location }) => {
return `It's 72°F and sunny in ${location}`;
}5. Input Validation
const validatingTool = tool({
description: "Process payment",
inputSchema: z.object({
amount: z.number().positive().max(10000),
currency: z.enum(["USD", "EUR", "GBP"]),
description: z.string().min(5).max(200),
}),
execute: async ({ amount, currency, description }) => {
// Zod validates inputs automatically
// Additional business logic validation
if (amount > 1000 && !description.includes("verified")) {
return {
success: false,
error: "Large payments require verification in description",
};
}
// Process payment
return { success: true, transactionId: "..." };
},
});Tool Security
Sanitize User Inputs
import { tool } from "ai";
import { z } from "zod";
const secureSearchTool = tool({
description: "Search with sanitized input",
inputSchema: z.object({
query: z.string().max(200),
}),
execute: async ({ query }) => {
// Sanitize input
const sanitized = query
.replace(/[^\w\s-]/g, "") // Remove special chars
.trim()
.toLowerCase();
// Use sanitized input
const results = await searchDatabase(sanitized);
return results;
},
});Restrict File Access
const safeFileReadTool = tool({
description: "Read file contents safely",
inputSchema: z.object({
filename: z.string(),
}),
execute: async ({ filename }) => {
// Validate filename to prevent path traversal
if (filename.includes("..") || filename.startsWith("/")) {
return {
error: "Invalid filename",
};
}
// Restrict to allowed directory
const allowedDir = path.join(process.cwd(), "data");
const filePath = path.join(allowedDir, filename);
if (!filePath.startsWith(allowedDir)) {
return {
error: "Access denied",
};
}
const content = await fs.readFile(filePath, "utf-8");
return { content };
},
});Rate Limiting
import { tool } from "ai";
import { z } from "zod";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const redis = new Redis({ /* config */ });
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "1 m"),
});
const rateLimitedTool = tool({
description: "API call with rate limiting",
inputSchema: z.object({
query: z.string(),
userId: z.string(),
}),
execute: async ({ query, userId }) => {
// Check rate limit
const { success } = await ratelimit.limit(userId);
if (!success) {
return {
error: "Rate limit exceeded. Please try again later.",
};
}
// Execute tool logic
const result = await performQuery(query);
return result;
},
});Testing Tools
Unit Testing
import { describe, it, expect } from "vitest";
import { getWeatherTool } from "./weather-tools";
describe("getWeatherTool", () => {
it("should return weather data for valid location", async () => {
const result = await getWeatherTool.execute({
location: "San Francisco",
units: "celsius",
});
expect(result).toHaveProperty("temperature");
expect(result).toHaveProperty("condition");
expect(result.temperature).toBeTypeOf("number");
});
it("should handle invalid location", async () => {
const result = await getWeatherTool.execute({
location: "InvalidCity123",
units: "celsius",
});
expect(result).toHaveProperty("error");
});
});Integration Testing
import { describe, it, expect } from "vitest";
import { GaiaAgentProvider } from "agentlib/providers";
import { myTools } from "./tools";
describe("Agent with custom tools", () => {
it("should use tool correctly in conversation", async () => {
const provider = new GaiaAgentProvider({
model: "gpt-4",
apiKey: process.env.OPENAI_API_KEY,
tools: myTools,
});
const messages = [
{ role: "user", content: "What's the weather in Tokyo?" },
];
const result = await provider.stream(messages);
// Verify tool was called
expect(result).toBeDefined();
});
});Troubleshooting
Tool Not Being Called
Issue: Agent doesn't use the tool even when it should.
Solutions:
- Improve tool description to be more specific
- Make input schema descriptions clearer
- Provide examples in the description
- Check if tool name is intuitive
// Before
tool({
description: "Get data",
// ...
});
// After
tool({
description: "Get current weather data for any city. Example: 'What's the weather in Paris?' Returns temperature, condition, and humidity.",
// ...
});Tool Execution Errors
Issue: Tool throws errors during execution.
Solutions:
- Add comprehensive error handling
- Validate inputs beyond Zod schema
- Add logging for debugging
- Return meaningful error messages
execute: async (input) => {
try {
// Log inputs
console.log("Tool called with:", input);
// Validate
if (!isValid(input)) {
return { error: "Invalid input" };
}
// Execute
const result = await performAction(input);
return result;
} catch (error) {
console.error("Tool error:", error);
return {
error: error instanceof Error ? error.message : "Unknown error",
};
}
}Schema Validation Failures
Issue: Inputs don't match expected schema.
Solutions:
- Make optional fields truly optional with
.optional() - Provide sensible defaults with
.default() - Add clear descriptions to all fields
- Test schema with various inputs