diff --git a/docs/OVERRIDING_NPM_TOOL_EXECUTE.md b/docs/OVERRIDING_NPM_TOOL_EXECUTE.md new file mode 100644 index 0000000..3507502 --- /dev/null +++ b/docs/OVERRIDING_NPM_TOOL_EXECUTE.md @@ -0,0 +1,628 @@ +# Overriding Execute Functions for npm Tools + +When you import an AI SDK tool from npm, it comes with a built-in `execute` function. This guide shows how to override or extend that execution before passing the tool to `generateText` or `streamText`. + +## The Problem + +You install a tool from npm: + +```bash +npm install @tpmjs/some-tool +``` + +And import it: + +```typescript +import { someTool } from '@tpmjs/some-tool'; +import { generateText } from 'ai'; + +const result = await generateText({ + model: openai('gpt-4o'), + tools: { someTool }, // Uses the package's built-in execute + prompt: 'Do something', +}); +``` + +But what if you want to: +- Add logging before/after execution +- Add authentication or API keys +- Cache results +- Transform inputs or outputs +- Replace the execution entirely +- Add rate limiting or retries + +## Understanding Tool Structure + +An AI SDK tool is an object with this shape: + +```typescript +type Tool = { + description: string; + parameters: ZodSchema; + execute: (args: T, options: ToolExecuteOptions) => Promise; +}; +``` + +The key insight: **tools are just objects**. You can spread them, override properties, and create new tools from existing ones. + +--- + +## Pattern 1: Simple Override with Spread + +The most straightforward approach - spread the original tool and override `execute`: + +```typescript +import { someTool } from '@tpmjs/some-tool'; +import { generateText } from 'ai'; + +const myTool = { + ...someTool, + execute: async (args, options) => { + console.log('Custom execution with args:', args); + // Your completely custom implementation + return { result: 'my custom result' }; + }, +}; + +const result = await generateText({ + model: openai('gpt-4o'), + tools: { myTool }, + prompt: 'Do something', +}); +``` + +**Use when:** You want to completely replace the execution logic. + +--- + +## Pattern 2: Wrap with Pre/Post Processing + +Call the original execute but add behavior before and after: + +```typescript +import { someTool } from '@tpmjs/some-tool'; + +const wrappedTool = { + ...someTool, + execute: async (args, options) => { + // PRE-PROCESSING + console.log(`[${new Date().toISOString()}] Calling tool with:`, args); + const startTime = Date.now(); + + try { + // CALL ORIGINAL + const result = await someTool.execute(args, options); + + // POST-PROCESSING + const duration = Date.now() - startTime; + console.log(`[${duration}ms] Tool returned:`, result); + + return result; + } catch (error) { + console.error('Tool execution failed:', error); + throw error; + } + }, +}; +``` + +**Use when:** You want to keep the original behavior but add logging, metrics, or transformations. + +--- + +## Pattern 3: Transform Inputs + +Modify arguments before they reach the original execute: + +```typescript +import { searchTool } from '@tpmjs/search-tool'; + +const enhancedSearchTool = { + ...searchTool, + execute: async (args, options) => { + // Transform inputs + const enhancedArgs = { + ...args, + query: `${args.query} site:example.com`, // Add search filter + limit: Math.min(args.limit || 10, 50), // Cap results + }; + + return searchTool.execute(enhancedArgs, options); + }, +}; +``` + +--- + +## Pattern 4: Transform Outputs + +Modify the result before returning: + +```typescript +import { dataTool } from '@tpmjs/data-tool'; + +const formattedDataTool = { + ...dataTool, + execute: async (args, options) => { + const result = await dataTool.execute(args, options); + + // Transform output + return { + ...result, + data: result.data.map(item => ({ + ...item, + timestamp: new Date(item.timestamp).toLocaleString(), + })), + _meta: { + fetchedAt: new Date().toISOString(), + source: 'dataTool', + }, + }; + }, +}; +``` + +--- + +## Pattern 5: Add Authentication/API Keys + +Inject credentials that the original tool needs: + +```typescript +import { apiTool } from '@tpmjs/api-tool'; + +const authenticatedTool = { + ...apiTool, + execute: async (args, options) => { + // Inject API key into args + const argsWithAuth = { + ...args, + apiKey: process.env.EXTERNAL_API_KEY, + headers: { + ...args.headers, + 'Authorization': `Bearer ${process.env.AUTH_TOKEN}`, + }, + }; + + return apiTool.execute(argsWithAuth, options); + }, +}; +``` + +--- + +## Pattern 6: Conditional Execution + +Route to different implementations based on conditions: + +```typescript +import { defaultTool } from '@tpmjs/default-tool'; + +const conditionalTool = { + ...defaultTool, + execute: async (args, options) => { + // Use mock in development + if (process.env.NODE_ENV === 'development') { + return { result: 'mock data', mocked: true }; + } + + // Use cached result if available + const cacheKey = JSON.stringify(args); + const cached = await cache.get(cacheKey); + if (cached) { + return { ...cached, fromCache: true }; + } + + // Otherwise use original + const result = await defaultTool.execute(args, options); + await cache.set(cacheKey, result, { ttl: 3600 }); + return result; + }, +}; +``` + +--- + +## Pattern 7: Add Retry Logic + +Wrap execution with automatic retries: + +```typescript +import { unreliableTool } from '@tpmjs/unreliable-tool'; + +const reliableTool = { + ...unreliableTool, + execute: async (args, options) => { + const maxRetries = 3; + let lastError: Error; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await unreliableTool.execute(args, options); + } catch (error) { + lastError = error as Error; + console.warn(`Attempt ${attempt} failed:`, error.message); + + if (attempt < maxRetries) { + // Exponential backoff + await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 100)); + } + } + } + + throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`); + }, +}; +``` + +--- + +## Pattern 8: Add Timeout + +Prevent tools from hanging: + +```typescript +import { slowTool } from '@tpmjs/slow-tool'; + +const timedTool = { + ...slowTool, + execute: async (args, options) => { + const timeoutMs = 10000; // 10 seconds + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Tool execution timed out')), timeoutMs); + }); + + return Promise.race([ + slowTool.execute(args, options), + timeoutPromise, + ]); + }, +}; +``` + +--- + +## Pattern 9: Rate Limiting + +Control how often a tool can be called: + +```typescript +import { expensiveTool } from '@tpmjs/expensive-tool'; + +// Simple rate limiter +const rateLimiter = { + calls: [] as number[], + maxCalls: 10, + windowMs: 60000, // 1 minute + + async acquire() { + const now = Date.now(); + this.calls = this.calls.filter(t => now - t < this.windowMs); + + if (this.calls.length >= this.maxCalls) { + const oldestCall = this.calls[0]; + const waitTime = this.windowMs - (now - oldestCall); + await new Promise(r => setTimeout(r, waitTime)); + return this.acquire(); + } + + this.calls.push(now); + }, +}; + +const rateLimitedTool = { + ...expensiveTool, + execute: async (args, options) => { + await rateLimiter.acquire(); + return expensiveTool.execute(args, options); + }, +}; +``` + +--- + +## Pattern 10: Validation Layer + +Add input validation before execution: + +```typescript +import { unsafeTool } from '@tpmjs/unsafe-tool'; +import { z } from 'zod'; + +// Additional validation schema +const strictSchema = z.object({ + query: z.string().min(1).max(1000), + options: z.object({ + limit: z.number().int().min(1).max(100), + }).optional(), +}); + +const validatedTool = { + ...unsafeTool, + execute: async (args, options) => { + // Validate before execution + const validated = strictSchema.parse(args); + + // Sanitize + const sanitized = { + ...validated, + query: validated.query.replace(/<[^>]*>/g, ''), // Strip HTML + }; + + return unsafeTool.execute(sanitized, options); + }, +}; +``` + +--- + +## Creating a Wrapper Factory + +For reusable patterns, create a factory function: + +```typescript +type WrapperOptions = { + before?: (args: T) => T | Promise; + after?: (result: R, args: T) => R | Promise; + onError?: (error: Error, args: T) => R | Promise; + timeout?: number; + retries?: number; +}; + +function wrapTool( + tool: { description: string; parameters: any; execute: (args: T, opts: any) => Promise }, + options: WrapperOptions = {} +) { + return { + ...tool, + execute: async (args: T, execOptions: any): Promise => { + // Transform inputs + let processedArgs = args; + if (options.before) { + processedArgs = await options.before(args); + } + + // Execute with timeout + const executeWithTimeout = async () => { + if (options.timeout) { + return Promise.race([ + tool.execute(processedArgs, execOptions), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), options.timeout) + ), + ]); + } + return tool.execute(processedArgs, execOptions); + }; + + // Execute with retries + let lastError: Error | undefined; + const maxAttempts = (options.retries ?? 0) + 1; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + let result = await executeWithTimeout(); + + // Transform outputs + if (options.after) { + result = await options.after(result, processedArgs); + } + + return result; + } catch (error) { + lastError = error as Error; + if (attempt < maxAttempts) { + await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 100)); + } + } + } + + // Handle error + if (options.onError) { + return options.onError(lastError!, processedArgs); + } + throw lastError; + }, + }; +} + +// Usage +import { someTool } from '@tpmjs/some-tool'; + +const enhancedTool = wrapTool(someTool, { + before: (args) => ({ ...args, enhanced: true }), + after: (result) => ({ ...result, processedAt: new Date() }), + timeout: 5000, + retries: 2, + onError: (error, args) => ({ error: error.message, args, fallback: true }), +}); +``` + +--- + +## Using with generateText/streamText + +All patterns work the same way with the AI SDK: + +```typescript +import { generateText, streamText } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import { originalTool } from '@tpmjs/original-tool'; + +// Create your wrapped version +const myTool = { + ...originalTool, + execute: async (args, options) => { + // Your custom logic + return originalTool.execute(args, options); + }, +}; + +// Use with generateText +const result = await generateText({ + model: openai('gpt-4o'), + tools: { myTool }, + maxSteps: 5, + prompt: 'Use the tool to do something', +}); + +// Or with streamText +const stream = streamText({ + model: openai('gpt-4o'), + tools: { myTool }, + maxSteps: 5, + prompt: 'Use the tool to do something', +}); + +for await (const chunk of stream.textStream) { + process.stdout.write(chunk); +} +``` + +--- + +## Combining Multiple Tools + +Override multiple tools at once: + +```typescript +import { toolA } from '@tpmjs/tool-a'; +import { toolB } from '@tpmjs/tool-b'; +import { toolC } from '@tpmjs/tool-c'; + +// Logging wrapper for all tools +function withLogging>(tools: T): T { + const wrapped: any = {}; + + for (const [name, tool] of Object.entries(tools)) { + wrapped[name] = { + ...tool, + execute: async (args: any, options: any) => { + console.log(`[${name}] called with:`, args); + const result = await tool.execute(args, options); + console.log(`[${name}] returned:`, result); + return result; + }, + }; + } + + return wrapped; +} + +const tools = withLogging({ toolA, toolB, toolC }); + +const result = await generateText({ + model: openai('gpt-4o'), + tools, + prompt: 'Do something', +}); +``` + +--- + +## TypeScript: Preserving Types + +To maintain type safety when wrapping tools: + +```typescript +import { tool } from 'ai'; +import { z } from 'zod'; + +// If the original tool is typed +type OriginalTool = typeof import('@tpmjs/some-tool').someTool; + +// Your wrapper preserves the type +function wrapWithLogging any }>( + originalTool: T +): T { + return { + ...originalTool, + execute: async (...args: Parameters) => { + console.log('Calling tool'); + return originalTool.execute(...args); + }, + } as T; +} + +// Usage - types are preserved +import { someTool } from '@tpmjs/some-tool'; +const wrappedTool = wrapWithLogging(someTool); +// wrappedTool has the same type as someTool +``` + +--- + +## Real-World Example: Weather Tool with Caching + +```typescript +import { weatherTool } from '@tpmjs/weather'; +import { generateText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + +// Simple in-memory cache +const cache = new Map(); + +const cachedWeatherTool = { + ...weatherTool, + execute: async (args: { location: string }, options) => { + const cacheKey = `weather:${args.location.toLowerCase()}`; + const now = Date.now(); + + // Check cache + const cached = cache.get(cacheKey); + if (cached && cached.expiry > now) { + console.log(`Cache hit for ${args.location}`); + return { ...cached.data, cached: true }; + } + + // Fetch fresh data + console.log(`Fetching weather for ${args.location}`); + const result = await weatherTool.execute(args, options); + + // Cache for 5 minutes + cache.set(cacheKey, { + data: result, + expiry: now + 5 * 60 * 1000, + }); + + return { ...result, cached: false }; + }, +}; + +// Use it +const result = await generateText({ + model: openai('gpt-4o'), + tools: { weather: cachedWeatherTool }, + prompt: 'What is the weather in San Francisco and New York?', + maxSteps: 3, +}); +``` + +--- + +## Summary + +| Pattern | Use Case | +|---------|----------| +| Simple Override | Completely replace execution | +| Wrap Pre/Post | Add logging, metrics | +| Transform Inputs | Modify arguments | +| Transform Outputs | Format results | +| Authentication | Inject API keys | +| Conditional | Mock data, caching | +| Retry | Handle flaky services | +| Timeout | Prevent hanging | +| Rate Limiting | Control API usage | +| Validation | Sanitize inputs | + +The key insight is that AI SDK tools are plain objects. You can spread them and override `execute` to customize behavior while keeping the original `description` and `parameters` schema intact. + +```typescript +const customTool = { + ...originalTool, // Keep description & parameters + execute: async (args) => { // Override execute + // Your custom logic + }, +}; +```