LoginSign up
GitHub

Tools

Tools in Lightfast Core extend the capabilities of agents by providing access to external systems, APIs, and computations. Unlike traditional tool implementations, Lightfast tools can access runtime context including user information, session data, and request metadata.

The Tool System

Lightfast Core provides two ways to create tools:

  1. Tool Factories - Functions that create tools with access to runtime context
  2. Static Tools - Traditional Vercel AI SDK tools without context access

Creating Tools with Context

The createTool function creates tool factories that receive runtime context:

import { createTool } from "lightfast/tool";
import { z } from "zod";

export const searchTool = createTool({
  description: "Search for information on the web",
  inputSchema: z.object({
    query: z.string().describe("The search query"),
    limit: z.number().optional().default(5),
  }),
  outputSchema: z.object({
    results: z.array(z.object({
      title: z.string(),
      url: z.string(),
      snippet: z.string(),
    })),
  }),
  execute: async ({ query, limit }, context) => {
    // Access runtime context
    console.log("User:", context.userId);
    console.log("Session:", context.sessionId);
    console.log("IP:", context.ipAddress);
    
    // Perform the search
    const results = await performSearch(query, {
      userId: context.userId,
      limit,
    });
    
    return { results };
  },
});

Runtime Context

Tools receive a merged context containing:

interface RuntimeContext {
  // System context (always present)
  sessionId: string;
  resourceId: string;
  
  // Request context (from HTTP handler)
  userAgent?: string;
  ipAddress?: string;
  
  // Agent-specific context (from createRuntimeContext)
  // Your custom fields...
}

This enables tools to:

  • Track usage per user
  • Apply user-specific permissions
  • Access session state
  • Log with proper context

Tool Factory Pattern

Tool factories are functions that return configured tools:

// Define a tool factory with typed context
export const fileWriteTool = createTool<AppRuntimeContext>({
  description: "Write content to a file",
  inputSchema: z.object({
    filename: z.string(),
    content: z.string(),
    contentType: z.string().optional(),
  }),
  execute: async ({ filename, content, contentType }, context) => {
    // Organize files by session
    const fullPath = `sessions/${context.sessionId}/${filename}`;
    
    // Use context for authorization
    if (!context.canWriteFiles) {
      throw new Error("User not authorized to write files");
    }
    
    // Write the file
    await storage.put(fullPath, content, {
      metadata: {
        userId: context.userId,
        timestamp: Date.now(),
      },
    });
    
    return { 
      path: fullPath,
      size: content.length,
    };
  },
});

Tool Sets

Tools are organized into tool sets when creating agents:

const agent = createAgent({
  name: "assistant",
  tools: {
    // Tool factories
    search: searchTool,
    fileWrite: fileWriteTool,
    fileRead: fileReadTool,
    
    // Can mix with static tools
    calculator: calculatorTool,
  },
  // ... rest of config
});

Dynamic Tool Selection

Tools can be conditionally included based on context:

const agent = createAgent({
  tools: (context) => {
    const baseTools = {
      search: searchTool,
      calculate: calculatorTool,
    };
    
    // Add tools based on user permissions
    if (context.isPremium) {
      return {
        ...baseTools,
        advancedAnalysis: analysisTool,
        export: exportTool,
      };
    }
    
    return baseTools;
  },
  createRuntimeContext: ({ resourceId }) => ({
    isPremium: checkPremiumStatus(resourceId),
  }),
});

Input Validation

Use Zod schemas for robust input validation:

const complexTool = createTool({
  description: "Process complex data",
  inputSchema: z.object({
    action: z.enum(["create", "update", "delete"]),
    data: z.object({
      id: z.string().uuid(),
      name: z.string().min(1).max(100),
      tags: z.array(z.string()).optional(),
      metadata: z.record(z.unknown()).optional(),
    }),
    options: z.object({
      dryRun: z.boolean().default(false),
      validate: z.boolean().default(true),
    }).optional(),
  }),
  execute: async (input, context) => {
    // Input is fully typed and validated
    if (input.options?.dryRun) {
      return { simulated: true };
    }
    // Process the action
  },
});

Output Schemas

Define output schemas for type safety and documentation:

const analysisTool = createTool({
  description: "Analyze data and return insights",
  inputSchema: z.object({
    data: z.array(z.number()),
  }),
  outputSchema: z.object({
    mean: z.number(),
    median: z.number(),
    stdDev: z.number(),
    insights: z.array(z.string()),
  }),
  execute: async ({ data }) => {
    const stats = calculateStatistics(data);
    return {
      mean: stats.mean,
      median: stats.median,
      stdDev: stats.stdDev,
      insights: generateInsights(stats),
    };
  },
});

Error Handling

Tools should handle errors gracefully:

const apiTool = createTool({
  description: "Call external API",
  inputSchema: z.object({
    endpoint: z.string(),
    method: z.enum(["GET", "POST"]),
    body: z.any().optional(),
  }),
  execute: async ({ endpoint, method, body }, context) => {
    try {
      const response = await fetch(endpoint, {
        method,
        body: body ? JSON.stringify(body) : undefined,
        headers: {
          "X-User-Id": context.userId,
          "X-Session-Id": context.sessionId,
        },
      });
      
      if (!response.ok) {
        return {
          error: `API returned ${response.status}`,
          status: response.status,
        };
      }
      
      return {
        data: await response.json(),
        status: response.status,
      };
    } catch (error) {
      // Return error information for the model to handle
      return {
        error: error instanceof Error ? error.message : "Unknown error",
        status: 0,
      };
    }
  },
});

Async Operations

Tools naturally support async operations:

