Skip to Content
DocsTutorialsCustom Tools

Tutorial 3: Custom Tools

⏱️ Time: 45 minutes | 🎯 Goal: Create agents with custom capabilities

Let’s give your agents superpowers! In this tutorial, you’ll learn how to create custom tools that extend what your agents can do beyond text generation.

What You’ll Learn

  • Custom tool development in TypeScript
  • Tool integration with agents
  • External API usage
  • Error handling in tools

Prerequisites


Understanding Tools

Tools extend what agents can do. They allow agents to:

  • Fetch data from external APIs
  • Read and write files
  • Search the web
  • Interact with databases
  • Execute code
  • And much more!

VibeX provides built-in tools through @vibex/tools, and you can create custom ones.

Step 1: Project Setup

Create a new project:

mkdir custom-tools-demo cd custom-tools-demo pnpm init pnpm add vibex @vibex/tools dotenv pnpm add -D typescript tsx @types/node

Project structure:

custom-tools-demo/ ├── src/ │ ├── index.ts # Main application │ └── tools/ │ └── weather.ts # Custom weather tool ├── .env ├── package.json └── tsconfig.json

Step 2: Using Built-in Tools

First, let’s explore the built-in tools. Create src/index.ts:

import "dotenv/config"; import { XAgent } from "vibex"; async function main() { console.log("🔧 Tools Demo\n"); // Create a workspace const xAgent = await XAgent.start("Research assistant with tools"); const space = xAgent.getSpace(); console.log(`✨ Created Space: ${space.spaceId}\n`); // XAgent has access to built-in tools // Ask it to perform a task that might use tools console.log("🤖 XAgent: "); const stream = await xAgent.streamText({ messages: [ { role: "user", content: "Help me understand the current trends in AI. Summarize key developments.", }, ], metadata: { mode: "agent", requestedAgent: "X" }, }); for await (const chunk of stream.textStream) { process.stdout.write(chunk); } console.log("\n"); await space.persistState(); console.log(`💾 Saved: ${space.spaceId}`); } main().catch(console.error);

Step 3: Understanding Tool Architecture

In VibeX, tools are defined using a simple schema that LLMs can understand:

interface Tool { name: string; description: string; parameters: { type: "object"; properties: Record<string, { type: string; description: string; }>; required: string[]; }; execute: (params: Record<string, unknown>) => Promise<string>; }

Step 4: Create a Custom Tool

Let’s create a custom weather tool. Create src/tools/weather.ts:

/** * Custom Weather Tool using Open-Meteo API (free, no API key required) */ interface Coordinates { lat: number; lon: number; name: string; country: string; } interface WeatherData { current: { temperature_2m: number; weathercode: number; windspeed_10m: number; relative_humidity_2m: number; }; } // Tool definition for the LLM export const weatherTool = { name: "get_weather", description: "Get the current weather for a specific location", parameters: { type: "object" as const, properties: { location: { type: "string", description: "City name, e.g., 'San Francisco' or 'London, UK'", }, }, required: ["location"], }, }; // Tool implementation export async function getWeather(location: string): Promise<string> { try { // Get coordinates for the location const coords = await getCoordinates(location); if (!coords) { return `Sorry, I couldn't find the location '${location}'. Please try a more specific location name.`; } // Get weather data const weather = await getWeatherData(coords); if (!weather) { return `Sorry, I couldn't get weather data for ${location}. Please try again later.`; } return formatWeather(coords, weather); } catch (error) { return `Error getting weather for ${location}: ${error}`; } } async function getCoordinates(location: string): Promise<Coordinates | null> { const url = new URL("https://geocoding-api.open-meteo.com/v1/search"); url.searchParams.set("name", location); url.searchParams.set("count", "1"); url.searchParams.set("language", "en"); url.searchParams.set("format", "json"); const response = await fetch(url); if (!response.ok) return null; const data = await response.json(); if (!data.results || data.results.length === 0) return null; const result = data.results[0]; return { lat: result.latitude, lon: result.longitude, name: result.name, country: result.country || "", }; } async function getWeatherData(coords: Coordinates): Promise<WeatherData | null> { const url = new URL("https://api.open-meteo.com/v1/forecast"); url.searchParams.set("latitude", String(coords.lat)); url.searchParams.set("longitude", String(coords.lon)); url.searchParams.set( "current", "temperature_2m,weathercode,windspeed_10m,relative_humidity_2m" ); url.searchParams.set("timezone", "auto"); const response = await fetch(url); if (!response.ok) return null; return response.json(); } function formatWeather(coords: Coordinates, weather: WeatherData): string { const current = weather.current; const condition = getWeatherCondition(current.weathercode); const locationName = coords.country ? `${coords.name}, ${coords.country}` : coords.name; return `🌤️ Current Weather for ${locationName}: 🌡️ Temperature: ${current.temperature_2m}°C 💧 Humidity: ${current.relative_humidity_2m}% 💨 Wind Speed: ${current.windspeed_10m} km/h ☁️ Conditions: ${condition} Data from Open-Meteo API`; } function getWeatherCondition(code: number): string { const conditions: Record<number, string> = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", 95: "Thunderstorm", }; return conditions[code] || `Unknown (code: ${code})`; }

Step 5: Use Your Custom Tool

Update src/index.ts to use the custom tool:

import "dotenv/config"; import { XAgent } from "vibex"; import { getWeather } from "./tools/weather.js"; async function main() { console.log("🌤️ Weather Assistant Demo\n"); const xAgent = await XAgent.start("Weather information assistant"); const space = xAgent.getSpace(); console.log(`✨ Created Space: ${space.spaceId}\n`); // Demonstrate the custom tool const cities = ["San Francisco", "Tokyo", "London"]; for (const city of cities) { console.log(`\n📍 Checking weather in ${city}...`); const weather = await getWeather(city); console.log(weather); } // Now use XAgent with context about the weather console.log("\n" + "─".repeat(50)); console.log("\n🤖 XAgent Analysis:\n"); const weatherSummary = await Promise.all( cities.map((city) => getWeather(city)) ); const stream = await xAgent.streamText({ messages: [ { role: "user", content: `Here's the current weather data I gathered: ${weatherSummary.join("\n\n")} Based on this data, which city would be best for an outdoor picnic today? Explain your reasoning.`, }, ], metadata: { mode: "agent", requestedAgent: "X" }, }); for await (const chunk of stream.textStream) { process.stdout.write(chunk); } console.log("\n"); await space.persistState(); console.log(`\n💾 Saved: ${space.spaceId}`); } main().catch(console.error);

Step 6: Run the Demo

Add scripts to package.json:

{ "type": "module", "scripts": { "start": "tsx src/index.ts" } }

Run:

pnpm start

You’ll see:

🌤️ Weather Assistant Demo ✨ Created Space: space_abc123xyz 📍 Checking weather in San Francisco... 🌤️ Current Weather for San Francisco, United States: 🌡️ Temperature: 18°C 💧 Humidity: 65% 💨 Wind Speed: 12 km/h ☁️ Conditions: Partly cloudy 📍 Checking weather in Tokyo... 🌤️ Current Weather for Tokyo, Japan: 🌡️ Temperature: 24°C ... ────────────────────────────────────────────────── 🤖 XAgent Analysis: Based on the weather data, I'd recommend Tokyo for your outdoor picnic...

Step 7: Create a File Operations Tool

Create src/tools/files.ts:

import { readFile, writeFile, mkdir } from "fs/promises"; import { existsSync } from "fs"; import { dirname, join } from "path"; export const fileTools = { read: { name: "read_file", description: "Read the contents of a file", parameters: { type: "object" as const, properties: { path: { type: "string", description: "Path to the file" }, }, required: ["path"], }, }, write: { name: "write_file", description: "Write content to a file", parameters: { type: "object" as const, properties: { path: { type: "string", description: "Path to the file" }, content: { type: "string", description: "Content to write" }, }, required: ["path", "content"], }, }, }; export async function readFileContent(path: string): Promise<string> { try { const content = await readFile(path, "utf-8"); return content; } catch (error) { return `Error reading file: ${error}`; } } export async function writeFileContent( path: string, content: string ): Promise<string> { try { // Ensure directory exists const dir = dirname(path); if (!existsSync(dir)) { await mkdir(dir, { recursive: true }); } await writeFile(path, content, "utf-8"); return `✅ Successfully wrote to ${path}`; } catch (error) { return `Error writing file: ${error}`; } }

Step 8: Build an Interactive Tool-Enabled Assistant

Create src/interactive.ts:

import "dotenv/config"; import { XAgent } from "vibex"; import { getWeather } from "./tools/weather.js"; import { readFileContent, writeFileContent } from "./tools/files.js"; import * as readline from "readline"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); function prompt(question: string): Promise<string> { return new Promise((resolve) => { rl.question(question, resolve); }); } async function main() { console.log("🔧 Interactive Tool-Enabled Assistant\n"); console.log("Available commands:"); console.log(" weather <city> - Get weather for a city"); console.log(" read <path> - Read a file"); console.log(" write <path> - Write to a file"); console.log(" ask <question> - Ask XAgent"); console.log(" quit - Exit\n"); const xAgent = await XAgent.start("Tool-enabled assistant"); const space = xAgent.getSpace(); console.log(`✨ Space: ${space.spaceId}\n`); while (true) { const input = await prompt("📝 > "); const [command, ...args] = input.trim().split(" "); switch (command.toLowerCase()) { case "weather": { const city = args.join(" ") || "New York"; console.log(`\nFetching weather for ${city}...`); console.log(await getWeather(city)); break; } case "read": { const path = args[0]; if (!path) { console.log("Usage: read <path>"); break; } console.log(`\nReading ${path}...`); console.log(await readFileContent(path)); break; } case "write": { const path = args[0]; if (!path) { console.log("Usage: write <path>"); break; } const content = await prompt("Content: "); console.log(await writeFileContent(path, content)); break; } case "ask": { const question = args.join(" "); if (!question) { console.log("Usage: ask <question>"); break; } console.log("\n🤖 XAgent: "); const stream = await xAgent.streamText({ messages: [{ role: "user", content: question }], metadata: { mode: "agent", requestedAgent: "X" }, }); for await (const chunk of stream.textStream) { process.stdout.write(chunk); } console.log("\n"); break; } case "quit": case "exit": await space.persistState(); console.log(`\n💾 Saved: ${space.spaceId}`); rl.close(); return; default: if (input.trim()) { console.log("Unknown command. Try: weather, read, write, ask, quit"); } } } } main().catch(console.error);

🎉 Congratulations!

You’ve successfully built an agent with custom capabilities! Here’s what you accomplished:

Created custom tools for weather and file operations
Integrated external APIs (Open-Meteo)
Handled errors gracefully
Built an interactive tool-enabled assistant

💡 Key Concepts Learned

  • Tool Definition: Describing tools with name, description, and parameters
  • Tool Implementation: Writing TypeScript functions that do the work
  • External APIs: Connecting to real-world data sources
  • Error Handling: Building resilient tools that handle failures gracefully

🔍 Tool Design Principles

  1. Single Responsibility: Each tool should do one thing well
  2. Clear Descriptions: Help the LLM understand when to use the tool
  3. Error Handling: Always handle failures gracefully
  4. Type Safety: Use TypeScript for reliable tool implementations

🚀 What’s Next?

Now you know how to extend agent capabilities with tools! In Tutorial 4: Configuration Deep Dive, you’ll learn how to configure every aspect of your VibeX workspace for production.

Ideas for More Tools

  • Database tool: Query PostgreSQL or MongoDB
  • Email tool: Send notifications via SendGrid or Resend
  • Search tool: Web search with SerpAPI or Brave Search
  • Calendar tool: Manage events with Google Calendar API
  • Code execution: Run Python or JavaScript code safely

🔧 Troubleshooting

API errors?

  • Check your internet connection
  • Verify the API endpoint is working
  • Add retry logic for reliability

Tool not producing expected results?

  • Check the tool description is clear
  • Verify parameter types match expectations
  • Add logging to debug

Ready to configure for production? Continue to Tutorial 4: Configuration Deep Dive! ⚙️