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:
- Tool Factories - Functions that create tools with access to runtime context
- 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
- Explore Memory Management for stateful conversations
- Learn about Request Handlers for HTTP integration
- See Tool Factories for advanced patterns