UNPKG

tinyagent-ts

Version:

Modern TypeScript framework for building AI agents with pluggable tools and ReAct reasoning

1,580 lines (1,454 loc) 1.03 MB
import { z } from 'zod'; import fs$2 from 'fs'; import { exec } from 'child_process'; import require$$0$6, { randomUUID } from 'crypto'; import readline from 'readline'; import execa from 'execa'; import path$3 from 'path'; import require$$1$1 from 'tty'; import require$$1$2 from 'util'; import require$$0$3 from 'os'; import require$$0$4 from 'buffer'; import require$$1$3 from 'string_decoder'; import require$$4$1 from 'node:zlib'; import require$$1$5 from 'node:events'; import require$$0$5 from 'url'; import require$$7$1 from 'node:path'; import require$$2$1 from 'node:fs'; import require$$2$2 from 'node:http'; import require$$6$1 from 'querystring'; import require$$1$4 from 'node:net'; import require$$13 from 'stream'; /** * Model error types */ class ModelError extends Error { statusCode; constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.name = 'ModelError'; } } class ModelAbortError extends ModelError { constructor(message = 'Model request was aborted') { super(message); this.name = 'ModelAbortError'; } } /** * OpenRouter model provider implementation */ class OpenRouterProvider { baseUrl = 'https://openrouter.ai/api/v1/chat/completions'; getName() { return 'openrouter'; } async chat(messages, config, abortSignal) { if (abortSignal?.aborted) { throw new ModelAbortError(); } try { const response = await fetch(this.baseUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://github.com/yourusername/tinyagent-ts', 'X-Title': 'TinyAgent-TS', }, signal: abortSignal, body: JSON.stringify({ model: config.model, messages, }), }); if (!response.ok) { let errorDetails = { message: 'Failed to parse error response' }; try { errorDetails = await response.json(); } catch (parseError) { // Ignore parsing error, use default message } throw new ModelError(`OpenRouter API error: ${response.status} ${response.statusText}. Details: ${JSON.stringify(errorDetails)}`, response.status); } const data = await response.json(); const content = data.choices[0]?.message?.content?.trim() ?? ''; return { content, usage: data.usage ? { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens, totalTokens: data.usage.total_tokens, } : undefined, }; } catch (error) { if (error instanceof ModelError) { throw error; } if (error instanceof Error && error.name === 'AbortError') { throw new ModelAbortError(); } throw new ModelError(`Failed to communicate with OpenRouter: ${error instanceof Error ? error.message : String(error)}`); } } } /** * Manages model providers and handles communication with LLMs */ class ModelManager { providers = new Map(); config; constructor(config = {}) { this.config = { defaultProvider: config.defaultProvider || 'openrouter', defaultModel: config.defaultModel || 'openai/gpt-4o-mini', apiKey: config.apiKey || process.env.OPENROUTER_API_KEY || '', maxRetries: config.maxRetries || 3, retryDelay: config.retryDelay || 1000, }; if (!this.config.apiKey) { throw new ModelError('API key is required. Set OPENROUTER_API_KEY environment variable or provide apiKey in config.'); } // Register default providers this.registerProvider('openrouter', new OpenRouterProvider()); } /** * Register a new model provider */ registerProvider(name, provider) { this.providers.set(name, provider); } /** * Get a registered provider */ getProvider(name) { return this.providers.get(name); } /** * Send messages to a model with retry logic */ async chat(messages, options = {}) { const providerName = options.provider || this.config.defaultProvider; const provider = this.getProvider(providerName); if (!provider) { throw new ModelError(`Unknown provider: ${providerName}`); } const modelConfig = { model: options.model || this.config.defaultModel, apiKey: options.apiKey || this.config.apiKey, maxRetries: options.maxRetries || this.config.maxRetries, }; const maxRetries = modelConfig.maxRetries || 0; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await provider.chat(messages, modelConfig, options.abortSignal); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Don't retry on abort or certain error types if (lastError.name === 'ModelAbortError' || lastError.name === 'AbortError' || (error instanceof ModelError && error.statusCode === 401)) { throw lastError; } // If this is the last attempt, throw the error if (attempt === maxRetries) { break; } // Wait before retrying if (this.config.retryDelay > 0) { await new Promise(resolve => setTimeout(resolve, this.config.retryDelay)); } } } throw lastError; } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Get current configuration */ getConfig() { return { ...this.config }; } } /** * Implementation of ReAct state management */ class ReActStateManager { task = ''; steps = []; setTask(text) { this.task = text; } getTask() { return this.task; } clear() { this.steps = []; this.task = ''; } addThought(text) { this.steps.push({ type: 'thought', text, timestamp: new Date() }); } addAction(action) { this.steps.push({ ...action, timestamp: new Date() }); } addObservation(text) { this.steps.push({ type: 'observation', text, timestamp: new Date() }); } addReflexion(text) { this.steps.push({ type: 'reflexion', text, timestamp: new Date() }); } getSteps() { return [...this.steps]; } /** * Get the last argument value used for a specific key in any tool call. */ getLastArgValue(argKey) { for (let i = this.steps.length - 1; i >= 0; i--) { const step = this.steps[i]; if (step.type === 'action' && step.mode === 'json' && step.args && argKey in step.args) { return step.args[argKey]; } } return undefined; } /** * Convert state to LLM messages format */ toMessages(systemPrompt) { const msgs = []; if (systemPrompt) { msgs.push({ role: 'system', content: systemPrompt }); } if (this.task) { msgs.push({ role: 'user', content: this.task }); } for (const step of this.steps) { switch (step.type) { case 'thought': msgs.push({ role: 'assistant', content: `Thought: ${step.text}` }); break; case 'reflexion': msgs.push({ role: 'assistant', content: `Reflexion: ${step.text}` }); break; case 'action': const actionStep = step; if (actionStep.mode === 'code') { msgs.push({ role: 'assistant', content: `Action:\n\`\`\`ts\n${actionStep.text}\n\`\`\``, }); } else { const json = JSON.stringify({ tool: actionStep.tool, args: actionStep.args }); msgs.push({ role: 'assistant', content: `Action: ${json}` }); } break; case 'observation': msgs.push({ role: 'assistant', content: `Observation: ${step.text}`, }); break; } } return msgs; } } /** * Parse a ReAct response text into structured components */ function parseReActResponse(text) { const reflexMatch = text.match(/Reflect(?:ion|xion)?:([\s\S]*?)(?=\n(?:Thought|Action):|$)/i); const reflexion = reflexMatch ? reflexMatch[1].trim() : undefined; const thoughtMatch = text.match(/Thought:(.*?)(?:\nAction:|$)/s); const thought = thoughtMatch ? thoughtMatch[1].trim() : ''; const actionPart = text.split(/\nAction:/s)[1] ?? ''; const trimmed = actionPart.trim(); // First, try to parse as JSON directly try { // Check if the trimmed text is a JSON object if (trimmed.startsWith('{') && trimmed.endsWith('}')) { const parsed = JSON.parse(trimmed); if (typeof parsed.tool === 'string' && typeof parsed.args === 'object') { const action = { type: 'action', mode: 'json', tool: parsed.tool, args: parsed.args, text: trimmed, }; return { thought, action, reflexion }; } } } catch { // If JSON parsing fails, continue to other formats } // Try to extract JSON from the text (may be embedded in other text) const jsonMatch = trimmed.match(/{\s*"tool"\s*:\s*"([^"]+)".*}/); if (jsonMatch) { try { const jsonStr = jsonMatch[0]; const parsed = JSON.parse(jsonStr); if (typeof parsed.tool === 'string' && typeof parsed.args === 'object') { const action = { type: 'action', mode: 'json', tool: parsed.tool, args: parsed.args, text: jsonStr, }; return { thought, action, reflexion }; } } catch { // If JSON parsing fails, continue to other formats } } // Check for code blocks if (trimmed.startsWith('```')) { const codeMatch = trimmed.match(/```(?:\w+)?\n([\s\S]*?)```/); const code = codeMatch ? codeMatch[1].trim() : trimmed; const action = { type: 'action', mode: 'code', tool: 'code', text: code }; return { thought, action, reflexion }; } // Final fallback: treat as code if (trimmed) { const action = { type: 'action', mode: 'code', tool: 'code', text: trimmed }; return { thought, action, reflexion }; } return { thought, reflexion }; } /** * Schema for validating final answer structure */ const FinalAnswerSchema$1 = z.object({ answer: z.unknown().refine((val) => val !== undefined && val !== null, { message: "Answer cannot be undefined or null" }) }); /** * Validate and cast a raw final answer through the schema */ function validateFinalAnswer(rawAnswer) { try { return FinalAnswerSchema$1.parse(rawAnswer); } catch (error) { if (error instanceof z.ZodError) { throw new Error(`Invalid final answer structure: ${error.errors.map(e => e.message).join(', ')}`); } throw error; } } /** * ReAct engine that orchestrates the reasoning and acting loop */ class ReActEngine { modelManager; state; tools = new Map(); constructor(modelManager) { this.modelManager = modelManager; this.state = new ReActStateManager(); } /** * Register a tool for use in ReAct loops */ registerTool(tool) { this.tools.set(tool.name, tool); } /** * Unregister a tool */ unregisterTool(name) { this.tools.delete(name); } /** * Get all registered tools */ getTools() { return Array.from(this.tools.values()); } /** * Execute a ReAct loop */ async execute(task, systemPrompt, config = {}, options = {}) { const { maxSteps = 5, enableReflexion = true, enableTrace = false, onStep, onComplete, } = config; this.state.clear(); this.state.setTask(task); let finalAnswer = undefined; let usedTool = false; try { for (let step = 0; step < maxSteps; step++) { if (options.abortSignal?.aborted) { throw new Error('ReAct execution was aborted'); } // Get current state as messages const messages = this.state.toMessages(systemPrompt); // Get response from model const response = await this.modelManager.chat(messages, { model: options.model, abortSignal: options.abortSignal, }); // Parse the response const parsed = parseReActResponse(response.content); // Add thought if present if (parsed.thought) { this.state.addThought(parsed.thought); if (enableTrace) { console.log(`Thought: ${parsed.thought}`); } if (onStep) { onStep({ type: 'thought', text: parsed.thought }); } } // Process action if present let observation = ''; if (parsed.action) { this.state.addAction(parsed.action); if (enableTrace) { console.log(`Action: ${parsed.action.tool}(${JSON.stringify(parsed.action.args)})`); } if (onStep) { onStep(parsed.action); } try { // Check for final answer if (parsed.action.tool === 'final_answer') { if (!usedTool) { console.warn('final_answer called before any other tool'); } finalAnswer = parsed.action.args; observation = JSON.stringify(finalAnswer); this.state.addObservation(observation); if (enableTrace) { console.log(`Observation: ${observation}`); } if (onStep) { onStep({ type: 'observation', text: observation }); } break; } // Execute tool const tool = this.tools.get(parsed.action.tool); if (!tool) { observation = `Unknown tool: ${parsed.action.tool}`; } else { // Auto-fill missing arguments from state if possible let toolArgs = { ...parsed.action.args }; if (tool.schema && typeof tool.schema.shape === 'object') { const argKeys = Object.keys(tool.schema.shape); for (const key of argKeys) { if (toolArgs[key] === undefined) { const lastValue = this.state.getLastArgValue(key); if (lastValue !== undefined) { toolArgs[key] = lastValue; } } } } const result = await tool.execute(toolArgs, options.abortSignal); usedTool = true; observation = JSON.stringify(result); } } catch (error) { observation = error instanceof Error ? error.message : String(error); } this.state.addObservation(observation); if (enableTrace) { console.log(`Observation: ${observation}`); } if (onStep) { onStep({ type: 'observation', text: observation }); } } // Reflexion step if enabled if (enableReflexion && parsed.action) { const reflectMessages = this.state.toMessages(systemPrompt); reflectMessages.push({ role: 'user', content: 'Reflect:' }); if (options.abortSignal?.aborted) { throw new Error('ReAct execution was aborted'); } const reflectResponse = await this.modelManager.chat(reflectMessages, { model: options.model, abortSignal: options.abortSignal, }); const reflectParsed = parseReActResponse(reflectResponse.content); if (reflectParsed.reflexion) { this.state.addReflexion(reflectParsed.reflexion); if (enableTrace) { console.log(`Reflexion: ${reflectParsed.reflexion}`); } if (onStep) { onStep({ type: 'reflexion', text: reflectParsed.reflexion }); } } // Handle fix action from reflexion if (reflectParsed.action) { if (reflectParsed.action.tool === 'final_answer') { if (!usedTool) { console.warn('final_answer called before any other tool'); } finalAnswer = reflectParsed.action.args; const obs = JSON.stringify(finalAnswer); this.state.addObservation(obs); if (enableTrace) { console.log(`Observation: ${obs}`); } if (onStep) { onStep({ type: 'observation', text: obs }); } break; } // Execute fix action const tool = this.tools.get(reflectParsed.action.tool); let obs = ''; try { if (!tool) { obs = `Unknown tool: ${reflectParsed.action.tool}`; } else { const result = await tool.execute(reflectParsed.action.args || {}, options.abortSignal); usedTool = true; obs = JSON.stringify(result); } } catch (error) { obs = error instanceof Error ? error.message : String(error); } if (reflectParsed.thought) { this.state.addThought(reflectParsed.thought); } this.state.addAction(reflectParsed.action); this.state.addObservation(obs); if (enableTrace) { if (reflectParsed.thought) console.log(`Thought: ${reflectParsed.thought}`); console.log(`Action: ${reflectParsed.action.tool}(${JSON.stringify(reflectParsed.action.args)})`); console.log(`Observation: ${obs}`); } if (onStep) { if (reflectParsed.thought) onStep({ type: 'thought', text: reflectParsed.thought }); onStep(reflectParsed.action); onStep({ type: 'observation', text: obs }); } } else if (reflectParsed.thought) { this.state.addThought(reflectParsed.thought); if (enableTrace) { console.log(`Thought: ${reflectParsed.thought}`); } if (onStep) { onStep({ type: 'thought', text: reflectParsed.thought }); } } } } // Enforce final answer if not provided finalAnswer = await this.enforceFinalAnswer(finalAnswer, systemPrompt, options); // Validate final answer structure try { finalAnswer = validateFinalAnswer(finalAnswer); } catch (error) { throw new Error(`Final answer validation failed: ${error instanceof Error ? error.message : String(error)}`); } const result = { success: true, steps: this.state.getSteps(), finalAnswer, }; if (onComplete) { onComplete(result); } return result; } catch (error) { const result = { success: false, error: error instanceof Error ? error : new Error(String(error)), steps: this.state.getSteps(), finalAnswer, }; if (onComplete) { onComplete(result); } return result; } } /** * Enforce final answer if not provided by the model */ async enforceFinalAnswer(currentFinalAnswer, systemPrompt, options) { // If we already have a final answer, return it if (currentFinalAnswer !== undefined) { return currentFinalAnswer; } console.warn('ReAct loop completed without final_answer tool call. Forcing final answer generation.'); // Create a final answer request const finalMessages = this.state.toMessages(systemPrompt); finalMessages.push({ role: 'user', content: 'You must provide a final answer now using the final_answer tool. Summarize your findings and provide a conclusive response.' }); try { const finalResponse = await this.modelManager.chat(finalMessages, { model: options.model, abortSignal: options.abortSignal, }); const finalParsed = parseReActResponse(finalResponse.content); if (finalParsed.action && finalParsed.action.tool === 'final_answer') { const finalAnswer = finalParsed.action.args; this.state.addThought(finalParsed.thought || 'Providing final answer'); this.state.addAction(finalParsed.action); this.state.addObservation(JSON.stringify(finalAnswer)); return finalAnswer; } else { // Fallback: create a final answer from the most relevant tool execution result const steps = this.state.getSteps(); const observations = steps.filter(s => s.type === 'observation'); // Try to find the most recent meaningful tool execution result (not action text) let answerContent = 'Task completed'; // Look for observations that contain actual results (not action descriptions) for (let i = observations.length - 1; i >= 0; i--) { const obs = observations[i]; if (obs.text && !obs.text.startsWith('Action:') && !obs.text.startsWith('{\"answer\":')) { try { // If observation is JSON, try to extract meaningful content const parsed = JSON.parse(obs.text); if (typeof parsed === 'string') { // If it's a UUID or similar string result, format it nicely if (parsed.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) { answerContent = `The generated UUID is: ${parsed}`; } else { answerContent = parsed; } break; } else if (parsed && typeof parsed === 'object') { answerContent = JSON.stringify(parsed); break; } } catch { // If not JSON, use the text directly if it looks meaningful if (obs.text.length > 10 && !obs.text.includes('Unknown tool')) { answerContent = obs.text; break; } } } } // If no meaningful observation found, use the final response if (answerContent === 'Task completed' && finalResponse.content) { answerContent = finalResponse.content; } const finalAnswer = { answer: answerContent }; this.state.addThought('Providing final answer based on previous results'); // Create proper ActionStep for final_answer const finalAnswerAction = { type: 'action', mode: 'json', tool: 'final_answer', args: finalAnswer, text: JSON.stringify({ tool: 'final_answer', args: finalAnswer }) }; this.state.addAction(finalAnswerAction); this.state.addObservation(JSON.stringify(finalAnswer)); return finalAnswer; } } catch (error) { console.warn('Failed to force final answer:', error); return { answer: 'Task completed but final answer generation failed' }; } } /** * Get the current state */ getState() { return this.state; } /** * Reset the state */ reset() { this.state.clear(); } } /** * Base class for implementing tools */ class BaseTool { /** * Validate arguments against the tool's schema */ validateArgs(args) { return this.schema.parse(args); } /** * Create a successful result */ success(data, metadata) { return { success: true, data, metadata, }; } /** * Create an error result */ error(message, metadata) { return { success: false, error: message, metadata, }; } } /** * Standard implementation of the tool registry */ class StandardToolRegistry { tools = new Map(); metadata = new Map(); register(tool) { if (this.tools.has(tool.name)) { throw new Error(`Tool with name '${tool.name}' is already registered`); } this.tools.set(tool.name, tool); this.metadata.set(tool.name, { name: tool.name, description: tool.description, schema: tool.schema, }); } unregister(name) { this.tools.delete(name); this.metadata.delete(name); } get(name) { return this.tools.get(name); } getAll() { return Array.from(this.tools.values()); } getByCategory(category) { const result = []; for (const [name, meta] of this.metadata.entries()) { if (meta.category === category) { const tool = this.tools.get(name); if (tool) { result.push(tool); } } } return result; } has(name) { return this.tools.has(name); } /** * Get metadata for a tool */ getMetadata(name) { return this.metadata.get(name); } /** * Get all tool metadata */ getAllMetadata() { return Array.from(this.metadata.values()); } /** * Register a tool with additional metadata */ registerWithMetadata(tool, metadata) { this.register(tool); const fullMetadata = { name: tool.name, description: tool.description, schema: tool.schema, ...metadata, }; this.metadata.set(tool.name, fullMetadata); } /** * Create a StandardToolRegistry from an object of tools */ static fromTools(toolsObj) { const registry = new StandardToolRegistry(); for (const key in toolsObj) { if (Object.prototype.hasOwnProperty.call(toolsObj, key)) { registry.register(toolsObj[key]); } } return registry; } /** * Get tools catalog as string for LLM prompts */ getCatalog() { return this.getAll() .map(tool => `- ${tool.name}: ${tool.description}`) .join('\n'); } /** * Clear all registered tools */ clear() { this.tools.clear(); this.metadata.clear(); } /** * Get tool count */ size() { return this.tools.size; } } /** * Schema for final answer arguments */ const FinalAnswerSchema = z.object({ answer: z.string().describe('The final answer to provide to the user'), }); /** * Tool for providing final answers in ReAct loops */ class FinalAnswerTool extends BaseTool { name = 'final_answer'; description = 'Provide the final answer to the user\'s question or task'; schema = FinalAnswerSchema; async execute(args) { const validated = this.validateArgs(args); return validated; } } /** * Schema for file operations */ const FileToolSchema = z.object({ action: z.enum(['read', 'write', 'append', 'delete']).describe('The file operation to perform'), path: z.string().describe('The file path to operate on'), content: z.string().optional().describe('Content to write/append (required for write/append actions)'), }); /** * File system tool supporting basic CRUD operations */ class FileTool extends BaseTool { name = 'file'; description = 'Read, write, append or delete a file on disk'; schema = FileToolSchema; async execute(args, abortSignal) { if (abortSignal?.aborted) { throw new Error('Operation was aborted'); } const { action, path, content } = this.validateArgs(args); try { switch (action) { case 'read': if (abortSignal?.aborted) throw new Error('Operation was aborted'); return fs$2.existsSync(path) ? fs$2.readFileSync(path, 'utf-8') : ''; case 'write': if (!content && content !== '') { throw new Error('Content is required for write action'); } if (abortSignal?.aborted) throw new Error('Operation was aborted'); fs$2.writeFileSync(path, content); return `Successfully wrote to ${path}`; case 'append': if (!content && content !== '') { throw new Error('Content is required for append action'); } if (abortSignal?.aborted) throw new Error('Operation was aborted'); fs$2.appendFileSync(path, content); return `Successfully appended to ${path}`; case 'delete': if (abortSignal?.aborted) throw new Error('Operation was aborted'); if (fs$2.existsSync(path)) { fs$2.unlinkSync(path); return `Successfully deleted ${path}`; } else { return `File ${path} does not exist`; } default: throw new Error(`Unknown action: ${action}`); } } catch (error) { if (error instanceof Error) { throw error; } throw new Error(`File operation failed: ${String(error)}`); } } } /** * Schema for grep operations */ const GrepToolSchema = z.object({ pattern: z.string().describe('The pattern to search for'), file: z.string().describe('The file to search in'), flags: z.string().optional().describe('Additional grep flags (e.g., -i for case insensitive)'), }); /** * Grep tool for searching patterns in files */ class GrepTool extends BaseTool { name = 'grep'; description = 'Search for a pattern in a file using grep'; schema = GrepToolSchema; async execute(args, abortSignal) { if (abortSignal?.aborted) { throw new Error('Operation was aborted'); } const { pattern, file, flags = '' } = this.validateArgs(args); return new Promise((resolve, reject) => { // Escape the pattern to prevent shell injection const escapedPattern = pattern.replace(/'/g, "\\'"); const command = `grep ${flags} -n '${escapedPattern}' '${file}'`; const childProcess = exec(command, (error, stdout, stderr) => { if (abortSignal?.aborted) { childProcess.kill(); reject(new Error('Operation was aborted')); return; } if (error) { // Grep returns exit code 1 when no matches found, which is not an error if (error.code === 1) { resolve('No matches found'); return; } reject(new Error(`Grep failed: ${stderr.trim() || error.message}`)); return; } resolve(stdout.trim() || 'No matches found'); }); // Handle abort signal if (abortSignal) { abortSignal.addEventListener('abort', () => { childProcess.kill(); reject(new Error('Operation was aborted')); }); } }); } } /** * Schema for UUID generation (no arguments needed) */ const UuidToolSchema = z.object({ version: z.literal(4).optional().describe('UUID version (only v4 supported)'), }); /** * UUID generation tool */ class UuidTool extends BaseTool { name = 'uuid'; description = 'Generate a random UUID v4'; schema = UuidToolSchema; async execute(args, abortSignal) { if (abortSignal?.aborted) { throw new Error('Operation was aborted'); } // Validate args even though they're optional this.validateArgs(args); return randomUUID(); } } /** * Schema for human loop operations */ const HumanLoopToolSchema = z.object({ prompt: z.string().default('Need input:').describe('The prompt to show to the human operator'), }); /** * Human loop tool for getting input from human operators */ class HumanLoopTool extends BaseTool { name = 'human_loop'; description = 'Pause and ask the human operator for guidance or input'; schema = HumanLoopToolSchema; async execute(args, abortSignal) { if (abortSignal?.aborted) { throw new Error('Operation was aborted'); } const { prompt } = this.validateArgs(args); return new Promise((resolve, reject) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // Handle abort signal const cleanup = () => { rl.close(); }; if (abortSignal) { abortSignal.addEventListener('abort', () => { cleanup(); reject(new Error('Operation was aborted')); }); } rl.question(`${prompt} `, (answer) => { cleanup(); if (abortSignal?.aborted) { reject(new Error('Operation was aborted')); return; } resolve(answer.trim()); }); }); } } // ---------- schema ---------- const WebSearchSchema = z.object({ query: z.string() .min(2, 'Query must be at least 2 characters') .describe('The search term'), count: z.number() .int() .min(1) .max(20) .default(10) .describe('How many results to return (1-20)') }); // ---------- tool ---------- class WebSearchTool extends BaseTool { name = 'web-search'; description = 'Search the web and return a markdown list of results (title, URL, snippet).'; schema = WebSearchSchema; endpoint = 'https://api.search.brave.com/res/v1/web/search'; apiKey = process.env.BRAVE_API_KEY ?? ''; async execute(args, abortSignal) { // 1. validate + early cancel const { query, count } = this.validateArgs(args); if (abortSignal?.aborted) throw new Error('Search was cancelled'); // 2. fetch results const url = new URL(this.endpoint); url.searchParams.set('q', query); url.searchParams.set('count', String(count)); const res = await fetch(url.toString(), { headers: { 'X-Subscription-Token': this.apiKey }, signal: abortSignal }); if (!res.ok) { throw new Error(`Brave API error ${res.status}`); } const data = await res.json(); // 3. extract const list = data.web?.results?.map((r) => ({ title: r.title, url: r.url, desc: r.description ?? '' })) ?? []; // 4. format if (!list.length) return 'No results found.'; return ('## Search Results\n\n' + list .slice(0, count) .map((r, i) => `${i + 1}. [${r.title}](${r.url})\n${r.desc?.trim() ?? ''}`) .join('\n\n')); } } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var express$2 = {exports: {}}; var bodyParser$1 = {exports: {}}; var httpErrors = {exports: {}}; /*! * depd * Copyright(c) 2014-2018 Douglas Christopher Wilson * MIT Licensed */ /** * Module dependencies. */ var relative = path$3.relative; /** * Module exports. */ var depd_1 = depd; /** * Get the path to base files on. */ var basePath = process.cwd(); /** * Determine if namespace is contained in the string. */ function containsNamespace (str, namespace) { var vals = str.split(/[ ,]+/); var ns = String(namespace).toLowerCase(); for (var i = 0; i < vals.length; i++) { var val = vals[i]; // namespace contained if (val && (val === '*' || val.toLowerCase() === ns)) { return true } } return false } /** * Convert a data descriptor to accessor descriptor. */ function convertDataDescriptorToAccessor (obj, prop, message) { var descriptor = Object.getOwnPropertyDescriptor(obj, prop); var value = descriptor.value; descriptor.get = function getter () { return value }; if (descriptor.writable) { descriptor.set = function setter (val) { return (value = val) }; } delete descriptor.value; delete descriptor.writable; Object.defineProperty(obj, prop, descriptor); return descriptor } /** * Create arguments string to keep arity. */ function createArgumentsString (arity) { var str = ''; for (var i = 0; i < arity; i++) { str += ', arg' + i; } return str.substr(2) } /** * Create stack string from stack. */ function createStackString (stack) { var str = this.name + ': ' + this.namespace; if (this.message) { str += ' deprecated ' + this.message; } for (var i = 0; i < stack.length; i++) { str += '\n at ' + stack[i].toString(); } return str } /** * Create deprecate for namespace in caller. */ function depd (namespace) { if (!namespace) { throw new TypeError('argument namespace is required') } var stack = getStack(); var site = callSiteLocation(stack[1]); var file = site[0]; function deprecate (message) { // call to self as log log.call(deprecate, message); } deprecate._file = file; deprecate._ignored = isignored(namespace); deprecate._namespace = namespace; deprecate._traced = istraced(namespace); deprecate._warned = Object.create(null); deprecate.function = wrapfunction; deprecate.property = wrapproperty; return deprecate } /** * Determine if event emitter has listeners of a given type. * * The way to do this check is done three different ways in Node.js >= 0.8 * so this consolidates them into a minimal set using instance methods. * * @param {EventEmitter} emitter * @param {string} type * @returns {boolean} * @private */ function eehaslisteners (emitter, type) { var count = typeof emitter.listenerCount !== 'function' ? emitter.listeners(type).length : emitter.listenerCount(type); return count > 0 } /** * Determine if namespace is ignored. */ function isignored (namespace) { if (process.noDeprecation) { // --no-deprecation support return true } var str = process.env.NO_DEPRECATION || ''; // namespace ignored return containsNamespace(str, namespace) } /** * Determine if namespace is traced. */ function istraced (namespace) { if (process.traceDeprecation) { // --trace-deprecation support return true } var str = process.env.TRACE_DEPRECATION || ''; // namespace traced return containsNamespace(str, namespace) } /** * Display deprecation message. */ function log (message, site) { var haslisteners = eehaslisteners(process, 'deprecation'); // abort early if no destination if (!haslisteners && this._ignored) { return } var caller; var callFile; var callSite; var depSite; var i = 0; var seen = false; var stack = getStack(); var file = this._file; if (site) { // provided site depSite = site; callSite = callSiteLocation(stack[1]); callSite.name = depSite.name; file = callSite[0]; } else { // get call site i = 2; depSite = callSiteLocation(stack[i]); callSite = depSite; } // get caller of deprecated thing in relation to file for (; i < stack.length; i++) { caller = callSiteLocation(stack[i]); callFile = caller[0]; if (callFile === file) { seen = true; } else if (callFile === this._file) { file = this._file; } else if (seen) { break } } var key = caller ? depSite.join(':') + '__' + caller.join(':') : undefined; if (key !== undefined && key in this._warned) { // already warned return } this._warned[key] = true; // generate automatic message from call site var msg = message; if (!msg) { msg = callSite === depSite || !callSite.name ? defaultMessage(depSite) : defaultMessage(callSite); } // emit deprecation if listeners exist if (haslisteners) { var err = DeprecationError(this._namespace, msg, stack.slice(i)); process.emit('deprecation', err); return } // format and write message var format = process.stderr.isTTY ? formatColor : formatPlain; var output = format.call(this, msg, caller, stack.slice(i)); process.stderr.write(output + '\n', 'utf8'); } /** * Get call site location as array. */ function callSiteLocation (callSite) { var file = callSite.getFileName() || '<anonymous>'; var line = callSite.getLineNumber(); var colm = callSite.getColumnNumber(); if (callSite.isEval()) { file = callSite.getEvalOrigin() + ', ' + file; } var site = [file, line, colm]; site.callSite = callSite; site.name = callSite.getFunctionName(); return site } /** * Generate a default message from the site. */ function defaultMessage (site) { var callSite = site.callSite; var funcName = site.name; // make useful anonymous name if (!funcName) { funcName = '<anonymous@' + formatLocation(site) + '>'; } var context = callSite.getThis(); var typeName = context && callSite.getTypeName(); // ignore useless type name if (typeName === 'Object') { typeName = undefined; } // make useful type name if (typeName === 'Function') { typeName = context.name || typeName; } return typeName && callSite.getMethodName() ? typeName + '.' + funcName : funcName } /** * Format deprecation message without color. */ function formatPlain (msg, caller, stack) { var timestamp = new Date().toUTCString(); var formatted = timestamp + ' ' + this._namespace + ' deprecated ' + msg; // add stack trace if (this._traced) { for (var i = 0; i < stack.length; i++) { formatted += '\n at ' + stack[i].toString(); } return formatted } if (caller) { formatted += ' at ' + formatLocation(caller); } return formatted } /** * Format deprecation message with color. */ function formatColor (msg, caller, stack) { var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' + // bold cyan ' \x1b[33;1mdeprecated\x1b[22;39m' + // bold yellow ' \x1b[0m' + msg + '\x1b[39m'; // reset // add stack trace if (this._traced) { for (var i = 0; i < stack.length; i++) { formatted += '\n \x1b[36mat ' + stack[i].toString() + '\x1b[39m'; // cyan } return formatted } if (caller) { formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m'; // cyan } return formatted } /** * Format call site location. */ function formatLocation (callSite) { return relative(basePath, callSite[0]) + ':' + callSite[1] + ':' + callSite[2] } /** * Get the stack as array of call sites. */ function getStack () { var limit = Error.stackTraceLimit; var obj = {}; var prep = Error.prepareStackTrace; Error.prepareStackTrace = prepareObjectStackTrace; Error.stackTraceLimit = Math.max(10, limit); // capture the stack Error.captureStackTrace(obj); // slice this function off the top var stack = obj.stack.slice(1); Error.prepareStackTrace = prep; Error.stackTraceLimit = limit; return stack } /** * Capture call site stack from v8. */ function prepareObjectStackTrace (obj, stack) { return stack } /** * Return a wrapped function in a deprecation message. */ function wrapfunction (fn, message) { if (typeof fn !== 'function') { throw new TypeError('argument fn must be a function') } var args = createArgumentsString(fn.length); var stack = getStack(); var site = callSiteLocation(stack[1]); site.name = fn.name; // eslint-disable-next-line no-new-func var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site', '"use strict"\n' + 'return function (' + args + ') {' + 'log.call(deprecate, message, site)\n' + 'return fn.apply(this, arguments)\n' + '}')(fn, log, this, message, site); return deprecatedfn } /** * Wrap property in a deprecation message. */ function wrapproperty (obj, prop, message) { if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { throw new TypeError('argument obj must be object') } var descriptor = Object.getOwnPropertyDescriptor(obj, prop); if (!descriptor) { throw new TypeError('must