Tool Factories
Tool factories are functions that create tools with access to runtime context. This pattern enables secure, context-aware tool execution without global state or manual parameter passing.
Understanding Tool Factories
Traditional tools receive only their direct inputs:
// Traditional tool - no context access
const tool = tool({
execute: async ({ query }) => {
// No access to user ID, session, etc.
return search(query);
},
});
Tool factories receive runtime context:
// Tool factory - full context access
const toolFactory = createTool({
execute: async ({ query }, context) => {
// Access to userId, sessionId, and more
return search(query, { userId: context.userId });
},
});
The createTool Function
The createTool
function creates tool factories:
import { createTool } from "lightfast/tool";
import { z } from "zod";
export const myTool = createTool<RuntimeContext>({
description: "Tool description",
inputSchema: z.object({
// Input validation schema
}),
outputSchema: z.object({
// Optional output schema
}),
execute: async (input, context) => {
// Implementation with context access
},
});
Runtime Context Flow
Context flows from the request through to tool execution:
graph TD
A[HTTP Request] --> B[fetchRequestHandler]
B --> C[System Context]
B --> D[Request Context]
C --> E[Agent.createRuntimeContext]
D --> E
E --> F[Merged Context]
F --> G[Tool Factories]
G --> H[Tool Execution]
Context Layers
- System Context - Framework-provided
{ sessionId: string, resourceId: string }
- Request Context - From HTTP handler
{ userAgent?: string, ipAddress?: string }
- Runtime Context - Agent-specific
{ /* your custom fields */ }
Creating Tool Factories
Basic Tool Factory
import { createTool } from "lightfast/tool";
import { z } from "zod";
interface AppContext {
userId: string;
sessionId: string;
permissions: string[];
}
export const databaseQueryTool = createTool<AppContext>({
description: "Query the database",
inputSchema: z.object({
table: z.string(),
query: z.string(),
}),
execute: async ({ table, query }, context) => {
// Check permissions using context
if (!context.permissions.includes(`read:${table}`)) {
return { error: "Permission denied" };
}
// Execute query with user context
const results = await db.query({
sql: query,
userId: context.userId,
auditLog: true,
});
return { results };
},
});
Stateful Tool Factory
Tools can maintain session state:
export const shoppingCartTool = createTool<AppContext>({
description: "Manage shopping cart",
inputSchema: z.object({
action: z.enum(["add", "remove", "view", "checkout"]),
productId: z.string().optional(),
quantity: z.number().optional(),
}),
execute: async ({ action, productId, quantity }, context) => {
// Use session-scoped cart
const cartKey = `cart:${context.sessionId}`;
switch (action) {
case "add":
await redis.hset(cartKey, productId!, quantity!);
return { added: productId, quantity };
case "remove":
await redis.hdel(cartKey, productId!);
return { removed: productId };
case "view":
const items = await redis.hgetall(cartKey);
return { items };
case "checkout":
// Use userId for checkout
const orderId = await createOrder({
userId: context.userId,
items: await redis.hgetall(cartKey),
});
await redis.del(cartKey); // Clear cart
return { orderId };
}
},
});
Conditional Tool Factory
Tools can adapt based on context:
export const searchTool = createTool<AppContext>({
description: "Search for information",
inputSchema: z.object({
query: z.string(),
filters: z.record(z.string()).optional(),
}),
execute: async ({ query, filters }, context) => {
// Different search capabilities based on user
const searchConfig = {
query,
filters,
// Premium users get more results
limit: context.isPremium ? 50 : 10,
// Premium users can search all content
scope: context.isPremium ? "all" : "public",
// Track usage by user
userId: context.userId,
};
const results = await searchEngine.search(searchConfig);
// Log usage for analytics
await analytics.track("search", {
userId: context.userId,
query,
resultCount: results.length,
isPremium: context.isPremium,
});
return { results };
},
});
Tool Factory Sets
Organize related tool factories:
// Define your runtime context type
interface AppRuntimeContext {
userId: string;
sessionId: string;
environment: "development" | "production";
features: {
advancedSearch: boolean;
export: boolean;
};
}
// Create a set of tool factories
export const appTools = {
// File operations
fileWrite: createTool<AppRuntimeContext>({
description: "Write a file",
inputSchema: z.object({
path: z.string(),
content: z.string(),
}),
execute: async ({ path, content }, context) => {
const fullPath = `users/${context.userId}/${path}`;
await storage.write(fullPath, content);
return { path: fullPath };
},
}),
fileRead: createTool<AppRuntimeContext>({
description: "Read a file",
inputSchema: z.object({
path: z.string(),
}),
execute: async ({ path }, context) => {
const fullPath = `users/${context.userId}/${path}`;
const content = await storage.read(fullPath);
return { content };
},
}),
// Conditional tools based on features
...(process.env.ENABLE_SEARCH && {
search: createTool<AppRuntimeContext>({
description: "Search content",
inputSchema: z.object({
query: z.string(),
}),
execute: async ({ query }, context) => {
if (!context.features.advancedSearch) {
return { error: "Advanced search not enabled" };
}
return await search(query);
},
}),
}),
};
// Use in agent
const agent = createAgent({
tools: appTools,
createRuntimeContext: ({ sessionId, resourceId }): AppRuntimeContext => ({
userId: resourceId,
sessionId,
environment: process.env.NODE_ENV as "development" | "production",
features: {
advancedSearch: checkFeature(resourceId, "advancedSearch"),
export: checkFeature(resourceId, "export"),
},
}),
});
Dynamic Tool Selection
Select tools based on runtime conditions:
const agent = createAgent({
tools: (context) => {
// Base tools available to everyone
const baseTools = {
search: searchTool,
help: helpTool,
};
// Add tools based on user role
if (context.role === "admin") {
return {
...baseTools,
userManagement: userManagementTool,
systemConfig: systemConfigTool,
analytics: analyticsTool,
};
}
if (context.role === "developer") {
return {
...baseTools,
codeExecution: codeExecutionTool,
deployment: deploymentTool,
monitoring: monitoringTool,
};
}
// Regular users get base tools only
return baseTools;
},
createRuntimeContext: ({ resourceId }) => ({
role: getUserRole(resourceId),
}),
});
Wrapped Tool Execution
Add cross-cutting concerns to tool execution:
import { wrapTraced, currentSpan } from "braintrust";
// Wrap with observability
const tracedExecute = wrapTraced(
async function execute(input: any, context: AppRuntimeContext) {
// Log to tracing span
currentSpan().log({
metadata: {
toolName: "myTool",
userId: context.userId,
sessionId: context.sessionId,
input,
},
});
try {
// Tool logic
const result = await performOperation(input);
currentSpan().log({
output: result,
success: true,
});
return result;
} catch (error) {
currentSpan().log({
error: error.message,
stack: error.stack,
});
throw error;
}
}
);
export const observableTool = createTool<AppRuntimeContext>({
description: "Tool with observability",
inputSchema: schema,
execute: tracedExecute,
});
Tool Composition Patterns
Pipeline Pattern
Chain tools together:
export const analysisPipeline = createTool<AppRuntimeContext>({
description: "Complete analysis pipeline",
inputSchema: z.object({
dataSource: z.string(),
}),
execute: async ({ dataSource }, context) => {
// Step 1: Fetch data
const fetchTool = fetchDataTool(context);
const { data } = await fetchTool.execute({ source: dataSource });
// Step 2: Clean data
const cleanTool = cleanDataTool(context);
const { cleaned } = await cleanTool.execute({ data });
// Step 3: Analyze
const analyzeTool = analyzeDataTool(context);
const { insights } = await analyzeTool.execute({ data: cleaned });
// Step 4: Generate report
const reportTool = generateReportTool(context);
const { report } = await reportTool.execute({ insights });
return { report, insights };
},
});
Delegation Pattern
Tools that delegate to other tools:
export const smartQueryTool = createTool<AppRuntimeContext>({
description: "Intelligently route queries",
inputSchema: z.object({
query: z.string(),
type: z.enum(["structured", "semantic", "hybrid"]).optional(),
}),
execute: async ({ query, type }, context) => {
// Determine query type if not specified
const queryType = type || detectQueryType(query);
switch (queryType) {
case "structured":
// Use SQL tool for structured queries
const sqlTool = sqlQueryTool(context);
return sqlTool.execute({ query });
case "semantic":
// Use vector search for semantic queries
const vectorTool = vectorSearchTool(context);
return vectorTool.execute({ query });
case "hybrid":
// Combine both approaches
const [sqlResults, vectorResults] = await Promise.all([
sqlQueryTool(context).execute({ query }),
vectorSearchTool(context).execute({ query }),
]);
return mergeResults(sqlResults, vectorResults);
}
},
});
Retry Pattern
Add retry logic to tools:
export const resilientApiTool = createTool<AppRuntimeContext>({
description: "Call external API with retries",
inputSchema: z.object({
endpoint: z.string(),
data: z.any(),
}),
execute: async ({ endpoint, data }, context) => {
const maxRetries = 3;
const backoffMs = 1000;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(endpoint, {
method: "POST",
body: JSON.stringify(data),
headers: {
"X-User-Id": context.userId,
"X-Attempt": attempt.toString(),
},
});
if (response.ok) {
return await response.json();
}
// Retry on 5xx errors
if (response.status >= 500 && attempt < maxRetries) {
await new Promise(resolve =>
setTimeout(resolve, backoffMs * attempt)
);
continue;
}
// Don't retry client errors
return {
error: `API error: ${response.status}`,
};
} catch (error) {
if (attempt === maxRetries) {
return {
error: `Failed after ${maxRetries} attempts: ${error.message}`,
};
}
await new Promise(resolve =>
setTimeout(resolve, backoffMs * attempt)
);
}
}
},
});
Security Patterns
Permission Checking
export const secureTool = createTool<AppRuntimeContext>({
description: "Security-aware tool",
inputSchema: z.object({
operation: z.enum(["read", "write", "delete"]),
resource: z.string(),
}),
execute: async ({ operation, resource }, context) => {
// Check permissions
const hasPermission = await checkPermission({
userId: context.userId,
operation,
resource,
});
if (!hasPermission) {
return {
error: "Permission denied",
required: `${operation}:${resource}`,
};
}
// Audit log
await auditLog({
userId: context.userId,
operation,
resource,
timestamp: Date.now(),
sessionId: context.sessionId,
});
// Execute operation
return performSecureOperation({ operation, resource });
},
});
Input Sanitization
export const safeTool = createTool<AppRuntimeContext>({
description: "Tool with input sanitization",
inputSchema: z.object({
userInput: z.string(),
format: z.enum(["html", "markdown", "plain"]),
}),
execute: async ({ userInput, format }, context) => {
// Sanitize based on format
let sanitized: string;
switch (format) {
case "html":
sanitized = sanitizeHtml(userInput, {
allowedTags: ["p", "br", "strong", "em"],
allowedAttributes: {},
});
break;
case "markdown":
sanitized = sanitizeMarkdown(userInput);
break;
case "plain":
sanitized = stripHtml(userInput);
break;
}
// Process sanitized input
return processInput(sanitized, context);
},
});
Testing Tool Factories
Unit Testing
import { describe, it, expect } from "vitest";
describe("searchTool", () => {
it("should respect user permissions", async () => {
const mockContext: AppRuntimeContext = {
userId: "test-user",
sessionId: "test-session",
isPremium: false,
};
// Create tool with context
const tool = searchTool(mockContext);
// Execute tool
const result = await tool.execute({
query: "test query",
});
// Assert limited results for non-premium
expect(result.results).toHaveLength(10);
});
it("should provide extended results for premium users", async () => {
const premiumContext: AppRuntimeContext = {
userId: "premium-user",
sessionId: "test-session",
isPremium: true,
};
const tool = searchTool(premiumContext);
const result = await tool.execute({
query: "test query",
});
// Premium users can get more results
expect(result.results.length).toBeLessThanOrEqual(50);
});
});
Integration Testing
describe("Tool Factory Integration", () => {
it("should work with agent", async () => {
const agent = createAgent({
name: "test-agent",
system: "Test system",
model: testModel,
tools: {
search: searchTool,
},
createRuntimeContext: ({ resourceId }) => ({
userId: resourceId,
sessionId: "test",
isPremium: true,
}),
});
const { result } = await agent.stream({
sessionId: "test",
messages: [{ role: "user", content: "Search for TypeScript" }],
memory: new InMemoryMemory(),
resourceId: "test-user",
systemContext: { sessionId: "test", resourceId: "test-user" },
requestContext: {},
});
// Verify tool was called with context
expect(result).toBeDefined();
});
});
Best Practices
1. Type Your Context
Always define and use typed context:
// Good - typed context
interface MyContext {
userId: string;
permissions: string[];
}
const tool = createTool<MyContext>({
execute: async (input, context) => {
// context is fully typed
},
});
// Bad - untyped context
const tool = createTool({
execute: async (input, context: any) => {
// No type safety
},
});
2. Validate Context
Check context validity:
execute: async (input, context) => {
// Validate required context
if (!context.userId) {
return { error: "User not authenticated" };
}
if (!context.sessionId) {
return { error: "Session required" };
}
// Proceed with valid context
}
3. Handle Errors Gracefully
Return errors as data, not exceptions:
// Good - return error data
execute: async (input, context) => {
try {
const result = await riskyOperation();
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error.message,
retry: true,
};
}
}
// Less ideal - throwing errors
execute: async (input, context) => {
const result = await riskyOperation(); // May throw
return result;
}
4. Document Context Requirements
Be clear about what context is needed:
/**
* Search tool requiring authenticated context
*
* Required context fields:
* - userId: Authenticated user ID
* - sessionId: Current session
* - permissions: Array including "search" permission
*/
export const searchTool = createTool<AppContext>({
description: "Search with user context",
// ...
});
Next Steps
- Learn about Streaming for real-time responses
- Explore Memory Adapters for state management
- See Creating Agents for complete examples