UNPKG

visaire

Version:

A fully autonomous agentic CLI tool for interacting with large language models (Claude, Gemini, GPT) that can execute multi-step tasks automatically

454 lines (396 loc) 13.4 kB
const { v4: uuidv4 } = require('uuid'); const EventEmitter = require('events'); const Joi = require('joi'); const Logger = require('./Logger'); const ToolRegistry = require('./ToolRegistry'); const ConversationManager = require('./ConversationManager'); const ReasoningEngine = require('./ReasoningEngine'); const ContextManager = require('./ContextManager'); /** * Core Agent class implementing Forge-style sophisticated reasoning * This is the main orchestrator that coordinates all agent activities */ class Agent extends EventEmitter { constructor(options = {}) { super(); // Validate options const schema = Joi.object({ id: Joi.string().optional(), name: Joi.string().default('visaire-agent'), model: Joi.string().default('claude-3-sonnet-20240229'), provider: Joi.string().valid('claude', 'gpt', 'gemini').default('claude'), apiKey: Joi.string().optional().allow(null), temperature: Joi.number().min(0).max(2).default(0.7), maxTokens: Joi.number().min(1).max(100000).default(4000), reasoning: Joi.object({ effort: Joi.string().valid('low', 'medium', 'high', 'maximum').default('medium'), maxIterations: Joi.number().min(1).max(50).default(10), enableReflection: Joi.boolean().default(true), enablePlanning: Joi.boolean().default(true) }).default(), tools: Joi.object({ enabled: Joi.boolean().default(true), autoApprove: Joi.boolean().default(true), // Default to true for immediate tool execution confirmationRequired: Joi.boolean().default(true), maxConcurrent: Joi.number().min(1).max(10).default(3) }).default(), context: Joi.object({ maxSize: Joi.number().min(1000).max(1000000).default(100000), compressionThreshold: Joi.number().min(0.1).max(1).default(0.8), retentionStrategy: Joi.string().valid('fifo', 'importance', 'recency').default('importance') }).default(), security: Joi.object({ allowedPaths: Joi.array().items(Joi.string()).default(['./']), allowedCommands: Joi.array().items(Joi.string()).optional(), blockedCommands: Joi.array().items(Joi.string()).default(['rm -rf', 'sudo', 'format']), maxFileSize: Joi.number().min(1024).default(10485760), // 10MB maxExecutionTime: Joi.number().min(1000).optional(), maxOutputSize: Joi.number().min(1024).optional(), sandboxMode: Joi.boolean().default(false) }).default(), logging: Joi.object({ level: Joi.string().valid('debug', 'info', 'warn', 'error').default('info'), enableMetrics: Joi.boolean().default(true), enableTracing: Joi.boolean().default(false) }).default() }).unknown(true); // Allow unknown properties for flexibility const { error, value } = schema.validate(options); if (error) { throw new Error(`Invalid agent configuration: ${error.details[0].message}`); } // Initialize core properties this.id = value.id || uuidv4(); this.name = value.name; this.config = value; this.state = 'idle'; // idle, thinking, acting, error this.startTime = new Date(); this.metrics = { conversations: 0, toolCalls: 0, errors: 0, totalThinkingTime: 0, averageResponseTime: 0 }; // Initialize core components this.logger = new Logger({ agentId: this.id, level: value.logging.level, enableMetrics: value.logging.enableMetrics, enableTracing: value.logging.enableTracing }); this.toolRegistry = new ToolRegistry({ logger: this.logger, security: value.security, maxConcurrent: value.tools.maxConcurrent }); this.conversationManager = new ConversationManager({ logger: this.logger, agentId: this.id }); this.reasoningEngine = new ReasoningEngine({ logger: this.logger, effort: value.reasoning.effort, maxIterations: value.reasoning.maxIterations, enableReflection: value.reasoning.enableReflection, enablePlanning: value.reasoning.enablePlanning }); this.contextManager = new ContextManager({ logger: this.logger, maxSize: value.context.maxSize, compressionThreshold: value.context.compressionThreshold, retentionStrategy: value.context.retentionStrategy }); // Bind event handlers this.setupEventHandlers(); this.logger.info('Agent initialized', { id: this.id, name: this.name, config: this.config }); } /** * Setup event handlers for component coordination */ setupEventHandlers() { // Tool execution events this.toolRegistry.on('tool:start', (data) => { this.emit('tool:start', data); this.logger.debug('Tool execution started', data); }); this.toolRegistry.on('tool:complete', (data) => { this.metrics.toolCalls++; this.emit('tool:complete', data); this.logger.debug('Tool execution completed', data); }); this.toolRegistry.on('tool:error', (data) => { this.metrics.errors++; this.emit('tool:error', data); this.logger.error('Tool execution failed', data); }); // Reasoning events this.reasoningEngine.on('reasoning:start', (data) => { this.state = 'thinking'; this.emit('reasoning:start', data); }); this.reasoningEngine.on('reasoning:complete', (data) => { this.metrics.totalThinkingTime += data.duration || 0; this.emit('reasoning:complete', data); }); // Conversation events this.conversationManager.on('conversation:start', (data) => { this.metrics.conversations++; this.emit('conversation:start', data); }); this.conversationManager.on('conversation:end', (data) => { this.state = 'idle'; this.emit('conversation:end', data); }); } /** * Main entry point for processing user input * This implements the core agent reasoning loop */ async processInput(input, options = {}) { const startTime = Date.now(); const conversationId = uuidv4(); try { this.logger.info('Processing input', { conversationId, inputLength: input.length, options }); // Start conversation const conversation = await this.conversationManager.startConversation({ id: conversationId, input, options, agentId: this.id }); // Build context for reasoning const context = await this.contextManager.buildContext({ input, conversation, options }); // Engage reasoning engine const reasoning = await this.reasoningEngine.process({ input, context, conversation, config: this.config }); // Execute any planned actions let executionResults = null; const actionsToExecute = options.overrideActions || reasoning.actions; if (actionsToExecute && actionsToExecute.length > 0) { this.state = 'acting'; executionResults = await this.executeActions(actionsToExecute, { conversationId, autoApprove: options.autoApprove || this.config.tools.autoApprove }); } // Update conversation with results await this.conversationManager.updateConversation(conversationId, { reasoning, executionResults, metrics: { processingTime: Date.now() - startTime, toolCalls: executionResults?.results?.length || 0 } }); // Update context await this.contextManager.updateContext({ conversationId, reasoning, executionResults }); // Calculate metrics const processingTime = Date.now() - startTime; this.updateMetrics(processingTime); const result = { success: true, conversationId, reasoning, executionResults, metrics: { processingTime, toolCalls: executionResults?.results?.length || 0, tokensUsed: reasoning.tokensUsed || 0 } }; this.logger.info('Input processing completed', result.metrics); this.emit('processing:complete', result); return result; } catch (error) { this.metrics.errors++; this.state = 'error'; const errorResult = { success: false, conversationId, error: error.message, metrics: { processingTime: Date.now() - startTime } }; this.logger.error('Input processing failed', { error: error.message, stack: error.stack, conversationId }); this.emit('processing:error', errorResult); return errorResult; } } /** * Execute planned actions using the tool registry */ async executeActions(actions, options = {}) { try { this.logger.info('Executing actions', { count: actions.length, conversationId: options.conversationId }); const results = []; const errors = []; for (const action of actions) { try { // Validate action const validation = await this.toolRegistry.validateAction(action); if (!validation.valid) { errors.push({ action, error: `Validation failed: ${validation.errors.join(', ')}` }); continue; } // Request confirmation if needed if (this.config.tools.confirmationRequired && !options.autoApprove) { const approved = await this.requestActionConfirmation(action); if (!approved) { results.push({ action, status: 'skipped', reason: 'User declined' }); continue; } } // Execute action const result = await this.toolRegistry.executeAction(action); results.push({ action, status: result.success ? 'completed' : 'failed', result, duration: result.duration }); } catch (error) { errors.push({ action, error: error.message }); } } return { success: errors.length === 0, results, errors, summary: { total: actions.length, completed: results.filter(r => r.status === 'completed').length, failed: results.filter(r => r.status === 'failed').length, skipped: results.filter(r => r.status === 'skipped').length, errors: errors.length } }; } catch (error) { this.logger.error('Action execution failed', { error: error.message }); throw error; } } /** * Request user confirmation for action execution */ async requestActionConfirmation(action) { return new Promise((resolve) => { this.emit('confirmation:required', { action, callback: resolve }); }); } /** * Update agent metrics */ updateMetrics(processingTime) { const totalProcessingTime = this.metrics.averageResponseTime * this.metrics.conversations + processingTime; this.metrics.averageResponseTime = totalProcessingTime / (this.metrics.conversations + 1); } /** * Get current agent status */ getStatus() { return { id: this.id, name: this.name, state: this.state, uptime: Date.now() - this.startTime.getTime(), metrics: { ...this.metrics }, config: this.config, components: { toolRegistry: this.toolRegistry.getStatus(), conversationManager: this.conversationManager.getStatus(), reasoningEngine: this.reasoningEngine.getStatus(), contextManager: this.contextManager.getStatus() } }; } /** * Update agent configuration */ async updateConfig(newConfig) { const schema = Joi.object({ temperature: Joi.number().min(0).max(2), maxTokens: Joi.number().min(1).max(100000), reasoning: Joi.object(), tools: Joi.object(), context: Joi.object(), security: Joi.object(), logging: Joi.object() }); const { error, value } = schema.validate(newConfig); if (error) { throw new Error(`Invalid configuration update: ${error.details[0].message}`); } // Update configuration Object.assign(this.config, value); // Update components if (value.reasoning) { await this.reasoningEngine.updateConfig(value.reasoning); } if (value.tools) { await this.toolRegistry.updateConfig(value.tools); } if (value.context) { await this.contextManager.updateConfig(value.context); } this.logger.info('Configuration updated', { newConfig: value }); this.emit('config:updated', value); } /** * Gracefully shutdown the agent */ async shutdown() { this.logger.info('Shutting down agent', { id: this.id }); this.state = 'shutting_down'; // Close all conversations await this.conversationManager.closeAll(); // Stop all tool executions await this.toolRegistry.stopAll(); // Cleanup context await this.contextManager.cleanup(); this.state = 'shutdown'; this.emit('shutdown'); this.logger.info('Agent shutdown complete'); } } module.exports = Agent;