const longRunningTool = createTool({
  description: "Start a long-running process",
  inputSchema: z.object({
    taskId: z.string(),
    parameters: z.record(z.unknown()),
  }),
  execute: async ({ taskId, parameters }, context) => {
    // Start async job
    const jobId = await queue.enqueue({
      task: taskId,
      parameters,
      userId: context.userId,
      sessionId: context.sessionId,
    });
    
    // Poll for completion (with timeout)
    const result = await pollJobCompletion(jobId, {
      maxWaitTime: 30000, // 30 seconds
      pollInterval: 1000,
    });
    
    if (!result) {
      return {
        status: "pending",
        jobId,
        message: "Job is still running. Check back later.",
      };
    }
    
    return {
      status: "completed",
      result: result.data,
    };
  },
});

Tool Composition

Build complex tools from simpler ones:

const searchAndSummarizeTool = createTool({
  description: "Search and summarize results",
  inputSchema: z.object({
    query: z.string(),
    maxResults: z.number().default(5),
  }),
  execute: async ({ query, maxResults }, context) => {
    // Use the search tool
    const searchResults = await searchTool(context).execute({
      query,
      limit: maxResults,
    });
    
    // Use the summarize tool
    const summary = await summarizeTool(context).execute({
      text: searchResults.results
        .map(r => r.snippet)
        .join("\n"),
    });
    
    return {
      query,
      resultCount: searchResults.results.length,
      summary: summary.text,
      sources: searchResults.results.map(r => r.url),
    };
  },
});

Observability

Add logging and tracing to tools:

import { wrapTraced, currentSpan } from "braintrust";

const tracedTool = createTool({
  description: "Tool with observability",
  inputSchema: z.object({
    input: z.string(),
  }),
  execute: wrapTraced(async function execute({ input }, context) {
    // Log to current span
    currentSpan().log({
      metadata: {
        toolName: "tracedTool",
        userId: context.userId,
        sessionId: context.sessionId,
        inputLength: input.length,
      },
    });
    
    try {
      const result = await processInput(input);
      
      // Log success
      currentSpan().log({
        output: result,
        success: true,
      });
      
      return result;
    } catch (error) {
      // Log error
      currentSpan().log({
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }),
});

Security Considerations

Input Sanitization

Always sanitize user inputs:

const fileTool = createTool({
  inputSchema: z.object({
    path: z.string(),
  }),
  execute: async ({ path }, context) => {
    // Prevent path traversal
    const safePath = path.replace(/\.\./g, "");
    
    // Scope to user directory
    const fullPath = `users/${context.userId}/${safePath}`;
    
    // Additional validation
    if (!isValidPath(fullPath)) {
      throw new Error("Invalid path");
    }
    
    return await readFile(fullPath);
  },
});

Rate Limiting

Implement rate limiting for expensive operations:

const expensiveTool = createTool({
  execute: async (input, context) => {
    // Check rate limit
    const limited = await rateLimiter.check({
      key: `tool:expensive:${context.userId}`,
      limit: 10,
      window: 3600, // 1 hour
    });
    
    if (limited) {
      return {
        error: "Rate limit exceeded",
        retryAfter: limited.resetTime,
      };
    }
    
    // Perform expensive operation
    return await performExpensiveOperation(input);
  },
});

Authorization

Check permissions before executing:

const adminTool = createTool({
  execute: async (input, context) => {
    // Check admin permission
    if (!context.isAdmin) {
      return {
        error: "Unauthorized",
        message: "Admin access required",
      };
    }
    
    // Execute admin operation
    return await performAdminOperation(input);
  },
});

Best Practices

1. Keep Tools Focused

Each tool should do one thing well:

// Good: Single responsibility
const readFileTool = createTool({
  description: "Read a file's contents",
  // ...
});

// Bad: Multiple responsibilities
const fileManagerTool = createTool({
  description: "Read, write, delete, and analyze files",
  // ...
});

2. Provide Clear Descriptions

Help the model understand when to use your tool:

const tool = createTool({
  description: `Search for recent news articles.
    Use this when users ask about current events, news, or recent developments.
    Returns up to 10 articles from the last 7 days.`,
  // ...
});

3. Return Structured Data

Return consistent, structured responses:

// Good: Structured response
return {
  success: true,
  data: results,
  count: results.length,
  timestamp: Date.now(),
};

// Less ideal: Unstructured
return results;

4. Handle Partial Failures

Return partial results when possible:

const batchTool = createTool({
  execute: async ({ items }, context) => {
    const results = await Promise.allSettled(
      items.map(item => processItem(item))
    );
    
    return {
      successful: results.filter(r => r.status === "fulfilled"),
      failed: results.filter(r => r.status === "rejected"),
      summary: `Processed ${results.filter(r => r.status === "fulfilled").length}/${items.length} items`,
    };
  },
});

5. Document Edge Cases

Make tool behavior clear:

const searchTool = createTool({
  description: `Search for information.
    - Returns empty array if no results found
    - Limits results to 100 items maximum
    - Searches only public content
    - Results are sorted by relevance`,
  // ...
});

Type Safety

Leverage TypeScript for type-safe tools:

// Define types for your context
interface MyAppContext {
  userId: string;
  sessionId: string;
  permissions: string[];
  features: {
    advancedSearch: boolean;
    export: boolean;
  };
}

// Create strongly-typed tool
const typedTool = createTool<MyAppContext>({
  inputSchema: z.object({
    action: z.string(),
  }),
  execute: async ({ action }, context) => {
    // context is fully typed as MyAppContext
    if (context.features.advancedSearch) {
      // Feature-specific logic
    }
    
    return { processed: true };
  },
});

Next Steps