@astreus-ai/astreus
Version:
Open-source AI agent framework for building autonomous systems that solve real-world tasks effectively.
1 lines • 1.19 MB
Source Map (JSON)
{"version":3,"sources":["../src/agent/defaults.ts","../src/plugin/defaults.ts","../src/errors/index.ts","../src/plugin/index.ts","../src/task/index.ts","../src/database/index.ts","../src/database/defaults.ts","../src/logger/index.ts","../src/database/knex.ts","../src/database/encryption.ts","../src/database/utils.ts","../src/database/sensitive-fields.ts","../src/llm/providers/openai.ts","../src/llm/types.ts","../src/llm/providers/claude.ts","../src/llm/providers/gemini.ts","../src/llm/providers/ollama.ts","../src/llm/models.ts","../src/llm/defaults.ts","../src/llm/index.ts","../src/memory/defaults.ts","../src/memory/index.ts","../src/task/defaults.ts","../src/mcp/index.ts","../src/knowledge/storage.ts","../src/knowledge/defaults.ts","../src/knowledge/plugin.ts","../src/knowledge/index.ts","../src/vision/tools.ts","../src/vision/index.ts","../src/vision/defaults.ts","../src/sub-agent/coordination.ts","../src/sub-agent/defaults.ts","../src/sub-agent/delegation.ts","../src/sub-agent/index.ts","../src/context/defaults.ts","../src/context/strategies.ts","../src/context/compressor.ts","../src/context/storage.ts","../src/context/manager.ts","../src/agent/index.ts","../src/graph/storage.ts","../src/graph/index.ts","../src/scheduler/parser.ts"],"sourcesContent":["/**\n * Default configuration values for agents\n */\nexport const DEFAULT_AGENT_CONFIG = {\n model: 'gpt-4o-mini',\n temperature: 0.7,\n maxTokens: 2000,\n useTools: true,\n memory: false,\n knowledge: false,\n vision: false,\n autoContextCompression: false,\n debug: false,\n} as const;\n\n/**\n * Get default value for an agent config property\n */\nexport function getDefaultValue<K extends keyof typeof DEFAULT_AGENT_CONFIG>(\n key: K\n): (typeof DEFAULT_AGENT_CONFIG)[K] {\n return DEFAULT_AGENT_CONFIG[key];\n}\n","/**\n * Default configuration values for plugin module\n */\nexport const DEFAULT_PLUGIN_CONFIG = {\n defaultVersion: 'unknown',\n defaultDescription: 'none',\n missingName: '[missing]',\n defaultTimeout: 30000, // 30 seconds\n} as const;\n\n/**\n * Get default value for a plugin config property\n */\nexport function getPluginDefaultValue<K extends keyof typeof DEFAULT_PLUGIN_CONFIG>(\n key: K\n): (typeof DEFAULT_PLUGIN_CONFIG)[K] {\n return DEFAULT_PLUGIN_CONFIG[key];\n}\n","/**\n * Custom Error Classes for Astreus\n *\n * These error classes provide structured error handling with:\n * - Error cause chaining (ES2022 Error cause)\n * - Provider/service context\n * - Proper error names for debugging\n *\n * Graceful Degradation Notes:\n * - LLMApiError: Provider failures should trigger retry with exponential backoff\n * - DatabaseError: Non-critical DB operations can use cached data as fallback\n * - MCPConnectionError: MCP server failures allow agent to continue without that tool\n * - SubAgentError: Sub-agent failures return partial results when possible\n */\n\n/**\n * Base error class for all Astreus errors\n * Provides consistent error structure with cause chaining\n */\nexport class AstreusError extends Error {\n public readonly cause?: Error;\n\n constructor(message: string, cause?: Error) {\n super(message);\n this.cause = cause;\n this.name = 'AstreusError';\n // Maintain proper prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Get the full error chain as a string for logging\n */\n getFullStack(): string {\n let fullStack = this.stack || this.message;\n if (this.cause instanceof Error) {\n fullStack += `\\nCaused by: ${this.cause.stack || this.cause.message}`;\n }\n return fullStack;\n }\n}\n\n/**\n * Error thrown when an LLM API request fails\n * Use this for OpenAI, Claude, Gemini, Ollama provider errors\n *\n * Graceful Degradation:\n * - Retry with exponential backoff (already implemented in providers)\n * - For embedding failures: continue without semantic search\n * - For vision failures: return text-based analysis if available\n */\nexport class LLMApiError extends AstreusError {\n constructor(\n message: string,\n public readonly provider: string,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'LLMApiError';\n }\n}\n\n/**\n * Error thrown for database operations\n * Use this for connection, query, and schema errors\n *\n * Graceful Degradation:\n * - Memory operations: Fall back to in-memory cache\n * - Agent config: Use default configuration\n * - Context storage: Continue without persistence\n */\nexport class DatabaseError extends AstreusError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'DatabaseError';\n }\n}\n\n/**\n * Error thrown when MCP server connection or communication fails\n * Use this for MCP process spawn, message, and tool call errors\n *\n * Graceful Degradation:\n * - Tool discovery: Agent continues without MCP tools\n * - Tool call failure: Return error result to LLM for alternative approach\n * - Server crash: Attempt reconnection on next tool call\n */\nexport class MCPConnectionError extends AstreusError {\n constructor(\n message: string,\n public readonly serverName: string,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'MCPConnectionError';\n }\n}\n\n/**\n * Error thrown when sub-agent operations fail\n * Use this for delegation, coordination, and execution errors\n *\n * Graceful Degradation:\n * - Single sub-agent failure: Return results from successful sub-agents\n * - All sub-agents fail: Fall back to main agent processing\n * - Timeout: Return partial results with timeout indication\n */\nexport class SubAgentError extends AstreusError {\n constructor(\n message: string,\n public readonly agentId?: string,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'SubAgentError';\n }\n}\n\n/**\n * Error thrown when context operations fail\n * Use this for context compression, loading, and storage errors\n *\n * Graceful Degradation:\n * - Compression failure: Use uncompressed context (may hit token limits)\n * - Load failure: Start with empty context\n * - Save failure: Log warning and continue (context lost on restart)\n */\nexport class ContextError extends AstreusError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'ContextError';\n }\n}\n\n/**\n * Error thrown when knowledge base operations fail\n * Use this for document indexing, search, and retrieval errors\n *\n * Graceful Degradation:\n * - Search failure: Continue without knowledge augmentation\n * - Index failure: Log warning and skip document\n * - Embedding failure: Fall back to keyword search\n */\nexport class KnowledgeError extends AstreusError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'KnowledgeError';\n }\n}\n\n/**\n * Error thrown when vision analysis fails\n * Use this for image processing and analysis errors\n *\n * Graceful Degradation:\n * - Analysis failure: Return error message to user\n * - Unsupported format: Suggest alternative formats\n * - Model unavailable: Fall back to text description request\n */\nexport class VisionError extends AstreusError {\n constructor(\n message: string,\n public readonly provider: string,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'VisionError';\n }\n}\n\n/**\n * Error thrown when configuration is invalid\n * This is typically a non-recoverable error requiring user intervention\n */\nexport class ConfigurationError extends AstreusError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'ConfigurationError';\n }\n}\n\n/**\n * Error thrown when a Graph node execution fails\n * Provides detailed error chain with node context for debugging\n *\n * Graceful Degradation:\n * - Node failure: Skip dependent nodes, continue with independent ones\n * - Critical node failure: Mark graph as failed, return partial results\n * - Timeout: Return timeout error with node context\n */\nexport class GraphNodeError extends AstreusError {\n constructor(\n message: string,\n public readonly nodeId: string,\n public readonly nodeName: string,\n public readonly step: 'initialization' | 'dependency_check' | 'execution' | 'result_processing',\n public readonly graphId?: string,\n public readonly parentNodeId?: string,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'GraphNodeError';\n }\n\n /**\n * Get error chain information for debugging\n */\n getErrorChain(): {\n nodeId: string;\n nodeName: string;\n step: string;\n graphId?: string;\n parentNodeId?: string;\n message: string;\n cause?: string;\n } {\n return {\n nodeId: this.nodeId,\n nodeName: this.nodeName,\n step: this.step,\n graphId: this.graphId,\n parentNodeId: this.parentNodeId,\n message: this.message,\n cause: this.cause instanceof Error ? this.cause.message : undefined,\n };\n }\n}\n\n/**\n * Error thrown when a tool call fails (Plugin or MCP)\n * Provides normalized error structure across all tool types\n *\n * Graceful Degradation:\n * - Tool not found: Return error message to LLM\n * - Validation failure: Return validation error to LLM\n * - Execution failure: Return error with context for LLM to try alternative\n * - Timeout: Return timeout error with retry suggestion\n */\nexport class ToolError extends AstreusError {\n constructor(\n message: string,\n public readonly toolName: string,\n public readonly toolType: 'plugin' | 'mcp' | 'unknown',\n public readonly errorType: 'not_found' | 'validation' | 'execution' | 'timeout' | 'unknown',\n public readonly recoverable: boolean = true,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'ToolError';\n }\n\n /**\n * Get normalized error response for LLM\n */\n toToolResult(): { success: false; error: string; recoverable: boolean; toolType: string } {\n return {\n success: false,\n error: this.message,\n recoverable: this.recoverable,\n toolType: this.toolType,\n };\n }\n}\n\n/**\n * Type guard to check if an error is a GraphNodeError\n */\nexport function isGraphNodeError(error: unknown): error is GraphNodeError {\n return error instanceof GraphNodeError;\n}\n\n/**\n * Type guard to check if an error is a ToolError\n */\nexport function isToolError(error: unknown): error is ToolError {\n return error instanceof ToolError;\n}\n\n/**\n * Type guard to check if an error is an AstreusError\n */\nexport function isAstreusError(error: unknown): error is AstreusError {\n return error instanceof AstreusError;\n}\n\n/**\n * Type guard to check if an error is an LLMApiError\n */\nexport function isLLMApiError(error: unknown): error is LLMApiError {\n return error instanceof LLMApiError;\n}\n\n/**\n * Type guard to check if an error is a DatabaseError\n */\nexport function isDatabaseError(error: unknown): error is DatabaseError {\n return error instanceof DatabaseError;\n}\n\n/**\n * Type guard to check if an error is an MCPConnectionError\n */\nexport function isMCPConnectionError(error: unknown): error is MCPConnectionError {\n return error instanceof MCPConnectionError;\n}\n\n/**\n * Type guard to check if an error is a SubAgentError\n */\nexport function isSubAgentError(error: unknown): error is SubAgentError {\n return error instanceof SubAgentError;\n}\n\n/**\n * Wrap an error with proper cause chaining\n * Helper function for consistent error wrapping\n *\n * Note: The error class constructor signature is (message, ...contextArgs, cause?)\n * So we need to place the cause at the end of the args array\n */\nexport function wrapError<T extends AstreusError>(\n ErrorClass: new (message: string, ...args: unknown[]) => T,\n message: string,\n cause: unknown,\n ...contextArgs: unknown[]\n): T {\n const originalError = cause instanceof Error ? cause : new Error(String(cause));\n // Place cause at the end as per AstreusError subclass constructor signatures\n // e.g., LLMApiError(message, provider, cause?), MCPConnectionError(message, serverName, cause?)\n return new ErrorClass(message, ...contextArgs, originalError) as T;\n}\n\n/**\n * Type-safe wrapError for LLMApiError\n */\nexport function wrapLLMApiError(message: string, provider: string, cause: unknown): LLMApiError {\n const originalError = cause instanceof Error ? cause : new Error(String(cause));\n return new LLMApiError(message, provider, originalError);\n}\n\n/**\n * Type-safe wrapError for MCPConnectionError\n */\nexport function wrapMCPConnectionError(\n message: string,\n serverName: string,\n cause: unknown\n): MCPConnectionError {\n const originalError = cause instanceof Error ? cause : new Error(String(cause));\n return new MCPConnectionError(message, serverName, originalError);\n}\n\n/**\n * Type-safe wrapError for SubAgentError\n */\nexport function wrapSubAgentError(\n message: string,\n agentId: string | undefined,\n cause: unknown\n): SubAgentError {\n const originalError = cause instanceof Error ? cause : new Error(String(cause));\n return new SubAgentError(message, agentId, originalError);\n}\n\n/**\n * Type-safe wrapError for VisionError\n */\nexport function wrapVisionError(message: string, provider: string, cause: unknown): VisionError {\n const originalError = cause instanceof Error ? cause : new Error(String(cause));\n return new VisionError(message, provider, originalError);\n}\n\n/**\n * Type-safe wrapError for simple errors (no extra context)\n */\nexport function wrapSimpleError<T extends AstreusError>(\n ErrorClass: new (message: string, cause?: Error) => T,\n message: string,\n cause: unknown\n): T {\n const originalError = cause instanceof Error ? cause : new Error(String(cause));\n return new ErrorClass(message, originalError);\n}\n","import { IAgentModule, IAgent } from '../agent/types';\nimport {\n Plugin as IPlugin,\n PluginConfig,\n PluginManager as IPluginManager,\n ToolDefinition,\n ToolCall,\n ToolCallResult,\n ToolContext,\n ToolParameter,\n ToolParameterValue,\n} from './types';\nimport { Logger } from '../logger/types';\nimport { DEFAULT_PLUGIN_CONFIG } from './defaults';\nimport { ToolError } from '../errors';\n\n// Type for LLM function calling tool schema property\ninterface LLMToolProperty {\n type: 'string' | 'number' | 'boolean' | 'object' | 'array';\n description?: string;\n enum?: Array<string | number>;\n items?: { type: string };\n properties?: Record<\n string,\n {\n type: 'string' | 'number' | 'boolean' | 'object' | 'array';\n description?: string;\n }\n >;\n}\n\n// Type for LLM function calling tool schema\ninterface LLMToolSchema {\n type: 'object';\n properties: Record<string, LLMToolProperty>;\n required?: string[];\n}\n\nexport class Plugin implements IAgentModule, IPluginManager {\n readonly name = 'plugin';\n private plugins: Map<string, IPlugin> = new Map();\n private configs: Map<string, PluginConfig> = new Map();\n private tools: Map<string, ToolDefinition> = new Map();\n private logger: Logger;\n private registrationLock: Set<string> = new Set(); // Lock for preventing race conditions\n\n constructor(private agent: IAgent) {\n this.logger = agent.logger;\n\n // User-facing info log\n this.logger.info('Plugin manager initialized');\n\n this.logger.debug('Plugin manager initialized', {\n agentId: agent.id,\n agentName: agent.name,\n });\n }\n\n async initialize(): Promise<void> {\n // User-facing info log\n this.logger.info('Plugin manager ready');\n\n this.logger.debug('Plugin manager initialization completed');\n }\n\n async registerPlugin(plugin: IPlugin, config?: PluginConfig): Promise<void> {\n // User-facing info log\n this.logger.info(`Registering plugin: ${plugin.name}`);\n\n this.logger.debug('Registering plugin', {\n name: plugin.name,\n version: plugin.version,\n description: plugin.description,\n toolCount: plugin.tools.length,\n toolNames: plugin.tools.map((t) => t.name),\n hasConfig: !!config,\n hasInitialize: !!plugin.initialize,\n hasCleanup: !!plugin.cleanup,\n });\n\n // Check if plugin already exists\n if (this.plugins.has(plugin.name)) {\n this.logger.error(`Plugin already registered: ${plugin.name}`);\n this.logger.debug('Plugin registration failed - already exists', {\n pluginName: plugin.name,\n existingPlugins: Array.from(this.plugins.keys()),\n });\n throw new Error(`Plugin '${plugin.name}' is already registered`);\n }\n\n // Validate plugin\n this.logger.debug('Validating plugin structure', { pluginName: plugin.name });\n this.validatePlugin(plugin);\n\n // Set default config\n const pluginConfig: PluginConfig = config || {\n name: plugin.name,\n enabled: true,\n };\n\n this.logger.debug('Plugin config prepared', {\n pluginName: plugin.name,\n enabled: pluginConfig.enabled,\n hasCustomConfig: !!pluginConfig.config,\n });\n\n // Initialize plugin if it has an initialize method\n if (plugin.initialize) {\n this.logger.debug('Initializing plugin', { pluginName: plugin.name });\n await plugin.initialize(pluginConfig.config);\n this.logger.debug('Plugin initialized successfully', { pluginName: plugin.name });\n }\n\n // Register plugin and its tools\n this.plugins.set(plugin.name, plugin);\n this.configs.set(plugin.name, pluginConfig);\n\n // Register tools if plugin is enabled\n if (pluginConfig.enabled) {\n this.logger.debug('Registering plugin tools', {\n pluginName: plugin.name,\n toolCount: plugin.tools.length,\n });\n\n for (const tool of plugin.tools) {\n // Wait if another registration is in progress for this tool name\n // Add timeout to prevent infinite busy-wait\n const maxWaitTime = 5000; // 5 seconds max wait\n const startWait = Date.now();\n while (this.registrationLock.has(tool.name)) {\n if (Date.now() - startWait > maxWaitTime) {\n this.logger.error(`Tool registration lock timeout: ${tool.name}`);\n throw new Error(`Tool registration timeout for '${tool.name}': lock held for too long`);\n }\n await new Promise((r) => setTimeout(r, 10));\n }\n\n // Acquire lock for this tool name\n this.registrationLock.add(tool.name);\n\n try {\n if (this.tools.has(tool.name)) {\n this.logger.error(`Tool name conflict: ${tool.name}`);\n this.logger.debug('Tool registration failed - name conflict', {\n toolName: tool.name,\n pluginName: plugin.name,\n existingTools: Array.from(this.tools.keys()),\n });\n throw new Error(`Tool '${tool.name}' is already registered by another plugin`);\n }\n this.tools.set(tool.name, tool);\n\n this.logger.debug('Tool registered', {\n toolName: tool.name,\n pluginName: plugin.name,\n description: tool.description,\n });\n } finally {\n // Release lock\n this.registrationLock.delete(tool.name);\n }\n }\n } else {\n this.logger.debug('Plugin disabled, tools not registered', {\n pluginName: plugin.name,\n toolCount: plugin.tools.length,\n });\n }\n\n // User-facing success message\n this.logger.info(`Plugin registered: ${plugin.name} (${plugin.tools.length} tools)`);\n\n this.logger.debug('Plugin registered successfully', {\n pluginName: plugin.name,\n version: plugin.version,\n toolsRegistered: pluginConfig.enabled ? plugin.tools.length : 0,\n totalPlugins: this.plugins.size,\n totalTools: this.tools.size,\n });\n }\n\n async unregisterPlugin(name: string): Promise<void> {\n // User-facing info log\n this.logger.info(`Unregistering plugin: ${name}`);\n\n this.logger.debug('Unregistering plugin', {\n pluginName: name,\n isRegistered: this.plugins.has(name),\n });\n\n const plugin = this.plugins.get(name);\n if (!plugin) {\n this.logger.error(`Plugin not found: ${name}`);\n this.logger.debug('Plugin unregistration failed - not found', {\n pluginName: name,\n availablePlugins: Array.from(this.plugins.keys()),\n });\n throw new Error(`Plugin '${name}' is not registered`);\n }\n\n // Remove tools\n const removedTools = [];\n for (const tool of plugin.tools) {\n if (this.tools.has(tool.name)) {\n this.tools.delete(tool.name);\n removedTools.push(tool.name);\n\n this.logger.debug('Tool unregistered', {\n toolName: tool.name,\n pluginName: name,\n });\n }\n }\n\n this.logger.debug('Plugin tools removed', {\n pluginName: name,\n removedTools,\n removedCount: removedTools.length,\n });\n\n // Cleanup plugin if it has a cleanup method\n if (plugin.cleanup) {\n this.logger.debug('Running plugin cleanup', { pluginName: name });\n await plugin.cleanup();\n this.logger.debug('Plugin cleanup completed', { pluginName: name });\n }\n\n // Remove plugin\n this.plugins.delete(name);\n this.configs.delete(name);\n\n // User-facing success message\n this.logger.info(`Plugin unregistered: ${name}`);\n\n this.logger.debug('Plugin unregistered successfully', {\n pluginName: name,\n removedToolCount: removedTools.length,\n remainingPlugins: this.plugins.size,\n remainingTools: this.tools.size,\n });\n }\n\n getPlugin(name: string): IPlugin | undefined {\n const plugin = this.plugins.get(name);\n\n this.logger.debug('Plugin lookup', {\n pluginName: name,\n found: !!plugin,\n version: plugin?.version ?? DEFAULT_PLUGIN_CONFIG.defaultVersion,\n });\n\n return plugin;\n }\n\n getTools(): ToolDefinition[] {\n const tools = Array.from(this.tools.values());\n\n this.logger.debug('Retrieved all tools', {\n toolCount: tools.length,\n toolNames: tools.map((t) => t.name),\n });\n\n return tools;\n }\n\n getTool(name: string): ToolDefinition | undefined {\n const tool = this.tools.get(name);\n\n this.logger.debug('Tool lookup', {\n toolName: name,\n found: !!tool,\n description: tool?.description ?? DEFAULT_PLUGIN_CONFIG.defaultDescription,\n });\n\n return tool;\n }\n\n async executeTool(toolCall: ToolCall, context?: ToolContext): Promise<ToolCallResult> {\n const startTime = Date.now();\n\n // User-facing info log\n this.logger.info(`Executing tool: ${toolCall.name}`);\n\n this.logger.debug('Executing tool', {\n toolName: toolCall.name,\n callId: toolCall.id,\n parameters: Object.keys(toolCall.parameters),\n parameterCount: Object.keys(toolCall.parameters).length,\n hasContext: !!context,\n });\n\n try {\n const tool = this.getTool(toolCall.name);\n if (!tool) {\n this.logger.error(`Tool not found: ${toolCall.name}`);\n this.logger.debug('Tool execution failed - tool not found', {\n toolName: toolCall.name,\n callId: toolCall.id,\n availableTools: Array.from(this.tools.keys()),\n });\n\n return {\n id: toolCall.id,\n name: toolCall.name,\n result: {\n success: false,\n error: `Tool '${toolCall.name}' not found`,\n },\n executionTime: Date.now() - startTime,\n };\n }\n\n // Validate parameters\n this.logger.debug('Validating tool parameters', {\n toolName: toolCall.name,\n callId: toolCall.id,\n });\n\n const validationError = this.validateToolParameters(tool, toolCall.parameters);\n if (validationError) {\n this.logger.error(`Tool parameter validation failed: ${toolCall.name}`);\n this.logger.debug('Tool parameter validation error', {\n toolName: toolCall.name,\n callId: toolCall.id,\n validationError,\n parameters: toolCall.parameters,\n });\n\n return {\n id: toolCall.id,\n name: toolCall.name,\n result: {\n success: false,\n error: validationError,\n },\n executionTime: Date.now() - startTime,\n };\n }\n\n // Validate agent is available\n if (!this.agent || typeof this.agent.id !== 'string') {\n this.logger.error('Agent not available for tool execution');\n return {\n id: toolCall.id,\n name: toolCall.name,\n result: {\n success: false,\n error: 'Agent not available for tool execution',\n },\n executionTime: Date.now() - startTime,\n };\n }\n\n // Create isolated execution context for this tool call\n // This prevents state sharing between plugins and tool calls\n const isolatedContext: ToolContext = {\n // Copy existing context properties if provided\n ...(context || {}),\n // Add execution isolation metadata\n agentId: context?.agentId || this.agent.id,\n agent: context?.agent || this.agent,\n // Add unique execution ID for this specific call\n executionId: `${toolCall.id}-${Date.now()}`,\n // Isolate any shared state by creating fresh copies\n toolName: toolCall.name,\n callTimestamp: new Date(),\n };\n\n // Execute tool with timeout\n this.logger.debug('Calling tool handler with isolated context', {\n toolName: toolCall.name,\n callId: toolCall.id,\n timeout: DEFAULT_PLUGIN_CONFIG.defaultTimeout,\n executionId: isolatedContext.executionId ?? null,\n });\n\n let timeoutId: NodeJS.Timeout;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(\n new Error(\n `Tool '${toolCall.name}' execution timed out after ${DEFAULT_PLUGIN_CONFIG.defaultTimeout}ms`\n )\n );\n }, DEFAULT_PLUGIN_CONFIG.defaultTimeout);\n });\n\n let result;\n try {\n result = await Promise.race([\n tool.handler(toolCall.parameters, isolatedContext),\n timeoutPromise,\n ]);\n clearTimeout(timeoutId!);\n } catch (raceError) {\n clearTimeout(timeoutId!);\n throw raceError;\n }\n const executionTime = Date.now() - startTime;\n\n // User-facing success message\n this.logger.info(`Tool completed: ${toolCall.name} (${executionTime}ms)`);\n\n this.logger.debug('Tool execution successful', {\n toolName: toolCall.name,\n callId: toolCall.id,\n executionTime,\n success: result.success,\n hasData: !!result.data,\n hasError: !!result.error,\n executionId: isolatedContext.executionId ?? null,\n });\n\n return {\n id: toolCall.id,\n name: toolCall.name,\n result,\n executionTime,\n };\n } catch (error) {\n const executionTime = Date.now() - startTime;\n const originalError = error instanceof Error ? error : new Error('Unknown error occurred');\n\n // Determine error type based on error characteristics\n let errorType: 'not_found' | 'validation' | 'execution' | 'timeout' | 'unknown' = 'execution';\n if (\n originalError.message.includes('timed out') ||\n originalError.message.includes('timeout')\n ) {\n errorType = 'timeout';\n } else if (\n originalError.message.includes('not found') ||\n originalError.message.includes('not available')\n ) {\n errorType = 'not_found';\n } else if (\n originalError.message.includes('Invalid') ||\n originalError.message.includes('validation')\n ) {\n errorType = 'validation';\n }\n\n // Determine if error is recoverable\n const recoverable = errorType !== 'not_found';\n\n // Create normalized ToolError for consistent error handling\n const toolError = new ToolError(\n `Plugin tool '${toolCall.name}' failed: ${originalError.message}`,\n toolCall.name,\n 'plugin',\n errorType,\n recoverable,\n originalError\n );\n\n this.logger.error(`Tool execution failed: ${toolCall.name}`, toolError);\n this.logger.debug('Tool execution error', {\n toolName: toolCall.name,\n callId: toolCall.id,\n executionTime,\n errorType,\n recoverable,\n error: originalError.message,\n hasStack: !!originalError.stack,\n });\n\n // Return normalized error result\n const errorResult = toolError.toToolResult();\n return {\n id: toolCall.id,\n name: toolCall.name,\n result: {\n success: false,\n error: errorResult.error,\n },\n executionTime,\n };\n }\n }\n\n listPlugins(): IPlugin[] {\n const plugins = Array.from(this.plugins.values());\n\n this.logger.debug('Listed plugins', {\n pluginCount: plugins.length,\n pluginNames: plugins.map((p) => p.name),\n });\n\n return plugins;\n }\n\n // Get tools formatted for LLM function calling\n getToolsForLLM(): Array<{\n type: string;\n function: { name: string; description: string; parameters: LLMToolSchema };\n }> {\n const tools = this.getTools();\n const llmTools = tools.map((tool) => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: this.convertParametersToJsonSchema(tool.parameters),\n },\n }));\n\n this.logger.debug('Generated LLM tool schemas', {\n toolCount: llmTools.length,\n toolNames: llmTools.map((t) => t.function.name),\n });\n\n return llmTools;\n }\n\n private validatePlugin(plugin: IPlugin): void {\n this.logger.debug('Validating plugin structure', {\n pluginName: plugin.name,\n hasName: !!plugin.name,\n hasVersion: !!plugin.version,\n hasTools: Array.isArray(plugin.tools),\n toolCount: Array.isArray(plugin.tools) ? plugin.tools.length : 0,\n });\n\n // Validate plugin name\n if (!plugin.name || typeof plugin.name !== 'string' || plugin.name.trim().length === 0) {\n this.logger.debug('Plugin validation failed - invalid name', {\n pluginName: plugin.name ?? DEFAULT_PLUGIN_CONFIG.missingName,\n nameType: typeof plugin.name,\n });\n throw new Error('Plugin must have a valid non-empty name');\n }\n\n // Validate plugin version\n if (\n !plugin.version ||\n typeof plugin.version !== 'string' ||\n plugin.version.trim().length === 0\n ) {\n this.logger.debug('Plugin validation failed - invalid version', {\n pluginName: plugin.name,\n version: plugin.version ?? DEFAULT_PLUGIN_CONFIG.defaultVersion,\n versionType: typeof plugin.version,\n });\n throw new Error(`Plugin '${plugin.name}' must have a valid non-empty version`);\n }\n\n // Validate plugin name format (alphanumeric with hyphens/underscores)\n const namePattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/;\n if (!namePattern.test(plugin.name)) {\n this.logger.debug('Plugin validation failed - invalid name format', {\n pluginName: plugin.name,\n });\n throw new Error(\n `Plugin name '${plugin.name}' must start with a letter and contain only alphanumeric characters, hyphens, or underscores`\n );\n }\n\n if (!Array.isArray(plugin.tools)) {\n this.logger.debug('Plugin validation failed - invalid tools array', {\n pluginName: plugin.name,\n toolsType: typeof plugin.tools,\n isArray: Array.isArray(plugin.tools),\n });\n throw new Error(`Plugin '${plugin.name}' must have tools array`);\n }\n\n // Check for duplicate tool names within the plugin\n const toolNames = new Set<string>();\n for (const tool of plugin.tools) {\n if (toolNames.has(tool.name)) {\n this.logger.debug('Plugin validation failed - duplicate tool name', {\n pluginName: plugin.name,\n duplicateToolName: tool.name,\n });\n throw new Error(`Plugin '${plugin.name}' has duplicate tool name '${tool.name}'`);\n }\n toolNames.add(tool.name);\n }\n\n // Validate each tool\n for (const tool of plugin.tools) {\n this.logger.debug('Validating tool definition', {\n pluginName: plugin.name,\n toolName: tool.name,\n hasName: !!tool.name,\n hasDescription: !!tool.description,\n hasHandler: !!tool.handler,\n });\n\n if (!tool.name || typeof tool.name !== 'string' || tool.name.trim().length === 0) {\n this.logger.debug('Tool validation failed - invalid name', {\n pluginName: plugin.name,\n toolName: tool.name ?? DEFAULT_PLUGIN_CONFIG.missingName,\n });\n throw new Error(`Invalid tool name in plugin '${plugin.name}'`);\n }\n\n if (!tool.description || typeof tool.description !== 'string') {\n this.logger.debug('Tool validation failed - invalid description', {\n pluginName: plugin.name,\n toolName: tool.name,\n });\n throw new Error(\n `Tool '${tool.name}' in plugin '${plugin.name}' must have a valid description`\n );\n }\n\n if (!tool.handler || typeof tool.handler !== 'function') {\n this.logger.debug('Tool validation failed - invalid handler', {\n pluginName: plugin.name,\n toolName: tool.name,\n handlerType: typeof tool.handler,\n });\n throw new Error(\n `Tool '${tool.name}' in plugin '${plugin.name}' must have a valid handler function`\n );\n }\n\n // Validate tool parameters\n if (tool.parameters && typeof tool.parameters === 'object') {\n this.validateToolParameterDefinitions(plugin.name, tool.name, tool.parameters);\n }\n }\n\n this.logger.debug('Plugin validation successful', {\n pluginName: plugin.name,\n version: plugin.version,\n toolCount: plugin.tools.length,\n });\n }\n\n private validateToolParameterDefinitions(\n pluginName: string,\n toolName: string,\n parameters: Record<string, ToolParameter>,\n depth: number = 0\n ): void {\n // Prevent stack overflow with depth limit\n const MAX_DEPTH = 50;\n if (depth > MAX_DEPTH) {\n throw new Error(`Parameter nesting too deep in tool '${toolName}' (max depth: ${MAX_DEPTH})`);\n }\n\n const validTypes = ['string', 'number', 'boolean', 'object', 'array'];\n\n for (const [paramName, paramDef] of Object.entries(parameters)) {\n if (!paramDef.type || !validTypes.includes(paramDef.type)) {\n this.logger.debug('Parameter validation failed - invalid type', {\n pluginName,\n toolName,\n paramName,\n paramType: paramDef.type,\n validTypes,\n });\n throw new Error(\n `Parameter '${paramName}' in tool '${toolName}' has invalid type '${paramDef.type}'`\n );\n }\n\n if (!paramDef.description || typeof paramDef.description !== 'string') {\n this.logger.debug('Parameter validation failed - missing description', {\n pluginName,\n toolName,\n paramName,\n });\n throw new Error(`Parameter '${paramName}' in tool '${toolName}' must have a description`);\n }\n\n // Validate nested object properties\n if (paramDef.type === 'object' && paramDef.properties) {\n this.validateToolParameterDefinitions(pluginName, toolName, paramDef.properties, depth + 1);\n }\n\n // Validate array items\n if (paramDef.type === 'array' && paramDef.items) {\n if (!validTypes.includes(paramDef.items.type)) {\n this.logger.debug('Parameter validation failed - invalid array items type', {\n pluginName,\n toolName,\n paramName,\n itemsType: paramDef.items.type,\n });\n throw new Error(\n `Array parameter '${paramName}' in tool '${toolName}' has invalid items type`\n );\n }\n }\n }\n }\n\n private validateToolParameters(\n tool: ToolDefinition,\n parameters: Record<string, ToolParameterValue>\n ): string | null {\n this.logger.debug('Validating tool parameters', {\n toolName: tool.name,\n expectedParams: Object.keys(tool.parameters),\n providedParams: Object.keys(parameters),\n parameterCount: Object.keys(parameters).length,\n });\n\n for (const [paramName, paramDef] of Object.entries(tool.parameters)) {\n const value = parameters[paramName];\n\n this.logger.debug('Validating parameter', {\n toolName: tool.name,\n paramName,\n paramType: paramDef.type,\n required: !!paramDef.required,\n hasValue: value !== undefined && value !== null,\n valueType: typeof value,\n });\n\n // Check required parameters\n if (paramDef.required && (value === undefined || value === null)) {\n this.logger.debug('Required parameter missing', {\n toolName: tool.name,\n paramName,\n paramType: paramDef.type,\n });\n return `Required parameter '${paramName}' is missing`;\n }\n\n // Type validation\n if (value !== undefined && value !== null) {\n if (!this.isToolParameterValue(value) || !this.validateParameterType(value, paramDef)) {\n this.logger.debug('Parameter type validation failed', {\n toolName: tool.name,\n paramName,\n expectedType: paramDef.type,\n actualType: typeof value,\n value: String(value).slice(0, 100), // Truncate long values\n });\n return `Parameter '${paramName}' has invalid type. Expected ${paramDef.type}`;\n }\n }\n }\n\n this.logger.debug('Tool parameter validation successful', {\n toolName: tool.name,\n validatedParams: Object.keys(tool.parameters).length,\n });\n\n return null;\n }\n\n private isToolParameterValue(value: unknown, depth: number = 0): value is ToolParameterValue {\n // Prevent stack overflow with depth limit\n const MAX_DEPTH = 50;\n if (depth > MAX_DEPTH) {\n this.logger.warn('Maximum nesting depth exceeded for parameter validation', { depth });\n return false;\n }\n\n if (value === null) return true;\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return true;\n }\n if (Array.isArray(value)) {\n return value.every((item) => this.isToolParameterValue(item, depth + 1));\n }\n if (typeof value === 'object') {\n return Object.values(value as Record<string, unknown>).every((v) =>\n this.isToolParameterValue(v, depth + 1)\n );\n }\n return false;\n }\n\n private validateParameterType(value: ToolParameterValue, paramDef: ToolParameter): boolean {\n switch (paramDef.type) {\n case 'string':\n return typeof value === 'string';\n case 'number':\n return typeof value === 'number';\n case 'boolean':\n return typeof value === 'boolean';\n case 'object':\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n case 'array':\n return Array.isArray(value);\n default:\n return false;\n }\n }\n\n public convertParametersToJsonSchema(\n parameters: Record<string, ToolParameter>,\n depth: number = 0\n ): LLMToolSchema {\n // Prevent stack overflow with depth limit\n const MAX_DEPTH = 50;\n if (depth > MAX_DEPTH) {\n this.logger.warn('Maximum nesting depth exceeded for JSON schema conversion', { depth });\n return { type: 'object', properties: {}, required: [] };\n }\n\n const properties: Record<string, LLMToolProperty> = {};\n\n for (const [name, param] of Object.entries(parameters)) {\n properties[name] = {\n type: param.type as 'string' | 'number' | 'boolean' | 'object' | 'array',\n description: param.description,\n };\n\n if (param.enum) {\n properties[name].enum = param.enum;\n }\n\n if (param.properties) {\n properties[name].properties = this.convertParametersToJsonSchema(\n param.properties,\n depth + 1\n ).properties;\n }\n\n if (param.items) {\n properties[name].items = {\n type: param.items.type,\n };\n }\n }\n\n return {\n type: 'object',\n properties,\n required: Object.entries(parameters)\n .filter(([, param]) => param.required)\n .map(([name]) => name),\n };\n }\n}\n\n// Agent-based plugin instances\nconst pluginInstances = new Map<string, Plugin>();\n\nexport function getPlugin(agent?: IAgent): Plugin {\n if (!agent) {\n throw new Error('Agent required for plugin initialization');\n }\n const agentId = agent.id;\n if (!pluginInstances.has(agentId)) {\n pluginInstances.set(agentId, new Plugin(agent));\n }\n return pluginInstances.get(agentId)!;\n}\n\n/**\n * Cleanup plugin instance for a specific agent to prevent memory leaks.\n * Should be called when an agent is destroyed or no longer needed.\n * @param agentId - The agent ID to cleanup plugin for.\n */\nexport async function cleanupPluginForAgent(agentId: string): Promise<void> {\n const plugin = pluginInstances.get(agentId);\n if (plugin) {\n // Unregister all plugins to trigger their cleanup methods\n const plugins = plugin.listPlugins();\n for (const p of plugins) {\n await plugin.unregisterPlugin(p.name);\n }\n pluginInstances.delete(agentId);\n }\n}\n\n/**\n * Cleanup plugin instance for a specific agent to prevent memory leaks.\n * Should be called when the plugin manager is no longer needed.\n * @param agentId - The agent ID to cleanup plugin for. If not provided, cleans up all instances.\n */\nexport async function cleanupPlugin(agentId?: string): Promise<void> {\n if (agentId) {\n await cleanupPluginForAgent(agentId);\n } else {\n // Cleanup all instances\n const agentIds = Array.from(pluginInstances.keys());\n for (const id of agentIds) {\n await cleanupPluginForAgent(id);\n }\n }\n}\n\n/**\n * Reset plugin instances (mainly for testing purposes).\n * Performs cleanup before clearing instances to prevent memory leaks.\n * @param agentId - The agent ID to reset plugin for. If not provided, resets all instances.\n */\nexport async function resetPlugin(agentId?: string): Promise<void> {\n if (agentId) {\n // Cleanup before deleting\n const plugin = pluginInstances.get(agentId);\n if (plugin) {\n const plugins = plugin.listPlugins();\n for (const p of plugins) {\n try {\n await plugin.unregisterPlugin(p.name);\n } catch {\n // Ignore cleanup errors during reset\n }\n }\n }\n pluginInstances.delete(agentId);\n } else {\n // Cleanup all instances before clearing\n for (const [, plugin] of pluginInstances) {\n const plugins = plugin.listPlugins();\n for (const p of plugins) {\n try {\n await plugin.unregisterPlugin(p.name);\n } catch {\n // Ignore cleanup errors during reset\n }\n }\n }\n pluginInstances.clear();\n }\n}\n\nexport * from './types';\n\n// Export utility function for converting tool parameters to JSON schema\nexport function convertToolParametersToJsonSchema(\n parameters: Record<string, ToolParameter>,\n depth: number = 0\n): LLMToolSchema {\n // Prevent stack overflow with depth limit\n const MAX_DEPTH = 50;\n if (depth > MAX_DEPTH) {\n return { type: 'object', properties: {}, required: [] };\n }\n\n const properties: Record<string, LLMToolProperty> = {};\n\n for (const [name, param] of Object.entries(parameters)) {\n properties[name] = {\n type: param.type as 'string' | 'number' | 'boolean' | 'object' | 'array',\n description: param.description,\n };\n\n if (param.enum) {\n properties[name].enum = param.enum;\n }\n\n if (param.properties) {\n properties[name].properties = convertToolParametersToJsonSchema(\n param.properties,\n depth + 1\n ).properties;\n }\n\n if (param.items) {\n properties[name].items = {\n type: param.items.type,\n };\n }\n }\n\n return {\n type: 'object',\n properties,\n required: Object.entries(parameters)\n .filter(([, param]) => param.required)\n .map(([name]) => name),\n };\n}\n","import crypto from 'crypto';\nimport { IAgentModule, IAgent } from '../agent/types';\nimport { SubAgentRunOptions } from '../sub-agent/types';\nimport {\n Task as TaskType,\n TaskSearchOptions,\n TaskStatus,\n TaskRequest,\n TaskResponse,\n} from './types';\nimport { getDatabase } from '../database';\nimport {\n getLLM,\n LLMResponse,\n LLMMessage,\n LLMMessageContent,\n LLMMessageContentPart,\n Tool,\n ToolCall,\n} from '../llm';\nimport { LLMUsage } from '../llm/types';\nimport { Memory } from '../memory';\nimport { Memory as MemoryType } from '../memory/types';\nimport { Knex } from 'knex';\nimport { Logger } from '../logger/types';\nimport { encryptSensitiveFields, decryptSensitiveFields } from '../database/utils';\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { DEFAULT_AGENT_CONFIG } from '../agent/defaults';\nimport { DEFAULT_TASK_CONFIG } from './defaults';\nimport { convertToolParametersToJsonSchema } from '../plugin';\nimport { MetadataObject } from '../types';\n\n/**\n * Simple async mutex for protecting initialization.\n * Replaces spin-wait anti-pattern with proper promise-based waiting.\n */\nclass AsyncMutex {\n private locked = false;\n private queue: Array<() => void> = [];\n\n async acquire(): Promise<void> {\n if (!this.locked) {\n this.locked = true;\n return;\n }\n return new Promise<void>((resolve) => {\n this.queue.push(resolve);\n });\n }\n\n release(): void {\n const next = this.queue.shift();\n if (next) {\n next();\n } else {\n this.locked = false;\n }\n }\n}\n\n// Database row interfaces\ninterface TaskDbRow {\n id: string; // UUID\n agentId: string; // UUID\n prompt: string;\n response: string | null;\n status: TaskStatus;\n metadata: string | null;\n created_at: string;\n updated_at: string;\n completedAt: string | null;\n}\n\nexport class Task implements IAgentModule {\n readonly name = 'task';\n private knex: Knex | null = null;\n private logger: Logger;\n private static initializingDatabase: Promise<void> | null = null;\n private static readonly MAX_TOOL_CALLS = DEFAULT_TASK_CONFIG.maxToolCalls;\n // AsyncMutex pattern for race condition prevention (replaces spin-wait)\n private static initMutex = new AsyncMutex();\n\n constructor(private agent: IAgent) {\n this.logger = agent.logger;\n }\n\n async initialize(): Promise<void> {\n await this.ensureDatabase();\n }\n\n private async ensureDatabase(): Promise<void> {\n if (this.knex) {\n return;\n }\n\n // Check if another instance is already initializing\n if (Task.initializingDatabase) {\n await Task.initializingDatabase;\n // After waiting, get the knex instance from the initialized database\n const db = await getDatabase();\n this.knex = db.getKnex();\n return;\n }\n\n // Use AsyncMutex instead of spin-wait for proper synchronization\n await Task.initMutex.acquire();\n let mutexReleased = false;\n try {\n // Double-check after acquiring lock\n if (this.knex) return;\n\n // Check again if initialization started while waiting for lock\n if (Task.initializingDatabase) {\n // Release mutex early since we'll wait on the promise\n Task.initMutex.release();\n mutexReleased = true;\n await Task.initializingDatabase;\n const db = await getDatabase();\n this.knex = db.getKnex();\n return;\n }\n\n // Start initialization\n Task.initializingDatabase = (async () => {\n const db = await getDatabase();\n this.knex = db.getKnex();\n })();\n\n try {\n await Task.initializingDatabase;\n } finally {\n Task.initializingDatabase = null;\n }\n } finally {\n // Only release if not already released\n if (!mutexReleased) {\n Task.initMutex.release();\n }\n }\n }\n\n private getKnex(): Knex {\n if (!this.knex) {\n throw new Error('Database not initialized. Call ensureDatabase() first.');\n }\n return this.knex;\n }\n\n async createTask(request: TaskRequest): Promise<TaskType> {\n // User-facing info log\n this.logger.info('Creating new task');\n\n this.logger.debug('Creating task', {\n promptLength: request.prompt.length,\n promptPreview: request.prompt.slice(0, 100) + '...',\n agentId: this.agent.id,\n hasAttachments: !!request.attachments?.length,\n hasMcpServers: !!request.mcpServers?.length,\n hasPlugins: !!request.plugins?.length,\n useTools: !!request.useTools,\n });\n\n await this.ensureDatabase();\n const tableName = 'tasks';\n\n const metadata = request.metadata || {};\n if (request.useTools !== undefined) {\n metadata.useTools = request.useTools;\n }\n if (request.attachments) {\n metadata.attachments = JSON.stringify(request.attachments);\n }\n if (request.mcpServer