Back to CourseLesson 3 of 12

Creating Custom Tools

Tools are what give agents their power. A tool is a function that an agent can call to interact with the outside world -- fetching data, running calculations, writing files, or calling APIs. In this lesson, you will learn how to define, validate, and implement custom tools using CoFounder's tool interface.

The Tool Interface

Every tool in CoFounder implements the Tool interface. It requires four pieces: a name, a description the LLM uses to decide when to call it, a parameter schema, and an execute function.

import { Tool } from '@waymakerai/aicofounder-core';

const calculatorTool: Tool = {
  name: 'calculator',
  description: 'Perform basic arithmetic calculations. Use this when you need exact math results.',
  parameters: {
    type: 'object',
    properties: {
      expression: {
        type: 'string',
        description: 'A mathematical expression like "2 + 2" or "15 * 3.14"',
      },
    },
    required: ['expression'],
  },
  execute: async ({ expression }) => {
    const result = Function(`"use strict"; return (${expression})`)();
    return JSON.stringify({ result });
  },
};

The description is critical -- it tells the LLM when and why to use this tool. Be specific about what the tool does and when it should be chosen over other tools.

Input Validation with Zod

LLMs sometimes produce malformed tool arguments. CoFounder integrates with Zod so you can validate inputs before execution:

import { Tool, createToolWithValidation } from '@waymakerai/aicofounder-core';
import { z } from 'zod';

const searchSchema = z.object({
  query: z.string().min(1).max(500),
  maxResults: z.number().int().min(1).max(20).default(5),
  language: z.enum(['en', 'es', 'fr', 'de']).default('en'),
});

const searchTool = createToolWithValidation({
  name: 'web_search',
  description: 'Search the web for information. Returns titles, URLs, and snippets.',
  schema: searchSchema,
  execute: async ({ query, maxResults, language }) => {
    const results = await performSearch(query, { maxResults, language });
    return JSON.stringify(results);
  },
});

When validation fails, CoFounder automatically returns a structured error to the agent so it can retry with corrected parameters.

Async Tool Execution

All tool execute functions are async by default. This lets you call external APIs, query databases, or perform any I/O operation:

import { Tool } from '@waymakerai/aicofounder-core';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!);

const databaseQueryTool: Tool = {
  name: 'query_database',
  description: 'Query the product database. Use this to look up product details, inventory, or pricing.',
  parameters: {
    type: 'object',
    properties: {
      table: { type: 'string', description: 'Table name to query' },
      filters: { type: 'object', description: 'Key-value pairs for WHERE clauses' },
      limit: { type: 'number', description: 'Max rows to return' },
    },
    required: ['table'],
  },
  execute: async ({ table, filters = {}, limit = 10 }) => {
    let query = supabase.from(table).select('*').limit(limit);
    for (const [key, value] of Object.entries(filters)) {
      query = query.eq(key, value);
    }
    const { data, error } = await query;
    if (error) throw new Error(error.message);
    return JSON.stringify(data);
  },
};

Error Handling in Tools

Robust tools handle errors gracefully. There are two strategies:

  • Throw errors -- CoFounder catches them and passes the error message back to the agent, which can decide to retry or use a different approach.
  • Return error objects -- For expected failure modes, return a structured error response so the agent can reason about it.

The best practice is to throw for unexpected errors (network failures, auth issues) and return structured messages for expected ones (no results found, invalid input):

execute: async ({ url }) => {
  try {
    const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
    if (!response.ok) {
      return JSON.stringify({
        error: true,
        message: `HTTP ${response.status}: ${response.statusText}`,
        suggestion: 'Try a different URL or check if the site is accessible.',
      });
    }
    const html = await response.text();
    return JSON.stringify({ content: extractText(html) });
  } catch (err) {
    if (err instanceof DOMException && err.name === 'TimeoutError') {
      return JSON.stringify({ error: true, message: 'Request timed out after 10 seconds.' });
    }
    throw err; // Unexpected error -- let CoFounder handle it
  }
}