Implements a complete dynamic tool loading system that allows the playground to discover and load tools from the TPMJS registry at runtime. **Architecture:** - Search tool package (@tpmjs/search-registry) - Searches registry for tools - Search API endpoint (/api/tools/search) - Text-based search with scoring - Pre-flight tool loading - Automatically searches and loads tools on every message - Railway executor service (Deno) - Loads tools from esm.sh via HTTP imports - Dynamic tool loader - Calls Railway to load and execute tools remotely **Key Components:** 1. Railway Executor (apps/railway-executor/) - Deno-based service that natively supports HTTP imports - Endpoints: /load-and-describe, /execute-tool, /cache/stats, /cache/clear - Deploys to Railway with deno run --allow-net --allow-env server.ts 2. Search Tool Package (packages/tools/search-registry/) - AI SDK v6 tool for searching TPMJS registry - Uses jsonSchema + inputSchema pattern - Searches /api/tools/search endpoint 3. Search API (apps/web/src/app/api/tools/search/) - Text-based search with composite scoring - Scores: text relevance + quality boost + download boost - Returns tool metadata with importUrl for dynamic loading 4. Dynamic Tool Loader (apps/playground/src/lib/dynamic-tool-loader.ts) - Calls Railway service to load tools from esm.sh - Creates tool wrappers that execute remotely - Process-level module cache + per-conversation tracking 5. Pre-flight Loading (apps/playground/src/app/api/chat/route.ts) - Automatically searches for tools on every user message - Loads top 5 matching tools before agent processes request - Merges with static tools for seamless experience **Technical Decisions:** - Deno over Node.js: Native HTTP import support without flags - Remote execution: Tools run in Railway sandbox, not Vercel - Pre-flight loading: Better UX than two-turn search pattern - Text search: BM25 had dependency issues, simple scoring works well **Environment Variables:** - RAILWAY_SERVICE_URL: https://endearing-commitment-production.up.railway.app 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
988 lines
27 KiB
Markdown
988 lines
27 KiB
Markdown
# Dynamic Tool Loading System - Product Requirements Document
|
|
|
|
## Executive Summary
|
|
|
|
Build a self-referential tool discovery system where AI agents can search the TPMJS registry, find relevant tools, and dynamically import them during conversation. This creates a "meta-tool" that makes the entire TPMJS ecosystem available to any agent at runtime.
|
|
|
|
**Core Innovation:** An AI agent can discover and load tools on-demand by searching the registry, rather than having all tools pre-loaded. This enables infinite tool extensibility without bundle size concerns.
|
|
|
|
---
|
|
|
|
## Problem Statement
|
|
|
|
### Current Limitations
|
|
|
|
1. **Static Tool Loading**: Playground requires all tools to be hardcoded in `tool-loader.ts`
|
|
2. **Bundle Size**: Loading many tools increases bundle size and initialization time
|
|
3. **Discovery Gap**: Agents can't discover new tools that match their current task
|
|
4. **Manual Updates**: Adding tools requires code changes and redeployment
|
|
|
|
### User Pain Points
|
|
|
|
- Users want agents to access the full TPMJS registry without manual configuration
|
|
- Developers want to publish tools that are immediately available to all agents
|
|
- Agents need context-aware tool selection based on the conversation
|
|
|
|
---
|
|
|
|
## Solution Overview
|
|
|
|
### The Meta-Tool: `searchTpmjsTools`
|
|
|
|
A TPMJS tool that searches the TPMJS registry and returns tool metadata needed for dynamic import.
|
|
|
|
**Flow:**
|
|
```
|
|
User: "Search Wikipedia for quantum computing"
|
|
↓
|
|
Agent: Calls searchTpmjsTools("wikipedia search")
|
|
↓
|
|
API: Returns tools matching "wikipedia" (BM25 search)
|
|
↓
|
|
Playground: Dynamically imports matching tools
|
|
↓
|
|
Agent: Now has Wikipedia tools available, uses them
|
|
```
|
|
|
|
### Key Components
|
|
|
|
1. **`@tpmjs/search-registry`** - NPM package exporting `searchTpmjsToolsTool`
|
|
2. **`/api/tools/search`** - New API endpoint with BM25 full-text search
|
|
3. **Playground Dynamic Loader** - Runtime tool import system
|
|
4. **Tool Import Strategy** - ESM CDN imports or bundled approach
|
|
|
|
---
|
|
|
|
## Technical Architecture
|
|
|
|
### Component 1: Search Tool Package
|
|
|
|
**Package:** `packages/tools/search-registry/`
|
|
|
|
```typescript
|
|
// packages/tools/search-registry/src/index.ts
|
|
import { tool } from 'ai';
|
|
import { z } from 'zod';
|
|
|
|
export const searchTpmjsToolsTool = tool({
|
|
description: 'Search the TPMJS tool registry to find AI SDK tools. Use this when you need a tool that isn\'t currently available. Returns tool metadata including package names and descriptions.',
|
|
parameters: z.object({
|
|
query: z.string().describe('Search query (e.g., "weather", "database", "wikipedia")'),
|
|
category: z.enum([
|
|
'text-analysis',
|
|
'code-generation',
|
|
'data-processing',
|
|
'image-generation',
|
|
'audio-processing',
|
|
'search',
|
|
'integration',
|
|
'other'
|
|
]).optional().describe('Filter by tool category'),
|
|
limit: z.number().min(1).max(20).default(10).describe('Max number of tools to return'),
|
|
}),
|
|
execute: async ({ query, category, limit }) => {
|
|
// Call TPMJS search API
|
|
const params = new URLSearchParams({
|
|
q: query,
|
|
limit: String(limit),
|
|
...(category && { category }),
|
|
});
|
|
|
|
const response = await fetch(
|
|
`https://tpmjs.com/api/tools/search?${params}`
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Search failed: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Return structured tool metadata
|
|
return {
|
|
query,
|
|
matchCount: data.tools.length,
|
|
tools: data.tools.map((tool: any) => ({
|
|
packageName: tool.package.npmPackageName,
|
|
exportName: tool.exportName,
|
|
description: tool.description,
|
|
category: tool.package.category,
|
|
qualityScore: tool.qualityScore,
|
|
frameworks: tool.package.frameworks,
|
|
env: tool.package.env,
|
|
})),
|
|
};
|
|
},
|
|
});
|
|
```
|
|
|
|
**Package Metadata:**
|
|
|
|
```json
|
|
{
|
|
"name": "@tpmjs/search-registry",
|
|
"version": "0.1.0",
|
|
"description": "AI SDK tool for searching the TPMJS tool registry",
|
|
"keywords": ["tpmjs-tool", "ai", "search"],
|
|
"tpmjs": {
|
|
"category": "search",
|
|
"frameworks": ["vercel-ai"],
|
|
"tools": [
|
|
{
|
|
"exportName": "searchTpmjsToolsTool",
|
|
"description": "Search the TPMJS tool registry to find AI SDK tools by keyword, category, or description. Returns tool metadata for dynamic loading.",
|
|
"parameters": [
|
|
{
|
|
"name": "query",
|
|
"type": "string",
|
|
"description": "Search query (keywords, tool names, descriptions)",
|
|
"required": true
|
|
},
|
|
{
|
|
"name": "category",
|
|
"type": "string",
|
|
"description": "Filter by category (text-analysis, search, etc.)",
|
|
"required": false
|
|
},
|
|
{
|
|
"name": "limit",
|
|
"type": "number",
|
|
"description": "Maximum number of results (1-20, default 10)",
|
|
"required": false
|
|
}
|
|
],
|
|
"returns": {
|
|
"type": "object",
|
|
"description": "Search results with tool metadata for dynamic import"
|
|
},
|
|
"aiAgent": {
|
|
"useCase": "Use this tool when you need a tool that isn't currently available. For example, if asked to search Wikipedia but you don't have a Wikipedia tool, search for 'wikipedia' to find and load it.",
|
|
"examples": [
|
|
"Search for 'weather' tools when asked about weather",
|
|
"Search for 'database' tools when working with data",
|
|
"Search for 'code' tools when generating code"
|
|
],
|
|
"limitations": "Returns metadata only - the playground handles actual tool loading"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Component 2: BM25 Search API Endpoint
|
|
|
|
**File:** `apps/web/src/app/api/tools/search/route.ts`
|
|
|
|
**Requirements:**
|
|
|
|
1. **Full-Text Search with BM25**
|
|
- Search across: tool description, package name, npm description, npm keywords
|
|
- BM25 scoring for relevance ranking
|
|
- Category filtering
|
|
- Quality score boosting (rich tier tools rank higher)
|
|
|
|
2. **Search Implementation Options**
|
|
|
|
**Option A: PostgreSQL Full-Text Search**
|
|
```sql
|
|
-- Add tsvector column to tools table
|
|
ALTER TABLE tools ADD COLUMN search_vector tsvector;
|
|
|
|
-- Create GIN index for fast full-text search
|
|
CREATE INDEX tools_search_idx ON tools USING GIN(search_vector);
|
|
|
|
-- Update search vector on insert/update
|
|
CREATE TRIGGER tools_search_update
|
|
BEFORE INSERT OR UPDATE ON tools
|
|
FOR EACH ROW EXECUTE FUNCTION
|
|
tsvector_update_trigger(search_vector, 'pg_catalog.english',
|
|
description);
|
|
```
|
|
|
|
**Option B: JavaScript BM25 Library**
|
|
```typescript
|
|
import { BM25 } from 'bm25';
|
|
|
|
// Load all tools into memory (cached)
|
|
const tools = await prisma.tool.findMany({
|
|
include: { package: true },
|
|
});
|
|
|
|
// Build BM25 index
|
|
const documents = tools.map(tool => ({
|
|
id: tool.id,
|
|
text: `${tool.description} ${tool.package.npmPackageName} ${tool.package.npmDescription} ${tool.package.npmKeywords.join(' ')}`,
|
|
}));
|
|
|
|
const bm25 = new BM25(documents);
|
|
const results = bm25.search(query);
|
|
```
|
|
|
|
**Option C: Hybrid Approach**
|
|
- Use PostgreSQL `LIKE` for exact matches (fastest)
|
|
- Fall back to BM25 for fuzzy/semantic search
|
|
- Cache search results in Redis
|
|
|
|
3. **API Response Format**
|
|
|
|
```typescript
|
|
// GET /api/tools/search?q=weather&category=integration&limit=10
|
|
|
|
{
|
|
"success": true,
|
|
"query": "weather",
|
|
"filters": {
|
|
"category": "integration"
|
|
},
|
|
"results": {
|
|
"total": 23,
|
|
"returned": 10,
|
|
"tools": [
|
|
{
|
|
"id": "clx...",
|
|
"exportName": "getWeatherTool",
|
|
"description": "Get current weather data for any location using OpenWeatherMap API",
|
|
"qualityScore": 0.85,
|
|
"package": {
|
|
"npmPackageName": "@tpmjs/weather",
|
|
"npmVersion": "1.2.0",
|
|
"category": "integration",
|
|
"frameworks": ["vercel-ai"],
|
|
"env": [
|
|
{
|
|
"name": "OPENWEATHER_API_KEY",
|
|
"description": "OpenWeatherMap API key",
|
|
"required": true
|
|
}
|
|
],
|
|
"npmRepository": {
|
|
"type": "git",
|
|
"url": "https://github.com/user/weather-tool"
|
|
},
|
|
"isOfficial": false
|
|
},
|
|
// Include everything needed for dynamic import
|
|
"importUrl": "https://esm.sh/@tpmjs/weather@1.2.0",
|
|
"cdnUrl": "https://cdn.jsdelivr.net/npm/@tpmjs/weather@1.2.0/+esm"
|
|
}
|
|
// ... more tools
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Component 3: Dynamic Tool Loader (Playground)
|
|
|
|
**File:** `apps/playground/src/lib/dynamic-tool-loader.ts`
|
|
|
|
**Requirements:**
|
|
|
|
1. **Runtime ESM Import**
|
|
```typescript
|
|
async function loadToolDynamically(
|
|
packageName: string,
|
|
exportName: string,
|
|
version: string
|
|
) {
|
|
// Option 1: ESM CDN (esm.sh, unpkg, jsdelivr)
|
|
const cdnUrl = `https://esm.sh/${packageName}@${version}`;
|
|
|
|
try {
|
|
const module = await import(/* @vite-ignore */ cdnUrl);
|
|
const tool = module[exportName];
|
|
|
|
if (!isValidTool(tool)) {
|
|
throw new Error(`Invalid tool: ${exportName}`);
|
|
}
|
|
|
|
return tool;
|
|
} catch (error) {
|
|
console.error(`Failed to load ${packageName}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Tool Caching Strategy**
|
|
```typescript
|
|
// Cache loaded tools to avoid redundant imports
|
|
const toolCache = new Map<string, any>();
|
|
|
|
function getCacheKey(packageName: string, exportName: string): string {
|
|
return `${packageName}::${exportName}`;
|
|
}
|
|
|
|
async function loadToolWithCache(
|
|
packageName: string,
|
|
exportName: string,
|
|
version: string
|
|
) {
|
|
const key = getCacheKey(packageName, exportName);
|
|
|
|
if (toolCache.has(key)) {
|
|
return toolCache.get(key);
|
|
}
|
|
|
|
const tool = await loadToolDynamically(packageName, exportName, version);
|
|
|
|
if (tool) {
|
|
toolCache.set(key, tool);
|
|
}
|
|
|
|
return tool;
|
|
}
|
|
```
|
|
|
|
3. **Tool Registry Integration**
|
|
```typescript
|
|
// Merge static tools + dynamically loaded tools
|
|
async function getAllAvailableTools(
|
|
staticTools: Record<string, any>,
|
|
searchResults: SearchResult[]
|
|
): Promise<Record<string, any>> {
|
|
const allTools = { ...staticTools };
|
|
|
|
// Load tools from search results
|
|
for (const result of searchResults) {
|
|
const tool = await loadToolWithCache(
|
|
result.package.npmPackageName,
|
|
result.exportName,
|
|
result.package.npmVersion
|
|
);
|
|
|
|
if (tool) {
|
|
const key = sanitizeToolName(
|
|
`${result.package.npmPackageName}-${result.exportName}`
|
|
);
|
|
allTools[key] = tool;
|
|
}
|
|
}
|
|
|
|
return allTools;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Component 4: Playground Chat Integration
|
|
|
|
**File:** `apps/playground/src/app/api/chat/route.ts`
|
|
|
|
**Flow:**
|
|
|
|
1. **Initial Tool Set**
|
|
- Load static tools (hardcoded in tool-loader)
|
|
- Always include `searchTpmjsToolsTool` in initial set
|
|
|
|
2. **Agent Invokes Search**
|
|
- Agent calls `searchTpmjsToolsTool` with query
|
|
- Search API returns matching tool metadata
|
|
- Response includes tool metadata
|
|
|
|
3. **Dynamic Loading Trigger**
|
|
- Detect when agent successfully calls `searchTpmjsToolsTool`
|
|
- Extract tool metadata from response
|
|
- Load tools dynamically before next agent turn
|
|
|
|
4. **Tool Availability Update**
|
|
- Merge dynamically loaded tools into available tool set
|
|
- Agent can now use newly loaded tools in subsequent turns
|
|
|
|
**Implementation:**
|
|
|
|
```typescript
|
|
// apps/playground/src/app/api/chat/route.ts
|
|
export async function POST(req: Request) {
|
|
const { messages } = await req.json();
|
|
|
|
// 1. Load static tools + search tool
|
|
let availableTools = await loadAllTools(); // static
|
|
availableTools['searchTpmjsTools'] = searchTpmjsToolsTool; // meta-tool
|
|
|
|
// 2. Create streamText with current tools
|
|
const result = streamText({
|
|
model: openai('gpt-4'),
|
|
messages,
|
|
tools: availableTools,
|
|
maxSteps: 10, // Allow multiple tool call rounds
|
|
|
|
onStepFinish: async (step) => {
|
|
// 3. Check if agent called searchTpmjsToolsTool
|
|
for (const toolCall of step.toolCalls) {
|
|
if (toolCall.toolName === 'searchTpmjsTools') {
|
|
const searchResults = toolCall.result?.tools || [];
|
|
|
|
// 4. Dynamically load tools from search results
|
|
console.log(`Loading ${searchResults.length} tools dynamically...`);
|
|
|
|
for (const toolMeta of searchResults) {
|
|
const tool = await loadToolWithCache(
|
|
toolMeta.packageName,
|
|
toolMeta.exportName,
|
|
'latest' // or toolMeta.version
|
|
);
|
|
|
|
if (tool) {
|
|
const key = sanitizeToolName(
|
|
`${toolMeta.packageName}-${toolMeta.exportName}`
|
|
);
|
|
availableTools[key] = tool;
|
|
console.log(`✅ Loaded: ${key}`);
|
|
}
|
|
}
|
|
|
|
// 5. Update tool registry for subsequent steps
|
|
// Note: This requires AI SDK to support dynamic tool updates
|
|
// May need to restart the streamText with updated tools
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
return result.toDataStreamResponse();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Technical Challenges & Solutions
|
|
|
|
### Challenge 1: AI SDK Doesn't Support Dynamic Tool Updates Mid-Stream
|
|
|
|
**Problem:** Vercel AI SDK's `streamText` sets tools at initialization. Can't add tools after streaming starts.
|
|
|
|
**Solutions:**
|
|
|
|
**Option A: Multi-Turn Pattern**
|
|
```typescript
|
|
// Turn 1: Agent searches for tools
|
|
// Turn 2: Agent uses loaded tools
|
|
|
|
// Detect search tool call, return early
|
|
if (hasSearchToolCall) {
|
|
return new Response(JSON.stringify({
|
|
type: 'tools_loaded',
|
|
tools: searchResults,
|
|
message: 'Tools loaded. Please continue your request.',
|
|
}));
|
|
}
|
|
```
|
|
|
|
**Option B: Pre-Flight Search (Recommended)**
|
|
```typescript
|
|
// Before calling streamText, analyze user message
|
|
const needsTools = await analyzeMessageForToolNeeds(userMessage);
|
|
|
|
if (needsTools.length > 0) {
|
|
// Pre-load tools based on intent
|
|
const searchResults = await searchTools(needsTools);
|
|
const dynamicTools = await loadToolsFromResults(searchResults);
|
|
availableTools = { ...staticTools, ...dynamicTools };
|
|
}
|
|
|
|
// Now call streamText with full tool set
|
|
const result = streamText({
|
|
model,
|
|
messages,
|
|
tools: availableTools,
|
|
});
|
|
```
|
|
|
|
**Option C: Agent-Driven Two-Phase**
|
|
```typescript
|
|
// Phase 1: Planning
|
|
const planResult = await generateText({
|
|
model,
|
|
messages: [
|
|
{ role: 'system', content: 'Analyze this request and determine what tools are needed. Call searchTpmjsTools if needed.' },
|
|
...messages,
|
|
],
|
|
tools: { searchTpmjsTools },
|
|
});
|
|
|
|
// Phase 2: Execution with loaded tools
|
|
const executionResult = await streamText({
|
|
model,
|
|
messages,
|
|
tools: { ...staticTools, ...loadedTools },
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### Challenge 2: ESM Dynamic Import in Browser vs Node.js
|
|
|
|
**Problem:** Dynamic `import()` works differently in browser vs server environments.
|
|
|
|
**Solutions:**
|
|
|
|
**Server-Side (Recommended):**
|
|
```typescript
|
|
// Use Node.js dynamic import
|
|
// Works with esm.sh CDN
|
|
const tool = await import(`https://esm.sh/${pkg}@${version}`);
|
|
```
|
|
|
|
**Client-Side (Avoid):**
|
|
```typescript
|
|
// Browser import() has CORS and CSP restrictions
|
|
// Would require:
|
|
// 1. CDN supports CORS
|
|
// 2. CSP allows script-src from CDN
|
|
// 3. Tools are browser-compatible (no Node.js APIs)
|
|
```
|
|
|
|
**Hybrid Approach:**
|
|
```typescript
|
|
// Load tools server-side, serialize to client
|
|
// Client displays available tools
|
|
// Server executes tool calls
|
|
```
|
|
|
|
---
|
|
|
|
### Challenge 3: Tool Dependencies & Environment Variables
|
|
|
|
**Problem:** Dynamically loaded tools may require:
|
|
- Environment variables (API keys)
|
|
- npm dependencies not in bundle
|
|
- Node.js-specific APIs
|
|
|
|
**Solutions:**
|
|
|
|
**Option A: Require Pre-Configuration**
|
|
```typescript
|
|
// Before loading, check if tool requirements are met
|
|
async function canLoadTool(toolMeta: ToolMetadata): Promise<boolean> {
|
|
// Check required env vars
|
|
for (const env of toolMeta.package.env || []) {
|
|
if (env.required && !process.env[env.name]) {
|
|
console.warn(`Missing required env: ${env.name}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
```
|
|
|
|
**Option B: Graceful Degradation**
|
|
```typescript
|
|
// Load tool, catch errors, inform agent
|
|
try {
|
|
const tool = await loadTool(packageName, exportName);
|
|
return tool;
|
|
} catch (error) {
|
|
return createStubTool(packageName, exportName, error);
|
|
}
|
|
|
|
function createStubTool(pkg: string, exp: string, error: Error) {
|
|
return tool({
|
|
description: `[UNAVAILABLE] ${exp} from ${pkg}: ${error.message}`,
|
|
parameters: z.object({}),
|
|
execute: async () => {
|
|
throw new Error(`Cannot execute ${exp}: ${error.message}`);
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
**Option C: Proxy Through Server**
|
|
```typescript
|
|
// All tools execute server-side where env vars exist
|
|
// Client just displays tool calls, server handles execution
|
|
```
|
|
|
|
---
|
|
|
|
### Challenge 4: Security & Sandboxing
|
|
|
|
**Problem:** Dynamically importing arbitrary npm packages is a security risk.
|
|
|
|
**Solutions:**
|
|
|
|
**Option A: Allowlist Only**
|
|
```typescript
|
|
// Only load tools from TPMJS registry (already vetted)
|
|
const allowedPackages = await prisma.package.findMany({
|
|
select: { npmPackageName: true }
|
|
});
|
|
|
|
if (!allowedPackages.includes(packageName)) {
|
|
throw new Error('Package not in TPMJS registry');
|
|
}
|
|
```
|
|
|
|
**Option B: Version Pinning**
|
|
```typescript
|
|
// Only load specific versions from registry
|
|
// Don't use 'latest' to avoid supply chain attacks
|
|
const version = toolMeta.package.npmVersion; // e.g., "1.2.0"
|
|
const url = `https://esm.sh/${pkg}@${version}`;
|
|
```
|
|
|
|
**Option C: VM Sandbox (Advanced)**
|
|
```typescript
|
|
// Execute tools in isolated VM context
|
|
import { VM } from 'vm2';
|
|
|
|
const vm = new VM({
|
|
timeout: 5000,
|
|
sandbox: {
|
|
fetch: safeFetch, // Wrapped fetch with rate limits
|
|
console: safeConsole,
|
|
},
|
|
});
|
|
|
|
const tool = vm.run(toolCode);
|
|
```
|
|
|
|
---
|
|
|
|
### Challenge 5: Performance & Bundle Size
|
|
|
|
**Problem:** Loading many tools dynamically could be slow.
|
|
|
|
**Solutions:**
|
|
|
|
**Option A: Lazy Loading**
|
|
```typescript
|
|
// Only load tools when agent decides to use them
|
|
// Not when they're discovered
|
|
```
|
|
|
|
**Option B: Parallel Loading**
|
|
```typescript
|
|
// Load multiple tools concurrently
|
|
const toolPromises = searchResults.map(result =>
|
|
loadToolWithCache(result.package.npmPackageName, result.exportName, result.package.npmVersion)
|
|
);
|
|
|
|
const tools = await Promise.all(toolPromises);
|
|
```
|
|
|
|
**Option C: CDN Caching**
|
|
```typescript
|
|
// Use CDN with aggressive caching
|
|
// esm.sh has built-in caching
|
|
const url = `https://esm.sh/${pkg}@${version}?target=es2022&bundle`;
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Plan
|
|
|
|
### Phase 1: MVP (Week 1-2)
|
|
|
|
**Goal:** Prove dynamic loading works with simple prototype
|
|
|
|
1. **Create `@tpmjs/search-registry` package**
|
|
- Implement `searchTpmjsToolsTool`
|
|
- Publish to npm
|
|
- Add to manual-tools registry
|
|
|
|
2. **Build `/api/tools/search` endpoint**
|
|
- Start with simple PostgreSQL `LIKE` search
|
|
- Return tool metadata with package info
|
|
- Test with curl
|
|
|
|
3. **Implement basic dynamic loader**
|
|
- Use esm.sh CDN for imports
|
|
- Load tools server-side only
|
|
- Cache in memory
|
|
|
|
4. **Playground integration - Two-Turn Pattern**
|
|
- User asks question
|
|
- Agent calls `searchTpmjsToolsTool`
|
|
- Backend loads tools
|
|
- Agent uses tools in next turn
|
|
|
|
**Success Criteria:**
|
|
- Agent can search registry
|
|
- Agent can use dynamically loaded tools
|
|
- End-to-end flow works for 1-2 example tools
|
|
|
|
---
|
|
|
|
### Phase 2: BM25 Search (Week 3)
|
|
|
|
**Goal:** Improve search relevance with BM25
|
|
|
|
1. **Research BM25 implementation options**
|
|
- Test PostgreSQL full-text search
|
|
- Test JavaScript BM25 libraries
|
|
- Benchmark performance
|
|
|
|
2. **Implement chosen approach**
|
|
- Add search vector column if using PostgreSQL
|
|
- Create search index
|
|
- Update search endpoint
|
|
|
|
3. **Test search quality**
|
|
- Create test queries
|
|
- Measure precision/recall
|
|
- Compare to baseline `LIKE` search
|
|
|
|
**Success Criteria:**
|
|
- BM25 search returns more relevant results than LIKE
|
|
- Search latency < 100ms for 95th percentile
|
|
- Agent can find tools for diverse queries
|
|
|
|
---
|
|
|
|
### Phase 3: Production Hardening (Week 4)
|
|
|
|
**Goal:** Make system production-ready
|
|
|
|
1. **Error Handling**
|
|
- Handle import failures gracefully
|
|
- Validate tool schemas
|
|
- Return helpful error messages to agent
|
|
|
|
2. **Security**
|
|
- Implement package allowlist
|
|
- Pin versions from registry
|
|
- Add rate limiting to search API
|
|
|
|
3. **Performance**
|
|
- Implement Redis caching for search results
|
|
- Add CDN caching headers
|
|
- Optimize tool loading parallelism
|
|
|
|
4. **Monitoring**
|
|
- Log all dynamic tool loads
|
|
- Track search queries and results
|
|
- Monitor import success/failure rates
|
|
|
|
**Success Criteria:**
|
|
- System handles errors without crashing
|
|
- Security review passes
|
|
- Latency and reliability SLOs met
|
|
|
|
---
|
|
|
|
### Phase 4: Advanced Features (Week 5+)
|
|
|
|
**Goal:** Enhance UX and capabilities
|
|
|
|
1. **Pre-flight Search**
|
|
- Analyze user message for intent
|
|
- Proactively load tools before agent call
|
|
- Reduce total turns needed
|
|
|
|
2. **Tool Recommendations**
|
|
- "You might also need..." suggestions
|
|
- Based on tool co-occurrence data
|
|
- Help agent discover related tools
|
|
|
|
3. **Client-Side Tool Display**
|
|
- Show which tools are available
|
|
- Indicate dynamically loaded tools
|
|
- Allow user to manually load tools
|
|
|
|
4. **Tool Versioning**
|
|
- Support multiple versions of same tool
|
|
- Let agent choose version
|
|
- Handle breaking changes gracefully
|
|
|
|
---
|
|
|
|
## Success Metrics
|
|
|
|
### Technical Metrics
|
|
|
|
1. **Search Quality**
|
|
- Precision@10 > 0.8 (80% of top 10 results are relevant)
|
|
- Mean Reciprocal Rank (MRR) > 0.7
|
|
- Search latency p95 < 100ms
|
|
|
|
2. **Tool Loading**
|
|
- Import success rate > 95%
|
|
- Tool load time p95 < 2 seconds
|
|
- Cache hit rate > 70% after warmup
|
|
|
|
3. **End-to-End Performance**
|
|
- Total conversation latency < 5 seconds (including tool search + load + execution)
|
|
- Agent uses correct tools > 90% of time
|
|
|
|
### User Metrics
|
|
|
|
1. **Adoption**
|
|
- % of playground sessions using dynamic tools > 30%
|
|
- Number of unique tools loaded dynamically per week > 50
|
|
|
|
2. **Tool Coverage**
|
|
- % of user queries satisfied with available tools > 80%
|
|
- Tool search leading to successful task completion > 70%
|
|
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
### 1. CDN Choice for ESM Imports
|
|
|
|
**Options:**
|
|
- **esm.sh** - Purpose-built for ESM imports, fast, reliable
|
|
- **unpkg** - Popular, simple, but slower
|
|
- **jsdelivr** - Fast CDN, good for production
|
|
- **Custom bundler** - Pre-bundle tools, serve from our CDN
|
|
|
|
**Recommendation:** Start with esm.sh for MVP, evaluate custom bundler for production.
|
|
|
|
---
|
|
|
|
### 2. When to Load Tools?
|
|
|
|
**Options:**
|
|
- **On-demand**: Load when agent calls search tool (current plan)
|
|
- **Pre-flight**: Analyze user message, load proactively
|
|
- **Lazy**: Load when agent tries to use tool (not when discovered)
|
|
- **Eager**: Load all tools from search results immediately
|
|
|
|
**Recommendation:** Start with on-demand (Phase 1), add pre-flight in Phase 4.
|
|
|
|
---
|
|
|
|
### 3. How to Handle Environment Variables?
|
|
|
|
**Problem:** Dynamically loaded tools may need API keys (e.g., OpenWeather API).
|
|
|
|
**Options:**
|
|
- **User provides**: UI for users to enter API keys (like playground settings)
|
|
- **Server-managed**: Admin pre-configures keys in .env
|
|
- **Graceful fail**: Load tool, but execution fails if env missing
|
|
- **Hybrid**: Some tools work without keys (free tier), others require keys
|
|
|
|
**Recommendation:** Start with graceful fail (Phase 1), add user-provided keys (Phase 4).
|
|
|
|
---
|
|
|
|
### 4. Should Tools Load Client-Side or Server-Side?
|
|
|
|
**Client-Side Pros:**
|
|
- Reduces server load
|
|
- Faster for subsequent uses
|
|
- Better for browser-compatible tools
|
|
|
|
**Client-Side Cons:**
|
|
- Requires CORS-enabled CDN
|
|
- CSP restrictions
|
|
- Many tools need Node.js APIs
|
|
- Exposing API keys in browser is insecure
|
|
|
|
**Server-Side Pros:**
|
|
- Access to Node.js APIs
|
|
- Secure environment variable access
|
|
- No CORS issues
|
|
- Easier to implement
|
|
|
|
**Server-Side Cons:**
|
|
- Requires server memory for caching
|
|
- Increases server load
|
|
- Cold starts for new tools
|
|
|
|
**Recommendation:** Server-side for MVP (Phase 1), evaluate client-side for browser-compatible tools (Phase 4+).
|
|
|
|
---
|
|
|
|
### 5. How to Handle Tool Dependencies?
|
|
|
|
**Problem:** Some tools depend on other npm packages (e.g., `axios`, `cheerio`).
|
|
|
|
**Options:**
|
|
- **Bundled**: CDN bundles dependencies (esm.sh does this)
|
|
- **Peer deps**: Require dependencies in playground package.json
|
|
- **Dynamic install**: npm install on-the-fly (slow, risky)
|
|
- **Pre-vetted**: Only allow tools with no/minimal dependencies
|
|
|
|
**Recommendation:** Use esm.sh bundling (Phase 1), bundle size limits if issues arise.
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
### High Risk
|
|
|
|
1. **Security Vulnerability**
|
|
- **Risk**: Malicious package in registry executes code
|
|
- **Mitigation**: Allowlist registry packages, version pinning, VM sandboxing
|
|
- **Owner**: Security team
|
|
|
|
2. **Performance Degradation**
|
|
- **Risk**: Loading many tools causes timeout/slow response
|
|
- **Mitigation**: Parallel loading, caching, lazy loading, timeouts
|
|
- **Owner**: Backend team
|
|
|
|
### Medium Risk
|
|
|
|
3. **Import Failures**
|
|
- **Risk**: CDN down, package incompatible, missing dependencies
|
|
- **Mitigation**: Fallback CDNs, error handling, stub tools
|
|
- **Owner**: Frontend team
|
|
|
|
4. **AI SDK Limitations**
|
|
- **Risk**: Can't dynamically update tools mid-stream
|
|
- **Mitigation**: Two-turn pattern, pre-flight search
|
|
- **Owner**: AI team
|
|
|
|
### Low Risk
|
|
|
|
5. **Search Quality**
|
|
- **Risk**: BM25 doesn't return relevant tools
|
|
- **Mitigation**: A/B test search algorithms, collect feedback
|
|
- **Owner**: Search team
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
### 1. Tool Composition
|
|
- Agent can combine multiple tools
|
|
- Example: `searchTool` + `summarizeTool` = search and summarize
|
|
|
|
### 2. Tool Learning
|
|
- Track which tools are used together
|
|
- Recommend tool combinations
|
|
- "Users who used X also used Y"
|
|
|
|
### 3. Custom Tool Registry
|
|
- Users can add private tools
|
|
- Organization-specific tool registry
|
|
- Access control and permissions
|
|
|
|
### 4. Tool Marketplace
|
|
- Developers promote their tools
|
|
- Usage analytics and ratings
|
|
- Paid/premium tools
|
|
|
|
### 5. Agent Templates
|
|
- Pre-configured agents with tool sets
|
|
- "Research Agent" has search + summarize tools
|
|
- "Code Agent" has code generation tools
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
This dynamic tool loading system represents a paradigm shift in how AI agents discover and use tools. By making the TPMJS registry itself searchable, we enable infinite extensibility without the limitations of static bundling.
|
|
|
|
**Key Innovation:** Self-referential tool discovery - a tool that searches for tools.
|
|
|
|
**Next Steps:**
|
|
1. Review this PRD with team
|
|
2. Validate technical feasibility with ChatGPT/Claude
|
|
3. Spike on BM25 search implementation
|
|
4. Spike on dynamic ESM import
|
|
5. Begin Phase 1 implementation
|
|
|
|
**Success Looks Like:**
|
|
- User: "Search Wikipedia for quantum computing"
|
|
- Agent: *searches registry, finds Wikipedia tool, loads it, uses it*
|
|
- User: Gets Wikipedia results without any manual tool configuration
|
|
|
|
This is a novel approach that could define how AI agents discover and use tools. Let's build it. 🚀
|