- Add download trend sparkline chart showing 30-day download history - Add bundle size component with minified/gzipped sizes via bundlephobia proxy - Add more install commands: yarn, bun, deno (in addition to npm, pnpm) - Change discovery keyword from "tpmjs-tool" to "tpmjs" across entire codebase - Update sync endpoints to use new keyword - Update all documentation and package.json files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1203 lines
32 KiB
Markdown
1203 lines
32 KiB
Markdown
# AI SDK 6 Tool Execution: Override & Extend Patterns
|
|
|
|
> Comprehensive guide for customizing tool execution in TPMJS with Vercel AI SDK 6
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [AI SDK 6 Tool Architecture](#ai-sdk-6-tool-architecture)
|
|
3. [How TPMJS Tool Execution Works](#how-tpmjs-tool-execution-works)
|
|
4. [Override Patterns](#override-patterns)
|
|
5. [Extension Patterns](#extension-patterns)
|
|
6. [Middleware Approach](#middleware-approach)
|
|
7. [Factory Pattern Implementation](#factory-pattern-implementation)
|
|
8. [Complete Examples](#complete-examples)
|
|
9. [Best Practices](#best-practices)
|
|
10. [API Reference](#api-reference)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
When using tools from the TPMJS registry, developers have different needs:
|
|
|
|
| Use Case | Approach |
|
|
|----------|----------|
|
|
| **Use default execution** | Import and use `registryExecuteTool` directly |
|
|
| **Modify parameters** | Wrap the tool with custom logic |
|
|
| **Add logging/telemetry** | Use middleware pattern |
|
|
| **Replace execution entirely** | Create custom tool with same schema |
|
|
| **Extend with pre/post processing** | Use factory functions |
|
|
| **Add approval workflows** | Use AI SDK 6's `needsApproval` feature |
|
|
|
|
This document covers all patterns with practical examples.
|
|
|
|
---
|
|
|
|
## AI SDK 6 Tool Architecture
|
|
|
|
### The `tool()` Function
|
|
|
|
The AI SDK's `tool()` helper creates typed tool definitions:
|
|
|
|
```typescript
|
|
import { tool } from 'ai';
|
|
import { z } from 'zod';
|
|
|
|
const myTool = tool({
|
|
description: 'What the tool does',
|
|
inputSchema: z.object({
|
|
param1: z.string().describe('Parameter description'),
|
|
}),
|
|
execute: async ({ param1 }, options) => {
|
|
// options includes:
|
|
// - toolCallId: unique identifier for this call
|
|
// - messages: conversation history
|
|
// - abortSignal: for cancellation
|
|
// - experimental_context: custom data from generateText/streamText
|
|
return { result: 'value' };
|
|
},
|
|
});
|
|
```
|
|
|
|
### Execute Function Signature
|
|
|
|
```typescript
|
|
type ExecuteFunction<INPUT, OUTPUT> = (
|
|
input: INPUT,
|
|
options: {
|
|
toolCallId: string;
|
|
messages: CoreMessage[];
|
|
abortSignal: AbortSignal;
|
|
experimental_context?: unknown;
|
|
}
|
|
) => Promise<OUTPUT>;
|
|
```
|
|
|
|
### AI SDK 6 New Features
|
|
|
|
**Tool Approval System:**
|
|
```typescript
|
|
const sensitiveOperation = tool({
|
|
description: 'Performs a sensitive operation',
|
|
inputSchema: z.object({ action: z.string() }),
|
|
needsApproval: true, // Requires user confirmation
|
|
execute: async ({ action }) => {
|
|
// Only runs after approval
|
|
},
|
|
});
|
|
```
|
|
|
|
**Dynamic Approval Based on Input:**
|
|
```typescript
|
|
const transferTool = tool({
|
|
description: 'Transfer funds',
|
|
inputSchema: z.object({ amount: z.number() }),
|
|
needsApproval: ({ amount }) => amount > 1000, // Approve if > $1000
|
|
execute: async ({ amount }) => { /* ... */ },
|
|
});
|
|
```
|
|
|
|
**Lifecycle Hooks:**
|
|
```typescript
|
|
const toolWithHooks = tool({
|
|
description: 'Tool with lifecycle hooks',
|
|
inputSchema: z.object({ data: z.string() }),
|
|
onInputStart: () => console.log('Argument generation started'),
|
|
onInputDelta: (delta) => console.log('Streaming:', delta),
|
|
onInputAvailable: (input) => console.log('Ready to execute:', input),
|
|
execute: async ({ data }) => { /* ... */ },
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## How TPMJS Tool Execution Works
|
|
|
|
### Current Architecture
|
|
|
|
TPMJS provides two main tools:
|
|
|
|
**1. `registrySearchTool`** - Searches the registry for tools
|
|
```typescript
|
|
import { registrySearchTool } from '@tpmjs/registry-search';
|
|
|
|
// Returns tools with toolIds in format: "package::exportName"
|
|
const result = await registrySearchTool.execute({
|
|
query: 'web scraping',
|
|
category: 'web-scraping',
|
|
limit: 5,
|
|
});
|
|
```
|
|
|
|
**2. `registryExecuteTool`** - Executes tools in sandbox
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
|
|
// Executes in secure sandbox - no local installation needed
|
|
const result = await registryExecuteTool.execute({
|
|
toolId: '@anthropic/tavily-search::search',
|
|
params: { query: 'AI news' },
|
|
env: { TAVILY_API_KEY: 'your-key' },
|
|
});
|
|
```
|
|
|
|
### Execution Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ AI Agent │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ registrySearchTool │
|
|
│ Query: "scrape website" │
|
|
│ Returns: [{ toolId: "@example/scraper::scrape", ... }] │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ registryExecuteTool │
|
|
│ toolId: "@example/scraper::scrape" │
|
|
│ params: { url: "https://..." } │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ TPMJS Sandbox Executor │
|
|
│ - Fetches package from npm │
|
|
│ - Runs in isolated environment │
|
|
│ - Returns results │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Override Patterns
|
|
|
|
### Pattern 1: Complete Execute Override
|
|
|
|
Replace the entire execute function while keeping the same schema:
|
|
|
|
```typescript
|
|
import { tool, jsonSchema } from 'ai';
|
|
|
|
// Define your own execute function
|
|
const myRegistryExecuteTool = tool({
|
|
description: 'Execute a tool from the TPMJS registry with custom handling',
|
|
inputSchema: jsonSchema<{
|
|
toolId: string;
|
|
params: Record<string, unknown>;
|
|
env?: Record<string, string>;
|
|
}>({
|
|
type: 'object',
|
|
properties: {
|
|
toolId: { type: 'string' },
|
|
params: { type: 'object', additionalProperties: true },
|
|
env: { type: 'object', additionalProperties: { type: 'string' } },
|
|
},
|
|
required: ['toolId', 'params'],
|
|
}),
|
|
|
|
// YOUR CUSTOM EXECUTE FUNCTION
|
|
async execute({ toolId, params, env }) {
|
|
// Option 1: Execute locally instead of sandbox
|
|
const [packageName, exportName] = toolId.split('::');
|
|
const pkg = await import(packageName);
|
|
const toolFn = pkg[exportName];
|
|
return toolFn.execute(params);
|
|
|
|
// Option 2: Route to your own executor
|
|
// return fetch('https://your-executor.com/run', { ... });
|
|
|
|
// Option 3: Add custom logic before/after
|
|
// const result = await defaultExecute(toolId, params, env);
|
|
// return transform(result);
|
|
},
|
|
});
|
|
```
|
|
|
|
### Pattern 2: Conditional Override
|
|
|
|
Override execution based on conditions:
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
import { tool } from 'ai';
|
|
|
|
const conditionalExecuteTool = tool({
|
|
...registryExecuteTool,
|
|
async execute({ toolId, params, env }, options) {
|
|
// Route internal tools differently
|
|
if (toolId.startsWith('@internal/')) {
|
|
return executeInternally(toolId, params);
|
|
}
|
|
|
|
// Use sandbox for external tools
|
|
return registryExecuteTool.execute({ toolId, params, env });
|
|
},
|
|
});
|
|
```
|
|
|
|
### Pattern 3: Schema-Only Override
|
|
|
|
Keep the schema but provide completely custom execution:
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
import { tool } from 'ai';
|
|
|
|
// Extract just the schema
|
|
const { description, inputSchema } = registryExecuteTool;
|
|
|
|
// Create new tool with same schema, different execution
|
|
const localExecuteTool = tool({
|
|
description,
|
|
inputSchema,
|
|
async execute({ toolId, params, env }) {
|
|
// Your custom execution logic
|
|
const [pkg, fn] = toolId.split('::');
|
|
const module = await import(pkg);
|
|
return module[fn](params);
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Extension Patterns
|
|
|
|
### Pattern 1: Pre/Post Processing Wrapper
|
|
|
|
Add logic before and after the default execution:
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
import { tool } from 'ai';
|
|
|
|
const extendedExecuteTool = tool({
|
|
description: registryExecuteTool.description,
|
|
inputSchema: registryExecuteTool.inputSchema,
|
|
|
|
async execute(input, options) {
|
|
// PRE-PROCESSING
|
|
console.log(`[${new Date().toISOString()}] Executing: ${input.toolId}`);
|
|
const startTime = Date.now();
|
|
|
|
// Validate or transform input
|
|
const sanitizedParams = sanitizeInput(input.params);
|
|
|
|
try {
|
|
// ORIGINAL EXECUTION
|
|
const result = await registryExecuteTool.execute({
|
|
...input,
|
|
params: sanitizedParams,
|
|
});
|
|
|
|
// POST-PROCESSING
|
|
const duration = Date.now() - startTime;
|
|
console.log(`[SUCCESS] ${input.toolId} completed in ${duration}ms`);
|
|
|
|
// Transform output if needed
|
|
return {
|
|
...result,
|
|
_metadata: {
|
|
executedAt: new Date().toISOString(),
|
|
duration,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
// ERROR HANDLING
|
|
console.error(`[ERROR] ${input.toolId} failed:`, error);
|
|
throw error;
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
### Pattern 2: Retry Wrapper
|
|
|
|
Add automatic retry logic:
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
import { tool } from 'ai';
|
|
|
|
const retryableExecuteTool = tool({
|
|
...registryExecuteTool,
|
|
|
|
async execute(input, options) {
|
|
const maxRetries = 3;
|
|
let lastError: Error;
|
|
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
return await registryExecuteTool.execute(input);
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
console.log(`Attempt ${attempt}/${maxRetries} failed: ${lastError.message}`);
|
|
|
|
if (attempt < maxRetries) {
|
|
// Exponential backoff
|
|
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(`Failed after ${maxRetries} attempts: ${lastError!.message}`);
|
|
},
|
|
});
|
|
```
|
|
|
|
### Pattern 3: Caching Wrapper
|
|
|
|
Cache results for expensive operations:
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
import { tool } from 'ai';
|
|
|
|
const cache = new Map<string, { result: unknown; timestamp: number }>();
|
|
const CACHE_TTL = 60_000; // 1 minute
|
|
|
|
const cachedExecuteTool = tool({
|
|
...registryExecuteTool,
|
|
|
|
async execute(input, options) {
|
|
// Create cache key from input
|
|
const cacheKey = JSON.stringify(input);
|
|
const cached = cache.get(cacheKey);
|
|
|
|
// Return cached if fresh
|
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
console.log(`Cache hit for ${input.toolId}`);
|
|
return cached.result;
|
|
}
|
|
|
|
// Execute and cache
|
|
const result = await registryExecuteTool.execute(input);
|
|
cache.set(cacheKey, { result, timestamp: Date.now() });
|
|
|
|
return result;
|
|
},
|
|
});
|
|
```
|
|
|
|
### Pattern 4: Validation Wrapper
|
|
|
|
Add input/output validation:
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
import { tool } from 'ai';
|
|
import { z } from 'zod';
|
|
|
|
// Define expected output schemas for specific tools
|
|
const outputSchemas: Record<string, z.ZodType> = {
|
|
'@example/weather::getWeather': z.object({
|
|
temperature: z.number(),
|
|
conditions: z.string(),
|
|
}),
|
|
};
|
|
|
|
const validatedExecuteTool = tool({
|
|
...registryExecuteTool,
|
|
|
|
async execute(input, options) {
|
|
// Input validation
|
|
if (!input.toolId.includes('::')) {
|
|
throw new Error('Invalid toolId format. Expected "package::export"');
|
|
}
|
|
|
|
// Execute
|
|
const result = await registryExecuteTool.execute(input);
|
|
|
|
// Output validation (if schema exists)
|
|
const schema = outputSchemas[input.toolId];
|
|
if (schema) {
|
|
const parsed = schema.safeParse(result.output);
|
|
if (!parsed.success) {
|
|
console.warn(`Output validation failed for ${input.toolId}:`, parsed.error);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Middleware Approach
|
|
|
|
AI SDK provides language model middleware, but for tool-level middleware, we can implement our own pattern.
|
|
|
|
### Tool Middleware Interface
|
|
|
|
```typescript
|
|
type ToolMiddleware<TInput, TOutput> = {
|
|
// Transform input before execution
|
|
transformInput?: (input: TInput) => TInput | Promise<TInput>;
|
|
|
|
// Wrap the execute function
|
|
wrapExecute?: (
|
|
execute: (input: TInput) => Promise<TOutput>,
|
|
input: TInput
|
|
) => Promise<TOutput>;
|
|
|
|
// Transform output after execution
|
|
transformOutput?: (output: TOutput) => TOutput | Promise<TOutput>;
|
|
|
|
// Handle errors
|
|
onError?: (error: Error, input: TInput) => void | Promise<void>;
|
|
};
|
|
```
|
|
|
|
### Middleware Implementation
|
|
|
|
```typescript
|
|
import { tool, Tool } from 'ai';
|
|
|
|
function applyMiddleware<TInput, TOutput>(
|
|
baseTool: Tool<TInput, TOutput>,
|
|
middleware: ToolMiddleware<TInput, TOutput>
|
|
): Tool<TInput, TOutput> {
|
|
return tool({
|
|
description: baseTool.description,
|
|
inputSchema: baseTool.inputSchema,
|
|
|
|
async execute(input: TInput, options) {
|
|
// Transform input
|
|
let processedInput = input;
|
|
if (middleware.transformInput) {
|
|
processedInput = await middleware.transformInput(input);
|
|
}
|
|
|
|
try {
|
|
// Execute with optional wrapper
|
|
let result: TOutput;
|
|
if (middleware.wrapExecute) {
|
|
result = await middleware.wrapExecute(
|
|
(i) => baseTool.execute(i, options),
|
|
processedInput
|
|
);
|
|
} else {
|
|
result = await baseTool.execute(processedInput, options);
|
|
}
|
|
|
|
// Transform output
|
|
if (middleware.transformOutput) {
|
|
result = await middleware.transformOutput(result);
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
if (middleware.onError) {
|
|
await middleware.onError(error as Error, processedInput);
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
### Using Middleware
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
|
|
// Logging middleware
|
|
const loggingMiddleware: ToolMiddleware<any, any> = {
|
|
transformInput: (input) => {
|
|
console.log('Input:', JSON.stringify(input, null, 2));
|
|
return input;
|
|
},
|
|
transformOutput: (output) => {
|
|
console.log('Output:', JSON.stringify(output, null, 2));
|
|
return output;
|
|
},
|
|
onError: (error, input) => {
|
|
console.error('Error executing', input.toolId, error);
|
|
},
|
|
};
|
|
|
|
// Telemetry middleware
|
|
const telemetryMiddleware: ToolMiddleware<any, any> = {
|
|
wrapExecute: async (execute, input) => {
|
|
const start = performance.now();
|
|
const result = await execute(input);
|
|
const duration = performance.now() - start;
|
|
|
|
// Send to telemetry service
|
|
trackToolExecution({
|
|
toolId: input.toolId,
|
|
duration,
|
|
success: true,
|
|
});
|
|
|
|
return result;
|
|
},
|
|
};
|
|
|
|
// Apply multiple middlewares
|
|
const enhancedTool = applyMiddleware(
|
|
applyMiddleware(registryExecuteTool, loggingMiddleware),
|
|
telemetryMiddleware
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Factory Pattern Implementation
|
|
|
|
For maximum flexibility, use a factory pattern that allows configuration at creation time.
|
|
|
|
### Factory Function
|
|
|
|
```typescript
|
|
import { tool, jsonSchema } from 'ai';
|
|
|
|
type ExecutorConfig = {
|
|
// Execution mode
|
|
mode: 'sandbox' | 'local' | 'custom';
|
|
|
|
// For 'sandbox' mode
|
|
executorUrl?: string;
|
|
|
|
// For 'local' mode
|
|
localPackagePath?: string;
|
|
|
|
// For 'custom' mode
|
|
customExecutor?: (toolId: string, params: any, env?: any) => Promise<any>;
|
|
|
|
// Common options
|
|
timeout?: number;
|
|
retries?: number;
|
|
cache?: boolean;
|
|
cacheTTL?: number;
|
|
|
|
// Hooks
|
|
onBeforeExecute?: (toolId: string, params: any) => void | Promise<void>;
|
|
onAfterExecute?: (toolId: string, result: any) => void | Promise<void>;
|
|
onError?: (toolId: string, error: Error) => void | Promise<void>;
|
|
|
|
// Approval
|
|
needsApproval?: boolean | ((input: any) => boolean);
|
|
};
|
|
|
|
function createRegistryExecuteTool(config: ExecutorConfig) {
|
|
const {
|
|
mode,
|
|
executorUrl = 'https://executor.tpmjs.com',
|
|
timeout = 30000,
|
|
retries = 0,
|
|
cache = false,
|
|
cacheTTL = 60000,
|
|
onBeforeExecute,
|
|
onAfterExecute,
|
|
onError,
|
|
needsApproval,
|
|
} = config;
|
|
|
|
const resultCache = new Map<string, { data: any; timestamp: number }>();
|
|
|
|
return tool({
|
|
description: 'Execute a tool from the TPMJS registry',
|
|
inputSchema: jsonSchema<{
|
|
toolId: string;
|
|
params: Record<string, unknown>;
|
|
env?: Record<string, string>;
|
|
}>({
|
|
type: 'object',
|
|
properties: {
|
|
toolId: { type: 'string' },
|
|
params: { type: 'object', additionalProperties: true },
|
|
env: { type: 'object', additionalProperties: { type: 'string' } },
|
|
},
|
|
required: ['toolId', 'params'],
|
|
}),
|
|
|
|
needsApproval,
|
|
|
|
async execute({ toolId, params, env }, options) {
|
|
// Check cache
|
|
if (cache) {
|
|
const cacheKey = JSON.stringify({ toolId, params });
|
|
const cached = resultCache.get(cacheKey);
|
|
if (cached && Date.now() - cached.timestamp < cacheTTL) {
|
|
return cached.data;
|
|
}
|
|
}
|
|
|
|
// Before hook
|
|
if (onBeforeExecute) {
|
|
await onBeforeExecute(toolId, params);
|
|
}
|
|
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
try {
|
|
let result: any;
|
|
|
|
// Execute based on mode
|
|
switch (mode) {
|
|
case 'sandbox':
|
|
result = await executeSandbox(toolId, params, env, executorUrl, timeout, options.abortSignal);
|
|
break;
|
|
|
|
case 'local':
|
|
result = await executeLocal(toolId, params, config.localPackagePath);
|
|
break;
|
|
|
|
case 'custom':
|
|
if (!config.customExecutor) {
|
|
throw new Error('customExecutor required for custom mode');
|
|
}
|
|
result = await config.customExecutor(toolId, params, env);
|
|
break;
|
|
}
|
|
|
|
// After hook
|
|
if (onAfterExecute) {
|
|
await onAfterExecute(toolId, result);
|
|
}
|
|
|
|
// Cache result
|
|
if (cache) {
|
|
const cacheKey = JSON.stringify({ toolId, params });
|
|
resultCache.set(cacheKey, { data: result, timestamp: Date.now() });
|
|
}
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
if (onError) {
|
|
await onError(toolId, lastError);
|
|
}
|
|
|
|
if (attempt < retries) {
|
|
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
},
|
|
});
|
|
}
|
|
|
|
// Helper functions
|
|
async function executeSandbox(
|
|
toolId: string,
|
|
params: any,
|
|
env: any,
|
|
executorUrl: string,
|
|
timeout: number,
|
|
abortSignal?: AbortSignal
|
|
) {
|
|
const [packageName, exportName] = toolId.split('::');
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
|
|
// Combine abort signals
|
|
const combinedSignal = abortSignal
|
|
? anySignal([abortSignal, controller.signal])
|
|
: controller.signal;
|
|
|
|
try {
|
|
const response = await fetch(`${executorUrl}/execute-tool`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ packageName, exportName, params, env }),
|
|
signal: combinedSignal,
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Execution failed');
|
|
}
|
|
|
|
return result;
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
|
|
async function executeLocal(
|
|
toolId: string,
|
|
params: any,
|
|
basePath?: string
|
|
) {
|
|
const [packageName, exportName] = toolId.split('::');
|
|
const modulePath = basePath ? `${basePath}/${packageName}` : packageName;
|
|
|
|
const module = await import(modulePath);
|
|
const toolFn = module[exportName] || module.default;
|
|
|
|
if (!toolFn?.execute) {
|
|
throw new Error(`Tool ${exportName} not found or missing execute function`);
|
|
}
|
|
|
|
return toolFn.execute(params);
|
|
}
|
|
|
|
function anySignal(signals: AbortSignal[]): AbortSignal {
|
|
const controller = new AbortController();
|
|
for (const signal of signals) {
|
|
if (signal.aborted) {
|
|
controller.abort();
|
|
return controller.signal;
|
|
}
|
|
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
}
|
|
return controller.signal;
|
|
}
|
|
```
|
|
|
|
### Using the Factory
|
|
|
|
```typescript
|
|
// Default sandbox execution
|
|
const defaultTool = createRegistryExecuteTool({
|
|
mode: 'sandbox',
|
|
});
|
|
|
|
// Local execution with caching
|
|
const localTool = createRegistryExecuteTool({
|
|
mode: 'local',
|
|
localPackagePath: './node_modules',
|
|
cache: true,
|
|
cacheTTL: 300000, // 5 minutes
|
|
});
|
|
|
|
// Custom execution with telemetry
|
|
const customTool = createRegistryExecuteTool({
|
|
mode: 'custom',
|
|
customExecutor: async (toolId, params, env) => {
|
|
// Your custom execution logic
|
|
return myCustomExecutor.run(toolId, params, env);
|
|
},
|
|
retries: 2,
|
|
onBeforeExecute: (toolId) => {
|
|
metrics.increment(`tool.${toolId}.invocations`);
|
|
},
|
|
onAfterExecute: (toolId, result) => {
|
|
metrics.histogram(`tool.${toolId}.duration`, result.executionTimeMs);
|
|
},
|
|
onError: (toolId, error) => {
|
|
errorTracker.capture(error, { toolId });
|
|
},
|
|
});
|
|
|
|
// With approval for sensitive tools
|
|
const approvedTool = createRegistryExecuteTool({
|
|
mode: 'sandbox',
|
|
needsApproval: (input) => {
|
|
// Require approval for tools that modify data
|
|
const sensitiveTools = ['database', 'file-operations', 'communication'];
|
|
return sensitiveTools.some(cat => input.toolId.includes(cat));
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Complete Examples
|
|
|
|
### Example 1: Full Agent with Custom Execution
|
|
|
|
```typescript
|
|
import { generateText } from 'ai';
|
|
import { openai } from '@ai-sdk/openai';
|
|
import { registrySearchTool } from '@tpmjs/registry-search';
|
|
|
|
// Custom execute tool with logging and metrics
|
|
const myExecuteTool = createRegistryExecuteTool({
|
|
mode: 'sandbox',
|
|
retries: 2,
|
|
cache: true,
|
|
onBeforeExecute: async (toolId, params) => {
|
|
console.log(`Executing ${toolId}...`);
|
|
await analytics.track('tool_execution_start', { toolId });
|
|
},
|
|
onAfterExecute: async (toolId, result) => {
|
|
console.log(`${toolId} completed in ${result.executionTimeMs}ms`);
|
|
await analytics.track('tool_execution_complete', {
|
|
toolId,
|
|
duration: result.executionTimeMs,
|
|
});
|
|
},
|
|
onError: async (toolId, error) => {
|
|
await errorReporting.capture(error, { toolId });
|
|
},
|
|
});
|
|
|
|
// Use in agent
|
|
const result = await generateText({
|
|
model: openai('gpt-4o'),
|
|
tools: {
|
|
searchRegistry: registrySearchTool,
|
|
executeTool: myExecuteTool,
|
|
},
|
|
maxSteps: 10,
|
|
prompt: 'Find a tool to scrape https://example.com and extract all links',
|
|
});
|
|
|
|
console.log(result.text);
|
|
```
|
|
|
|
### Example 2: Local Development Override
|
|
|
|
```typescript
|
|
import { tool } from 'ai';
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
|
|
// In development, use local packages instead of sandbox
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
|
|
const executeTool = isDev
|
|
? tool({
|
|
...registryExecuteTool,
|
|
async execute({ toolId, params, env }) {
|
|
console.log('[DEV] Executing locally:', toolId);
|
|
|
|
const [pkg, fn] = toolId.split('::');
|
|
|
|
// Load from local node_modules
|
|
const module = await import(pkg);
|
|
const toolFn = module[fn] || module.default;
|
|
|
|
// Set env vars for local execution
|
|
Object.entries(env || {}).forEach(([key, value]) => {
|
|
process.env[key] = value;
|
|
});
|
|
|
|
return toolFn.execute(params);
|
|
},
|
|
})
|
|
: registryExecuteTool;
|
|
|
|
export { executeTool };
|
|
```
|
|
|
|
### Example 3: Multi-Provider Execution
|
|
|
|
```typescript
|
|
import { tool, jsonSchema } from 'ai';
|
|
|
|
type ExecutionProvider = 'tpmjs-sandbox' | 'aws-lambda' | 'local';
|
|
|
|
const providers: Record<ExecutionProvider, (toolId: string, params: any, env?: any) => Promise<any>> = {
|
|
'tpmjs-sandbox': async (toolId, params, env) => {
|
|
const response = await fetch('https://executor.tpmjs.com/execute-tool', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ toolId, params, env }),
|
|
});
|
|
return response.json();
|
|
},
|
|
|
|
'aws-lambda': async (toolId, params, env) => {
|
|
const lambda = new AWS.Lambda();
|
|
const result = await lambda.invoke({
|
|
FunctionName: `tpmjs-${toolId.replace('::', '-')}`,
|
|
Payload: JSON.stringify({ params, env }),
|
|
}).promise();
|
|
return JSON.parse(result.Payload as string);
|
|
},
|
|
|
|
'local': async (toolId, params, env) => {
|
|
const [pkg, fn] = toolId.split('::');
|
|
const module = await import(pkg);
|
|
return module[fn].execute(params);
|
|
},
|
|
};
|
|
|
|
// Provider selection based on tool or configuration
|
|
function selectProvider(toolId: string): ExecutionProvider {
|
|
// Route specific tools to specific providers
|
|
if (toolId.startsWith('@internal/')) return 'local';
|
|
if (toolId.startsWith('@aws/')) return 'aws-lambda';
|
|
return 'tpmjs-sandbox';
|
|
}
|
|
|
|
const multiProviderExecuteTool = tool({
|
|
description: 'Execute a tool using the optimal provider',
|
|
inputSchema: jsonSchema<{
|
|
toolId: string;
|
|
params: Record<string, unknown>;
|
|
env?: Record<string, string>;
|
|
provider?: ExecutionProvider;
|
|
}>({
|
|
type: 'object',
|
|
properties: {
|
|
toolId: { type: 'string' },
|
|
params: { type: 'object' },
|
|
env: { type: 'object' },
|
|
provider: {
|
|
type: 'string',
|
|
enum: ['tpmjs-sandbox', 'aws-lambda', 'local']
|
|
},
|
|
},
|
|
required: ['toolId', 'params'],
|
|
}),
|
|
|
|
async execute({ toolId, params, env, provider }) {
|
|
const selectedProvider = provider || selectProvider(toolId);
|
|
console.log(`Executing ${toolId} via ${selectedProvider}`);
|
|
|
|
return providers[selectedProvider](toolId, params, env);
|
|
},
|
|
});
|
|
```
|
|
|
|
### Example 4: Combining with Your Own Tools
|
|
|
|
```typescript
|
|
import { generateText } from 'ai';
|
|
import { openai } from '@ai-sdk/openai';
|
|
import { registrySearchTool } from '@tpmjs/registry-search';
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
import { tool } from 'ai';
|
|
import { z } from 'zod';
|
|
|
|
// Your own custom tools
|
|
const myDatabaseTool = tool({
|
|
description: 'Query your internal database',
|
|
inputSchema: z.object({
|
|
query: z.string(),
|
|
}),
|
|
execute: async ({ query }) => {
|
|
// Your database logic
|
|
return db.query(query);
|
|
},
|
|
});
|
|
|
|
const myNotificationTool = tool({
|
|
description: 'Send a notification',
|
|
inputSchema: z.object({
|
|
message: z.string(),
|
|
channel: z.enum(['slack', 'email', 'sms']),
|
|
}),
|
|
execute: async ({ message, channel }) => {
|
|
// Your notification logic
|
|
return notificationService.send(channel, message);
|
|
},
|
|
});
|
|
|
|
// Combine everything
|
|
const result = await generateText({
|
|
model: openai('gpt-4o'),
|
|
tools: {
|
|
// TPMJS registry tools
|
|
searchRegistry: registrySearchTool,
|
|
executeTool: registryExecuteTool,
|
|
|
|
// Your own tools
|
|
queryDatabase: myDatabaseTool,
|
|
sendNotification: myNotificationTool,
|
|
},
|
|
maxSteps: 15,
|
|
prompt: `
|
|
1. Search for a web scraping tool
|
|
2. Use it to scrape https://news.ycombinator.com
|
|
3. Query our database for relevant past articles
|
|
4. Send me a Slack notification with the summary
|
|
`,
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### 1. Error Handling
|
|
|
|
Always wrap tool execution in proper error handling:
|
|
|
|
```typescript
|
|
const robustExecuteTool = tool({
|
|
...registryExecuteTool,
|
|
async execute(input, options) {
|
|
try {
|
|
return await registryExecuteTool.execute(input);
|
|
} catch (error) {
|
|
// Log for debugging
|
|
console.error(`Tool ${input.toolId} failed:`, error);
|
|
|
|
// Return structured error instead of throwing
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
toolId: input.toolId,
|
|
};
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
### 2. Timeout Handling
|
|
|
|
Implement timeouts to prevent hanging:
|
|
|
|
```typescript
|
|
async function executeWithTimeout<T>(
|
|
promise: Promise<T>,
|
|
timeoutMs: number
|
|
): Promise<T> {
|
|
const timeout = new Promise<never>((_, reject) => {
|
|
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs);
|
|
});
|
|
|
|
return Promise.race([promise, timeout]);
|
|
}
|
|
|
|
const timeoutExecuteTool = tool({
|
|
...registryExecuteTool,
|
|
async execute(input, options) {
|
|
return executeWithTimeout(
|
|
registryExecuteTool.execute(input),
|
|
30000 // 30 second timeout
|
|
);
|
|
},
|
|
});
|
|
```
|
|
|
|
### 3. Rate Limiting
|
|
|
|
Prevent overwhelming the executor:
|
|
|
|
```typescript
|
|
import { RateLimiter } from 'limiter';
|
|
|
|
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: 'second' });
|
|
|
|
const rateLimitedTool = tool({
|
|
...registryExecuteTool,
|
|
async execute(input, options) {
|
|
await limiter.removeTokens(1);
|
|
return registryExecuteTool.execute(input);
|
|
},
|
|
});
|
|
```
|
|
|
|
### 4. Security Considerations
|
|
|
|
- **Validate toolIds**: Ensure they match expected patterns
|
|
- **Sanitize params**: Remove potentially dangerous values
|
|
- **Limit env vars**: Only pass necessary environment variables
|
|
- **Use approval**: For sensitive operations, require human approval
|
|
|
|
```typescript
|
|
const secureTool = tool({
|
|
...registryExecuteTool,
|
|
needsApproval: (input) => {
|
|
// Require approval for tools that access sensitive resources
|
|
const sensitivePatterns = [
|
|
/database/i,
|
|
/file/i,
|
|
/email/i,
|
|
/payment/i,
|
|
];
|
|
return sensitivePatterns.some(p => p.test(input.toolId));
|
|
},
|
|
async execute(input, options) {
|
|
// Validate toolId format
|
|
if (!/^@[\w-]+\/[\w-]+::\w+$/.test(input.toolId)) {
|
|
throw new Error('Invalid toolId format');
|
|
}
|
|
|
|
// Whitelist allowed env vars
|
|
const allowedEnvVars = ['API_KEY', 'ACCESS_TOKEN', 'SECRET'];
|
|
const filteredEnv = Object.fromEntries(
|
|
Object.entries(input.env || {}).filter(([key]) =>
|
|
allowedEnvVars.some(allowed => key.includes(allowed))
|
|
)
|
|
);
|
|
|
|
return registryExecuteTool.execute({
|
|
...input,
|
|
env: filteredEnv,
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## API Reference
|
|
|
|
### `registrySearchTool`
|
|
|
|
```typescript
|
|
import { registrySearchTool } from '@tpmjs/registry-search';
|
|
|
|
// Input
|
|
type SearchInput = {
|
|
query: string; // Search keywords
|
|
category?: string; // Filter by category
|
|
limit?: number; // Max results (1-20, default 5)
|
|
};
|
|
|
|
// Output
|
|
type SearchOutput = {
|
|
query: string;
|
|
matchCount: number;
|
|
tools: Array<{
|
|
toolId: string; // "package::exportName"
|
|
name: string;
|
|
package: string;
|
|
description: string;
|
|
category: string;
|
|
requiredEnvVars: string[];
|
|
healthStatus: string;
|
|
qualityScore: number;
|
|
}>;
|
|
};
|
|
```
|
|
|
|
### `registryExecuteTool`
|
|
|
|
```typescript
|
|
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
|
|
|
// Input
|
|
type ExecuteInput = {
|
|
toolId: string; // "package::exportName"
|
|
params: Record<string, unknown>; // Tool parameters
|
|
env?: Record<string, string>; // Environment variables
|
|
};
|
|
|
|
// Output
|
|
type ExecuteOutput = {
|
|
toolId: string;
|
|
executionTimeMs: number;
|
|
output: unknown; // Tool-specific output
|
|
};
|
|
```
|
|
|
|
### Environment Variables
|
|
|
|
| Variable | Description | Default |
|
|
|----------|-------------|---------|
|
|
| `TPMJS_API_URL` | Registry API URL | `https://tpmjs.com` |
|
|
| `TPMJS_EXECUTOR_URL` | Sandbox executor URL | `https://executor.tpmjs.com` |
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- [AI SDK Tool Reference](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool)
|
|
- [AI SDK Tool Calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
|
|
- [AI SDK Middleware](https://ai-sdk.dev/docs/ai-sdk-core/middleware)
|
|
- [AI SDK 6 Beta Announcement](https://v6.ai-sdk.dev/docs/announcing-ai-sdk-6-beta)
|
|
- [Dynamic Tools](https://ai-sdk.dev/docs/reference/ai-sdk-core/dynamic-tool)
|
|
- [MCP Tools Integration](https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools)
|
|
- [AI SDK Workflow Patterns](https://ai-sdk.dev/docs/agents/workflows)
|