tpmjs/docs/OVERRIDING_NPM_TOOL_EXECUTE.md
Ajax Davis 1b655571cc docs: add guide for overriding execute functions in npm tools
Focused documentation on customizing tool execution when importing
tools from npm packages. Covers 10 patterns:

- Simple override with spread
- Wrap with pre/post processing
- Transform inputs/outputs
- Add authentication/API keys
- Conditional execution
- Retry logic
- Timeout handling
- Rate limiting
- Validation layer
- Wrapper factory for reusable patterns

Includes TypeScript typing guidance and real-world examples.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 12:48:06 +10:00

628 lines
14 KiB
Markdown

# 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<Result>;
};
```
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<T, R> = {
before?: (args: T) => T | Promise<T>;
after?: (result: R, args: T) => R | Promise<R>;
onError?: (error: Error, args: T) => R | Promise<R>;
timeout?: number;
retries?: number;
};
function wrapTool<T, R>(
tool: { description: string; parameters: any; execute: (args: T, opts: any) => Promise<R> },
options: WrapperOptions<T, R> = {}
) {
return {
...tool,
execute: async (args: T, execOptions: any): Promise<R> => {
// 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<never>((_, 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<T extends Record<string, any>>(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<T extends { execute: (...args: any[]) => any }>(
originalTool: T
): T {
return {
...originalTool,
execute: async (...args: Parameters<T['execute']>) => {
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<string, { data: any; expiry: number }>();
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
},
};
```