import * as plugins from './plugins.js'; import * as interfaces from './smartagent.interfaces.js'; import { BaseToolWrapper } from './smartagent.tools.base.js'; import { DriverAgent } from './smartagent.classes.driveragent.js'; import { GuardianAgent } from './smartagent.classes.guardianagent.js'; import { FilesystemTool } from './smartagent.tools.filesystem.js'; import { HttpTool } from './smartagent.tools.http.js'; import { ShellTool } from './smartagent.tools.shell.js'; import { BrowserTool } from './smartagent.tools.browser.js'; import { DenoTool } from './smartagent.tools.deno.js'; /** * DualAgentOrchestrator - Coordinates Driver and Guardian agents * Manages the complete lifecycle of task execution with tool approval */ export class DualAgentOrchestrator { private options: interfaces.IDualAgentOptions; private smartai: plugins.smartai.SmartAi; private driverProvider: plugins.smartai.MultiModalModel; private guardianProvider: plugins.smartai.MultiModalModel; private driver: DriverAgent; private guardian: GuardianAgent; private tools: Map = new Map(); private isRunning = false; private conversationHistory: interfaces.IAgentMessage[] = []; private ownsSmartAi = true; // true if we created the SmartAi instance, false if it was provided constructor(options: interfaces.IDualAgentOptions) { this.options = { maxIterations: 20, maxConsecutiveRejections: 3, defaultProvider: 'openai', maxResultChars: 15000, maxHistoryMessages: 20, ...options, }; // Use existing SmartAi instance if provided, otherwise create a new one if (options.smartAiInstance) { this.smartai = options.smartAiInstance; this.ownsSmartAi = false; // Don't manage lifecycle of provided instance } else { this.smartai = new plugins.smartai.SmartAi(options); this.ownsSmartAi = true; } // Note: Don't access providers here - they don't exist until start() is called } /** * Get provider by name */ private getProviderByName(providerName: plugins.smartai.TProvider): plugins.smartai.MultiModalModel { switch (providerName) { case 'openai': return this.smartai.openaiProvider; case 'anthropic': return this.smartai.anthropicProvider; case 'perplexity': return this.smartai.perplexityProvider; case 'ollama': return this.smartai.ollamaProvider; case 'groq': return this.smartai.groqProvider; case 'xai': return this.smartai.xaiProvider; case 'exo': return this.smartai.exoProvider; default: return this.smartai.openaiProvider; } } /** * Emit a progress event if callback is configured */ private emitProgress(event: Omit): void { if (this.options.onProgress) { const prefix = this.options.logPrefix ? `${this.options.logPrefix} ` : ''; const { logLevel, logMessage } = this.formatProgressEvent(event, prefix); this.options.onProgress({ ...event, timestamp: new Date(), logLevel, logMessage, }); } } /** * Format a progress event into a log level and message */ private formatProgressEvent( event: Omit, prefix: string ): { logLevel: interfaces.TLogLevel; logMessage: string } { switch (event.type) { case 'task_started': return { logLevel: 'info', logMessage: `${prefix}Task started` }; case 'iteration_started': return { logLevel: 'info', logMessage: `${prefix}Iteration ${event.iteration}/${event.maxIterations}` }; case 'tool_proposed': return { logLevel: 'info', logMessage: `${prefix} → Proposing: ${event.toolName}.${event.action}` }; case 'guardian_evaluating': return { logLevel: 'info', logMessage: `${prefix} ⏳ Guardian evaluating...` }; case 'tool_approved': return { logLevel: 'info', logMessage: `${prefix} ✓ Approved: ${event.toolName}.${event.action}` }; case 'tool_rejected': return { logLevel: 'warn', logMessage: `${prefix} ✗ Rejected: ${event.toolName}.${event.action} - ${event.reason}` }; case 'tool_executing': return { logLevel: 'info', logMessage: `${prefix} ⚡ Executing: ${event.toolName}.${event.action}...` }; case 'tool_completed': return { logLevel: 'info', logMessage: `${prefix} ✓ Completed: ${event.message}` }; case 'task_completed': return { logLevel: 'success', logMessage: `${prefix}Task completed in ${event.iteration} iterations` }; case 'clarification_needed': return { logLevel: 'warn', logMessage: `${prefix}Clarification needed from user` }; case 'max_iterations': return { logLevel: 'error', logMessage: `${prefix}${event.message}` }; case 'max_rejections': return { logLevel: 'error', logMessage: `${prefix}${event.message}` }; default: return { logLevel: 'info', logMessage: `${prefix}${event.type}` }; } } /** * Register a custom tool */ public registerTool(tool: BaseToolWrapper): void { this.tools.set(tool.name, tool); // Register with agents if they exist (they're created in start()) if (this.driver) { this.driver.registerTool(tool); } if (this.guardian) { this.guardian.registerTool(tool); } } /** * Register all standard tools */ public registerStandardTools(): void { const standardTools = [ new FilesystemTool(), new HttpTool(), new ShellTool(), new BrowserTool(), new DenoTool(), ]; for (const tool of standardTools) { this.registerTool(tool); } } /** * Register a scoped filesystem tool that can only access files within the specified directory * @param basePath The directory to scope filesystem operations to */ public registerScopedFilesystemTool(basePath: string): void { const scopedTool = new FilesystemTool({ basePath }); this.registerTool(scopedTool); } /** * Initialize all tools (eager loading) */ public async start(): Promise { // Start smartai only if we created it (external instances should already be started) if (this.ownsSmartAi) { await this.smartai.start(); } // NOW get providers (after they've been initialized by smartai.start()) this.driverProvider = this.getProviderByName(this.options.defaultProvider!); this.guardianProvider = this.options.guardianProvider ? this.getProviderByName(this.options.guardianProvider) : this.driverProvider; // NOW create agents with initialized providers this.driver = new DriverAgent(this.driverProvider, { systemMessage: this.options.driverSystemMessage, maxHistoryMessages: this.options.maxHistoryMessages, }); this.guardian = new GuardianAgent(this.guardianProvider, this.options.guardianPolicyPrompt); // Register any tools that were added before start() with the agents for (const tool of this.tools.values()) { this.driver.registerTool(tool); this.guardian.registerTool(tool); } // Initialize all tools const initPromises: Promise[] = []; for (const tool of this.tools.values()) { initPromises.push(tool.initialize()); } await Promise.all(initPromises); this.isRunning = true; } /** * Cleanup all tools */ public async stop(): Promise { const cleanupPromises: Promise[] = []; for (const tool of this.tools.values()) { cleanupPromises.push(tool.cleanup()); } await Promise.all(cleanupPromises); // Only stop smartai if we created it (don't stop external instances) if (this.ownsSmartAi) { await this.smartai.stop(); } this.isRunning = false; if (this.driver) { this.driver.reset(); } } /** * Run a task through the dual-agent system */ public async run(task: string): Promise { if (!this.isRunning) { throw new Error('Orchestrator not started. Call start() first.'); } this.conversationHistory = []; let iterations = 0; let consecutiveRejections = 0; let completed = false; let finalResult: string | null = null; // Add initial task to history this.conversationHistory.push({ role: 'user', content: task, }); // Start the driver with the task let driverResponse = await this.driver.startTask(task); this.conversationHistory.push(driverResponse); // Emit task started event this.emitProgress({ type: 'task_started', message: task.length > 100 ? task.substring(0, 100) + '...' : task, }); while ( iterations < this.options.maxIterations! && consecutiveRejections < this.options.maxConsecutiveRejections! && !completed ) { iterations++; // Emit iteration started event this.emitProgress({ type: 'iteration_started', iteration: iterations, maxIterations: this.options.maxIterations, }); // Check if task is complete if (this.driver.isTaskComplete(driverResponse.content)) { completed = true; finalResult = this.driver.extractTaskResult(driverResponse.content) || driverResponse.content; // Emit task completed event this.emitProgress({ type: 'task_completed', iteration: iterations, message: 'Task completed successfully', }); break; } // Check if driver needs clarification if (this.driver.needsClarification(driverResponse.content)) { // Emit clarification needed event this.emitProgress({ type: 'clarification_needed', iteration: iterations, message: 'Driver needs clarification from user', }); // Return with clarification needed status return { success: false, completed: false, result: driverResponse.content, iterations, history: this.conversationHistory, status: 'clarification_needed', }; } // Parse tool call proposals const proposals = this.driver.parseToolCallProposals(driverResponse.content); if (proposals.length === 0) { // No tool calls, continue the conversation driverResponse = await this.driver.continueWithMessage( 'Please either use a tool to make progress on the task, or indicate that the task is complete with summary.' ); this.conversationHistory.push(driverResponse); continue; } // Process the first proposal (one at a time) const proposal = proposals[0]; // Emit tool proposed event this.emitProgress({ type: 'tool_proposed', iteration: iterations, toolName: proposal.toolName, action: proposal.action, message: `${proposal.toolName}.${proposal.action}`, }); // Quick validation first const quickDecision = this.guardian.quickValidate(proposal); let decision: interfaces.IGuardianDecision; if (quickDecision) { decision = quickDecision; } else { // Emit guardian evaluating event this.emitProgress({ type: 'guardian_evaluating', iteration: iterations, toolName: proposal.toolName, action: proposal.action, }); // Full AI evaluation decision = await this.guardian.evaluate(proposal, task); } if (decision.decision === 'approve') { consecutiveRejections = 0; // Emit tool approved event this.emitProgress({ type: 'tool_approved', iteration: iterations, toolName: proposal.toolName, action: proposal.action, }); // Execute the tool const tool = this.tools.get(proposal.toolName); if (!tool) { const errorMessage = `Tool "${proposal.toolName}" not found.`; driverResponse = await this.driver.continueWithMessage( `TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.` ); this.conversationHistory.push(driverResponse); continue; } try { // Emit tool executing event this.emitProgress({ type: 'tool_executing', iteration: iterations, toolName: proposal.toolName, action: proposal.action, }); const result = await tool.execute(proposal.action, proposal.params); // Emit tool completed event this.emitProgress({ type: 'tool_completed', iteration: iterations, toolName: proposal.toolName, action: proposal.action, message: result.success ? 'success' : result.error, }); // Build result message (prefer summary if provided, otherwise stringify result) let resultMessage: string; if (result.success) { if (result.summary) { // Use tool-provided summary resultMessage = `TOOL RESULT (${proposal.toolName}.${proposal.action}):\n${result.summary}`; } else { // Stringify and potentially truncate const resultStr = JSON.stringify(result.result, null, 2); const maxChars = this.options.maxResultChars ?? 15000; if (maxChars > 0 && resultStr.length > maxChars) { // Truncate the result const truncated = resultStr.substring(0, maxChars); const omittedTokens = Math.round((resultStr.length - maxChars) / 4); resultMessage = `TOOL RESULT (${proposal.toolName}.${proposal.action}):\n${truncated}\n\n[... output truncated, ~${omittedTokens} tokens omitted. Use more specific parameters to reduce output size.]`; } else { resultMessage = `TOOL RESULT (${proposal.toolName}.${proposal.action}):\n${resultStr}`; } } } else { resultMessage = `TOOL ERROR (${proposal.toolName}.${proposal.action}):\n${result.error}`; } this.conversationHistory.push({ role: 'system', content: resultMessage, toolCall: proposal, toolResult: result, }); driverResponse = await this.driver.continueWithMessage(resultMessage); this.conversationHistory.push(driverResponse); } catch (error) { const errorMessage = `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`; driverResponse = await this.driver.continueWithMessage( `TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.` ); this.conversationHistory.push(driverResponse); } } else { // Rejected consecutiveRejections++; // Emit tool rejected event this.emitProgress({ type: 'tool_rejected', iteration: iterations, toolName: proposal.toolName, action: proposal.action, reason: decision.reason, }); // Build rejection feedback let feedback = `TOOL CALL REJECTED by Guardian:\n`; feedback += `- Reason: ${decision.reason}\n`; if (decision.concerns && decision.concerns.length > 0) { feedback += `- Concerns:\n${decision.concerns.map(c => ` - ${c}`).join('\n')}\n`; } if (decision.suggestions) { feedback += `- Suggestions: ${decision.suggestions}\n`; } feedback += `\nPlease adapt your approach based on this feedback.`; this.conversationHistory.push({ role: 'system', content: feedback, toolCall: proposal, guardianDecision: decision, }); driverResponse = await this.driver.continueWithMessage(feedback); this.conversationHistory.push(driverResponse); } } // Determine final status let status: interfaces.TDualAgentRunStatus = 'completed'; if (!completed) { if (iterations >= this.options.maxIterations!) { status = 'max_iterations_reached'; // Emit max iterations event this.emitProgress({ type: 'max_iterations', iteration: iterations, maxIterations: this.options.maxIterations, message: `Maximum iterations (${this.options.maxIterations}) reached`, }); } else if (consecutiveRejections >= this.options.maxConsecutiveRejections!) { status = 'max_rejections_reached'; // Emit max rejections event this.emitProgress({ type: 'max_rejections', iteration: iterations, message: `Maximum consecutive rejections (${this.options.maxConsecutiveRejections}) reached`, }); } } return { success: completed, completed, result: finalResult || driverResponse.content, iterations, history: this.conversationHistory, status, }; } /** * Continue an existing task with user input */ public async continueTask(userInput: string): Promise { if (!this.isRunning) { throw new Error('Orchestrator not started. Call start() first.'); } this.conversationHistory.push({ role: 'user', content: userInput, }); const driverResponse = await this.driver.continueWithMessage(userInput); this.conversationHistory.push(driverResponse); // Continue the run loop // For simplicity, we return the current state - full continuation would need refactoring return { success: false, completed: false, result: driverResponse.content, iterations: 1, history: this.conversationHistory, status: 'in_progress', }; } /** * Get the conversation history */ public getHistory(): interfaces.IAgentMessage[] { return [...this.conversationHistory]; } /** * Update the guardian policy */ public setGuardianPolicy(policyPrompt: string): void { this.guardian.setPolicy(policyPrompt); } /** * Check if orchestrator is running */ public isActive(): boolean { return this.isRunning; } /** * Get registered tool names */ public getToolNames(): string[] { return Array.from(this.tools.keys()); } }