UNPKG

mcp-subagents

Version:

Multi-Agent AI Orchestration via Model Context Protocol - Access specialized CLI AI agents (Aider, Qwen, Gemini, Goose, etc.) with intelligent fallback and configuration

1,028 lines 46.1 kB
import { AgentExecutionError, MCPValidationError } from './errors.js'; import { ConfigManager } from './config.js'; import { ManagedProcess, ProcessPool } from './utils/process-manager.js'; import { ChunkedOutput } from './utils/chunked-output.js'; import { readFileSync, unlinkSync, existsSync } from 'fs'; import { z } from 'zod'; export class AgentManager { registry; tasks = new Map(); workflows = new Map(); runningCount = new Map(); configManager; processPool; runningAsyncOperations = new Set(); isShuttingDown = false; constructor(registry) { this.registry = registry; this.configManager = new ConfigManager(); this.processPool = new ProcessPool(); // Recursion prevention removed - fork bomb fixed at detection level // Cleanup on exit process.on('SIGINT', () => this.shutdown()); process.on('SIGTERM', () => this.shutdown()); // Global error handling process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); this.handleError(reason); }); process.on('uncaughtException', (error) => { console.error('Uncaught Exception thrown:', error); this.handleError(error); }); } handleError(error) { // In MCP mode, errors should not go to stderr // Store errors internally or send via proper MCP channels if (process.env['MCP_MODE']) { // Silently handle errors in MCP mode return; } // Log the error with additional context console.error('Error occurred:', error); // Additional error handling logic can be added here } logError(message, error) { // In MCP mode, avoid stderr output if (process.env['MCP_MODE']) { return; } console.error(`${message}:`, error); } async runAgent(params) { // Validate parameters const validatedParams = this.validateRunAgentParams(params); // Parse task parameter (handles file:// paths for long tasks) const parsedTask = this.parseTaskParam(validatedParams.task); // Create task with merged configuration const taskId = this.generateTaskId(); const baseConfig = validatedParams.config || { timeout: 300000, retries: 1, allowFallback: true }; const mergedConfig = this.mergeAgentConfig(validatedParams.agent, baseConfig); const task = { id: taskId, agent: validatedParams.agent, task: parsedTask, config: mergedConfig, status: 'queued', output: new ChunkedOutput(), taskPreview: parsedTask.substring(0, 50) + (parsedTask.length > 50 ? '...' : ''), lastActivityTime: Date.now() }; this.tasks.set(taskId, task); // Try to execute const attempts = []; let executedBy; let success = false; try { // Try primary agent const agent = this.registry.getAgent(validatedParams.agent); // Check concurrent task limit const agentConfig = this.configManager.getAgentConfig(validatedParams.agent); const currentTasks = this.runningCount.get(validatedParams.agent) || 0; const maxConcurrent = agentConfig.maxConcurrent || 1; if (currentTasks >= maxConcurrent) { throw new AgentExecutionError(`Agent ${validatedParams.agent} has reached max concurrent tasks (${maxConcurrent})`, validatedParams.agent); } await this.executeTask(task, agent); executedBy = validatedParams.agent; success = true; attempts.push({ agent: validatedParams.agent, status: 'success', startTime: task.startTime, endTime: task.endTime }); } catch (error) { this.logError(`Error executing task with agent ${validatedParams.agent}`, error); attempts.push({ agent: validatedParams.agent, status: 'failed', error: error instanceof Error ? error.message : String(error) }); // Try fallback if enabled (task config overrides global config) const taskFallbackSetting = validatedParams.config?.allowFallback; const globalFallbackEnabled = this.configManager.isFallbackEnabled(); const shouldUseFallback = taskFallbackSetting !== undefined ? taskFallbackSetting : globalFallbackEnabled; console.error(`🔧 Debug: taskFallbackSetting=${taskFallbackSetting}, globalFallbackEnabled=${globalFallbackEnabled}, shouldUseFallback=${shouldUseFallback}`); if (shouldUseFallback) { const fallbackAgents = validatedParams.config?.fallbackAgents || this.getAvailableFallbackAgents(validatedParams.agent); for (const fallbackAgent of fallbackAgents) { try { // Create clean config for fallback agent (excludes original agent's flags) const fallbackConfig = this.createFallbackConfig(fallbackAgent, baseConfig); const fallbackTask = { ...task, config: fallbackConfig }; const agent = this.registry.getAgent(fallbackAgent); await this.executeTask(fallbackTask, agent); executedBy = fallbackAgent; success = true; attempts.push({ agent: fallbackAgent, status: 'success', startTime: fallbackTask.startTime, endTime: fallbackTask.endTime }); break; } catch (fallbackError) { this.logError(`Error executing fallback task with agent ${fallbackAgent}`, fallbackError); attempts.push({ agent: fallbackAgent, status: 'failed', error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError) }); } } } } // For run_agent, return minimal output by default (agent-friendly) // Full output can be retrieved via get_task_status const outputResult = task.output.getLines({ limit: 10, fromEnd: true }); const minimalOutput = outputResult.lines; if (outputResult.hasMore) { minimalOutput.unshift(`... [${outputResult.totalLines - 10} more lines - use get_task_status for full output] ...`); } return { success, taskId, agent: validatedParams.agent, executedBy, fallbackUsed: executedBy !== validatedParams.agent, attempts, output: minimalOutput }; } /** * Run agent asynchronously - returns taskId immediately, task runs in background */ async runAgentAsync(params) { // Validate parameters const validatedParams = this.validateRunAgentParams(params); // Parse task parameter (handles file:// paths for long tasks) const parsedTask = this.parseTaskParam(validatedParams.task); // Create task with merged configuration const taskId = this.generateTaskId(); const baseConfig = validatedParams.config || { timeout: 300000, retries: 1, allowFallback: true }; const mergedConfig = this.mergeAgentConfig(validatedParams.agent, baseConfig); const task = { id: taskId, agent: validatedParams.agent, task: parsedTask, config: mergedConfig, status: 'queued', output: new ChunkedOutput(), taskPreview: parsedTask.slice(0, 50) + (parsedTask.length > 50 ? '...' : '') }; // Store task this.tasks.set(taskId, task); // Start task in background (fire and forget) const asyncOperation = this.executeTaskAsync(task, validatedParams.agent).catch(error => { // Handle async errors by updating task status task.status = 'failed'; task.endTime = Date.now(); task.error = error instanceof Error ? error.message : String(error); }).finally(() => { // Remove from tracking when done this.runningAsyncOperations.delete(asyncOperation); }); // Track this async operation this.runningAsyncOperations.add(asyncOperation); return { taskId, agent: validatedParams.agent, status: 'running', message: `Task started asynchronously. Use get_task_status({taskId: "${taskId}"}) to monitor progress.` }; } /** * Execute task asynchronously with fallback support */ async executeTaskAsync(task, requestedAgent) { // Don't start new tasks if shutting down if (this.isShuttingDown) { task.status = 'cancelled'; task.endTime = Date.now(); task.error = 'Manager is shutting down'; return; } let success = false; try { // Try primary agent const agent = this.registry.getAgent(requestedAgent); // Check concurrent task limit const agentConfig = this.configManager.getAgentConfig(requestedAgent); const currentTasks = this.runningCount.get(requestedAgent) || 0; const maxConcurrent = agentConfig.maxConcurrent || 1; if (currentTasks >= maxConcurrent) { throw new AgentExecutionError(`Agent ${requestedAgent} has reached max concurrent tasks (${maxConcurrent})`, requestedAgent); } await this.executeTask(task, agent); success = true; } catch (error) { this.logError(`Error executing async task with agent ${requestedAgent}`, error); // Try fallback agents if enabled if (task.config.allowFallback) { const fallbackAgents = task.config.fallbackAgents || this.getAvailableFallbackAgents(requestedAgent); for (const fallbackAgent of fallbackAgents) { try { // Create clean config for fallback agent (excludes original agent's flags) const fallbackConfig = this.createFallbackConfig(fallbackAgent, task.config); const fallbackTask = { ...task, config: fallbackConfig }; const agent = this.registry.getAgent(fallbackAgent); await this.executeTask(fallbackTask, agent); success = true; break; } catch (fallbackError) { this.logError(`Error executing async fallback task with agent ${fallbackAgent}`, fallbackError); continue; } } } if (!success) { task.status = 'failed'; task.endTime = Date.now(); task.error = error instanceof Error ? error.message : String(error); } } } async listAgents() { const detectionResults = this.registry.getDetectionResults(); const agents = detectionResults.map(result => { const config = this.configManager.getAgentConfig(result.name); return { name: result.name, available: result.available && config.enabled !== false, status: result.authStatus === 'ready' ? 'ready' : result.authStatus === 'needs_login' ? 'needs_auth' : result.authStatus === 'needs_api_key' ? 'needs_auth' : 'not_installed', version: result.version, currentTasks: this.runningCount.get(result.name) || 0, maxConcurrent: config.maxConcurrent || 1, authInstructions: result.authStatus !== 'ready' ? result.authInstructions : undefined }; }); return { agents }; } /** * List all currently running tasks */ async listRunningTasks(options) { const runningTasks = []; const now = Date.now(); for (const [taskId, task] of this.tasks.entries()) { if (task.status === 'running') { const taskInfo = { taskId, agent: task.agent, status: task.status, startTime: task.startTime || now, runtime: now - (task.startTime || now), pid: task.pid, outputLines: task.output.getTotalLines(), lastActivity: task.lastActivityTime || now, taskPreview: task.taskPreview }; if (options?.includeOutput && task.output.getTotalLines() > 0) { // Include last 5 lines of output const outputResult = task.output.getLines({ limit: 5, fromEnd: true }); taskInfo.currentOutput = outputResult.lines; } runningTasks.push(taskInfo); } } // Sort tasks if (options?.sortBy) { runningTasks.sort((a, b) => { switch (options.sortBy) { case 'startTime': return a.startTime - b.startTime; case 'agent': return a.agent.localeCompare(b.agent); case 'runtime': return b.runtime - a.runtime; // Descending - longest running first default: return 0; } }); } return { tasks: runningTasks }; } /** * Terminate a running task */ async terminateTask(taskId, _options) { const task = this.tasks.get(taskId); if (!task) { throw new Error(`Task ${taskId} not found`); } const previousStatus = task.status; const startTime = task.startTime || Date.now(); const runtime = Date.now() - startTime; if (task.status !== 'running') { return { success: false, previousStatus, finalStatus: task.status, runtime, outputLines: task.output.getTotalLines(), message: `Task is not running (status: ${task.status})` }; } try { // Terminate the process if it exists if (task.process) { await task.process.terminate(); } // Update task status task.status = 'cancelled'; task.endTime = Date.now(); task.lastActivityTime = Date.now(); // Clean up from process pool this.processPool.remove(taskId); return { success: true, previousStatus, finalStatus: 'cancelled', runtime, outputLines: task.output.getTotalLines(), message: `Task terminated successfully` }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { success: false, previousStatus, finalStatus: task.status, runtime, outputLines: task.output.getTotalLines(), message: `Failed to terminate task: ${errorMessage}` }; } } async getTaskStatus(taskId, outputOptions) { const task = this.tasks.get(taskId); if (!task) { throw new Error(`Task ${taskId} not found`); } // Check if streaming is requested if (outputOptions?.streaming) { const streamingOptions = outputOptions.streaming; const streamingResult = await task.output.getLinesStreaming({ ...(outputOptions.offset !== undefined && { offset: outputOptions.offset }), limit: outputOptions.limit || 20, fromEnd: outputOptions.fromEnd ?? false, // For streaming, default to forward ...(outputOptions.maxChars !== undefined && { maxChars: outputOptions.maxChars }), ...(outputOptions.search !== undefined && { search: outputOptions.search }), ...(outputOptions.context !== undefined && { context: outputOptions.context }), ...(outputOptions.outputMode !== undefined && { outputMode: outputOptions.outputMode }), ...(outputOptions.ignoreCase !== undefined && { ignoreCase: outputOptions.ignoreCase }), ...(outputOptions.matchNumbers !== undefined && { matchNumbers: outputOptions.matchNumbers }), ...(streamingOptions.lastSeenLine !== undefined && { lastSeenLine: streamingOptions.lastSeenLine }), ...(streamingOptions.waitForNew !== undefined && { waitForNew: streamingOptions.waitForNew }), ...(streamingOptions.maxWaitTime !== undefined && { maxWaitTime: streamingOptions.maxWaitTime }), ...(streamingOptions.includeLineNumbers !== undefined && { includeLineNumbers: streamingOptions.includeLineNumbers }) }); return { taskId: task.id, status: task.status, agent: task.agent, startTime: task.startTime, endTime: task.endTime, duration: task.startTime && task.endTime ? task.endTime - task.startTime : undefined, lines: streamingResult.lines, totalLines: streamingResult.totalLines, hasMore: streamingResult.hasMore, lastSeenLine: streamingResult.lastSeenLine, newLinesCount: streamingResult.newLinesCount, error: task.error, progress: this.calculateProgress(task) }; } // Non-streaming mode (backward compatible) const outputResult = task.output.getLines({ ...(outputOptions?.offset !== undefined && { offset: outputOptions.offset }), limit: outputOptions?.limit || 20, fromEnd: outputOptions?.fromEnd ?? true, ...(outputOptions?.maxChars !== undefined && { maxChars: outputOptions.maxChars }), ...(outputOptions?.search !== undefined && { search: outputOptions.search }), ...(outputOptions?.context !== undefined && { context: outputOptions.context }), ...(outputOptions?.outputMode !== undefined && { outputMode: outputOptions.outputMode }), ...(outputOptions?.ignoreCase !== undefined && { ignoreCase: outputOptions.ignoreCase }), ...(outputOptions?.matchNumbers !== undefined && { matchNumbers: outputOptions.matchNumbers }) }); const processedOutput = outputResult.lines; return { taskId: task.id, status: task.status, agent: task.agent, startTime: task.startTime, endTime: task.endTime, duration: task.startTime && task.endTime ? task.endTime - task.startTime : undefined, output: processedOutput, error: task.error, progress: this.calculateProgress(task) }; } async runAgentSequence(tasks, options) { const workflowId = this.generateWorkflowId(); const workflow = { id: workflowId, tasks: tasks.map(t => ({ id: t.id, agent: t.agent, status: 'pending' })), status: 'running', startTime: Date.now() }; this.workflows.set(workflowId, workflow); try { const taskResults = new Map(); let completedCount = 0; // Execute tasks in dependency order for (let i = 0; i < tasks.length; i++) { const workflowTask = tasks[i]; if (!workflowTask) continue; const workflowTaskState = workflow.tasks.find(t => t.id === workflowTask.id); if (!workflowTaskState) continue; // Check dependencies if (workflowTask.dependsOn) { const dependency = workflow.tasks.find(t => t.id === workflowTask.dependsOn); if (!dependency || dependency.status !== 'completed') { if (dependency?.status === 'failed' && !options?.continueOnFailure) { workflowTaskState.status = 'skipped'; workflowTaskState.error = `Dependency ${workflowTask.dependsOn} failed`; continue; } } // Check condition if (workflowTask.condition === 'success' && dependency?.status !== 'completed') { workflowTaskState.status = 'skipped'; workflowTaskState.error = `Dependency ${workflowTask.dependsOn} did not succeed`; continue; } } // Process task template let processedTask = workflowTask.task; // Replace template variables if (workflowTask.dependsOn && taskResults.has(workflowTask.dependsOn)) { const depResult = taskResults.get(workflowTask.dependsOn); processedTask = processedTask.replace(new RegExp(`\\{\\{${workflowTask.dependsOn}\\.output\\}\\}`, 'g'), depResult.output.slice(-10).join('\n')); } // Apply additional template data if (options?.templateData) { for (const [key, value] of Object.entries(options.templateData)) { processedTask = processedTask.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value)); } } // Execute the task workflowTaskState.status = 'running'; workflowTaskState.startTime = Date.now(); try { const result = await this.runAgent({ agent: workflowTask.agent, task: processedTask, config: workflowTask.config }); workflowTaskState.taskId = result.taskId; workflowTaskState.status = result.success ? 'completed' : 'failed'; workflowTaskState.endTime = Date.now(); if (!result.success) { workflowTaskState.error = result.attempts[result.attempts.length - 1]?.error || 'Task failed'; } // Store result for template substitution const taskStatus = await this.getTaskStatus(result.taskId); // Extract output array - handle both TaskStatus and StreamingTaskStatus const outputArray = 'output' in taskStatus ? taskStatus.output : taskStatus.lines.map(l => l.content); taskResults.set(workflowTask.id, { output: outputArray, success: result.success }); if (result.success) { completedCount++; } else if (!options?.continueOnFailure) { break; } } catch (error) { workflowTaskState.status = 'failed'; workflowTaskState.endTime = Date.now(); workflowTaskState.error = error instanceof Error ? error.message : String(error); if (!options?.continueOnFailure) { break; } } } // Mark remaining tasks as skipped if we stopped due to failure for (const task of workflow.tasks) { if (task.status === 'pending') { task.status = 'skipped'; task.error = 'Workflow stopped due to earlier failure'; } } // Determine overall success const success = completedCount > 0 && workflow.tasks.every(t => t.status === 'completed' || t.status === 'skipped'); workflow.status = success ? 'completed' : 'failed'; workflow.endTime = Date.now(); // Combine output from all completed tasks const combinedOutput = []; for (const workflowTask of workflow.tasks) { if (workflowTask.taskId) { const actualTask = this.tasks.get(workflowTask.taskId); if (actualTask && actualTask.output.getTotalLines() > 0) { combinedOutput.push(`=== ${workflowTask.id} (${workflowTask.agent}) ===`); const outputResult = actualTask.output.getLines({ limit: 5, fromEnd: true }); combinedOutput.push(...outputResult.lines); } } } return { success, workflowId, completedTasks: completedCount, totalTasks: tasks.length, tasks: workflow.tasks.map(t => ({ id: t.id, agent: t.agent, taskId: t.taskId, status: t.status, startTime: t.startTime, endTime: t.endTime, error: t.error })), output: combinedOutput.slice(-10) // Last 10 lines overall }; } catch (error) { workflow.status = 'failed'; workflow.endTime = Date.now(); return { success: false, workflowId, completedTasks: 0, totalTasks: tasks.length, tasks: workflow.tasks.map(t => ({ id: t.id, agent: t.agent, taskId: t.taskId, status: t.status === 'running' ? 'failed' : t.status, startTime: t.startTime, endTime: t.endTime, error: t.error || (error instanceof Error ? error.message : String(error)) })), output: [`Workflow failed: ${error instanceof Error ? error.message : String(error)}`] }; } } generateWorkflowId() { return `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } async runAgentsParallel(tasks, options) { const parallelId = this.generateParallelId(); const taskNames = Object.keys(tasks); const startTime = Date.now(); // Initialize result structure const result = { success: false, parallelId, completedTasks: 0, totalTasks: taskNames.length, tasks: {}, output: [] }; // Initialize task states for (const [name, task] of Object.entries(tasks)) { result.tasks[name] = { agent: task.agent, status: 'running', startTime: Date.now() }; } try { // Create promises for all tasks const taskPromises = taskNames.map(async (name) => { const task = tasks[name]; if (!task) { return { name, success: false, error: 'Task not found' }; } try { const taskResult = await this.runAgent({ agent: task.agent, task: task.task, config: task.config }); const taskState = result.tasks[name]; if (taskState) { taskState.taskId = taskResult.taskId; taskState.status = taskResult.success ? 'completed' : 'failed'; taskState.endTime = Date.now(); if (!taskResult.success) { taskState.error = taskResult.attempts[taskResult.attempts.length - 1]?.error || 'Task failed'; } } return { name, success: taskResult.success, taskId: taskResult.taskId }; } catch (error) { const taskState = result.tasks[name]; if (taskState) { taskState.status = 'failed'; taskState.endTime = Date.now(); taskState.error = error instanceof Error ? error.message : String(error); } return { name, success: false, error: error instanceof Error ? error.message : String(error) }; } }); // Handle timeout if specified let completedTasks; if (options?.timeout) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(`Parallel execution timed out after ${options.timeout}ms`)), options.timeout); }); try { completedTasks = await Promise.race([ Promise.all(taskPromises), timeoutPromise ]); } catch (error) { // Timeout occurred - mark all running tasks as cancelled for (const taskState of Object.values(result.tasks)) { if (taskState.status === 'running') { taskState.status = 'cancelled'; taskState.endTime = Date.now(); taskState.error = 'Cancelled due to timeout'; } } throw error; } } else { // Wait for all tasks with no timeout if (options?.failFast) { // Fail fast mode - stop on first failure const settledTasks = await Promise.allSettled(taskPromises); completedTasks = settledTasks.map((settled, index) => { const name = taskNames[index]; if (settled.status === 'fulfilled') { return settled.value; } else { return { name: name || 'unknown', success: false, error: settled.reason instanceof Error ? settled.reason.message : String(settled.reason) }; } }); } else { // Normal mode - wait for all to complete completedTasks = await Promise.all(taskPromises); } } // Count successful completions let successCount = 0; const combinedOutput = []; for (const completed of completedTasks) { if (completed.success) { successCount++; // Get output from completed task if (completed.taskId) { try { const taskStatus = await this.getTaskStatus(completed.taskId, { limit: 5 }); // Extract output array - handle both TaskStatus and StreamingTaskStatus const outputArray = 'output' in taskStatus ? taskStatus.output : taskStatus.lines.map(l => l.content); if (outputArray.length > 0) { const taskState = result.tasks[completed.name]; if (taskState) { combinedOutput.push(`=== ${completed.name} (${taskState.agent}) ===`); combinedOutput.push(...outputArray); } } } catch (error) { // Ignore errors getting task output } } } } result.completedTasks = successCount; result.success = successCount > 0; result.duration = Date.now() - startTime; result.output = combinedOutput.slice(-10); // Last 10 lines return result; } catch (error) { result.success = false; result.duration = Date.now() - startTime; result.output = [`Parallel execution failed: ${error instanceof Error ? error.message : String(error)}`]; return result; } } generateParallelId() { return `parallel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Parse task parameter - handles both direct task strings and file:// paths * For long tasks or those with special characters, agents can write to temp files * and pass file://path instead of the full task content */ parseTaskParam(taskParam) { // Check if this is a file reference if (taskParam.startsWith('file://')) { const filePath = taskParam.replace('file://', ''); // Security: Only allow temp directories const allowedPrefixes = ['/tmp/', '/var/tmp/']; if (process.env['TMPDIR']) { allowedPrefixes.push(process.env['TMPDIR'].endsWith('/') ? process.env['TMPDIR'] : process.env['TMPDIR'] + '/'); } const isAllowed = allowedPrefixes.some(prefix => filePath.startsWith(prefix)); if (!isAllowed) { throw new Error(`File path not allowed: ${filePath}. Must be in temp directory (/tmp/, /var/tmp/, or TMPDIR).`); } // Check if file exists if (!existsSync(filePath)) { throw new Error(`Task file not found: ${filePath}`); } try { // Read file content const content = readFileSync(filePath, 'utf8'); // Clean up temp file automatically try { unlinkSync(filePath); } catch (cleanupError) { // Log but don't fail - cleanup is best effort console.warn(`Failed to cleanup temp file ${filePath}:`, cleanupError); } return content; } catch (error) { throw new Error(`Failed to read task file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } // Regular task parameter return taskParam; } applySmartSummary(task, config) { // Check if smart summary is disabled if (config?.smartSummary === false) { return task; } // Determine if task is complex (heuristics) const isComplex = this.isComplexTask(task, config); if (isComplex) { const summaryRequest = "\n\nPlease provide a concise 2-3 line summary of what you accomplished and any issues encountered."; return task + summaryRequest; } return task; } isComplexTask(task, config) { // Task is complex if: // 1. Task description is long (> 200 chars) // 2. Multiple files specified // 3. Contains keywords suggesting complexity // 4. Explicitly enabled via config if (config?.smartSummary === true) { return true; } // Long task descriptions if (task.length > 200) { return true; } // Multiple files if (config?.files && config.files.length > 2) { return true; } // Complex operation keywords const complexKeywords = [ 'analyze', 'refactor', 'implement', 'debug', 'optimize', 'review', 'test', 'build', 'deploy', 'migrate', 'architecture', 'security', 'performance', 'integration' ]; const taskLower = task.toLowerCase(); const hasComplexKeywords = complexKeywords.some(keyword => taskLower.includes(keyword)); return hasComplexKeywords; } async executeTask(task, agent) { // Check if shutting down if (this.isShuttingDown) { throw new AgentExecutionError('Manager is shutting down', task.agent); } task.status = 'running'; task.startTime = Date.now(); // Update running count const currentCount = this.runningCount.get(task.agent) || 0; this.runningCount.set(task.agent, currentCount + 1); const managedProcess = new ManagedProcess(); task.process = managedProcess; this.processPool.add(task.id, managedProcess); try { const enhancedTask = this.applySmartSummary(task.task, task.config); const command = agent.buildCommand(enhancedTask, task.config); const [cmd, ...args] = command; // Merge environment variables const mergedEnv = { ...process.env, ...task.config.env, ...task.config.context?.environment }; // Spawn the process with proper management const resultPromise = managedProcess.spawn({ command: cmd, args, env: mergedEnv, cwd: task.config.workingDirectory, timeout: task.config.timeout, useRealTimeStreaming: true, // Enable PTY for true real-time output onOutput: (lines) => { // Stream output to ChunkedOutput in real-time task.output.addLines(lines); task.lastActivityTime = Date.now(); } }); // Handle stdin for agents that need it if (agent.getInputMethod() === 'stdin') { managedProcess.sendInput(enhancedTask + '\n'); } // Capture PID after process starts const pid = managedProcess.getPid(); if (pid !== undefined) { task.pid = pid; } task.status = 'running'; task.startTime = Date.now(); task.lastActivityTime = Date.now(); // Wait for completion const result = await resultPromise; // Output already streamed in real-time via onOutput callback // Just update final metadata task.lastActivityTime = Date.now(); // Check exit status if (result.exitCode !== 0) { throw new AgentExecutionError(`Agent exited with code ${result.exitCode}`, task.agent, result.exitCode ?? undefined); } task.status = 'completed'; task.endTime = Date.now(); } catch (error) { task.status = 'failed'; task.endTime = Date.now(); task.error = error instanceof Error ? error.message : String(error); throw error; } finally { // Update running count const count = this.runningCount.get(task.agent) || 1; this.runningCount.set(task.agent, Math.max(0, count - 1)); // Remove from process pool this.processPool.remove(task.id); } } validateRunAgentParams(params) { const schema = z.object({ agent: z.enum(['qwen', 'gemini', 'aider', 'goose', 'codex', 'opencode', 'claude']), task: z.string().min(1, 'Task cannot be empty'), config: z.any().optional() }); try { return schema.parse(params); } catch (error) { if (error instanceof z.ZodError) { throw new MCPValidationError('Invalid run_agent parameters', error.errors); } throw error; } } generateTaskId() { return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } mergeAgentConfig(agentName, taskConfig) { const globalAgentConfig = this.configManager.getAgentConfig(agentName); // Merge configurations with task config taking precedence return { ...taskConfig, // Merge environment variables (task overrides global) env: { ...globalAgentConfig.env, ...taskConfig.env }, // Merge flags (global first, then task flags) flags: [ ...(globalAgentConfig.flags || []), ...(taskConfig.flags || []) ], // Use task model if provided, otherwise global, otherwise undefined model: taskConfig.model || globalAgentConfig.model || taskConfig.model }; } /** * Create clean config for fallback agent, excluding agent-specific flags */ createFallbackConfig(fallbackAgent, originalTaskConfig) { // Agent-specific flags that should NOT be passed to other agents const agentSpecificFlags = new Set([ '--weak-model', '--strong-model', // aider specific '--model', // generic but handled separately '--yes-always', '--auto-commits', // aider specific '--quiet', '--no-session', // goose specific '--sandbox', '--workspace-write', // codex specific ]); // Create clean base config without agent-specific flags const cleanTaskConfig = { ...originalTaskConfig, flags: (originalTaskConfig.flags || []).filter(flag => { // Keep flag if it's not agent-specific if (!flag || typeof flag !== 'string') return false; const flagName = flag.startsWith('--') ? (flag.split('=')[0] || flag) : flag; return !agentSpecificFlags.has(flagName); }) }; // Now merge with fallback agent's config return this.mergeAgentConfig(fallbackAgent, cleanTaskConfig); } calculateProgress(task) { if (task.status === 'queued') return 0; if (task.status === 'completed') return 100; if (task.status === 'failed' || task.status === 'cancelled') return 0; // For running tasks, estimate based on output return Math.min(50 + Math.floor(task.output.getTotalLines() / 10), 90); } getAvailableFallbackAgents(excludeAgent) { // Get all available agents except the excluded one const availableAgents = this.registry.listAvailableAgents() .filter(a => a !== excludeAgent); // Sort by priority (lower number = higher priority) const agentsWithPriority = availableAgents.map(agent => ({ agent, config: this.configManager.getAgentConfig(agent), priority: this.configManager.getAgentConfig(agent).priority || 50 // Default priority })); // Filter out disabled agents and sort by priority return agentsWithPriority .filter(a => a.config.enabled !== false && this.configManager.isAgentEnabled(a.agent)) .sort((a, b) => a.priority - b.priority) .map(a => a.agent); } /** * Shutdown the manager and cleanup all processes */ async shutdown() { if (!process.env['MCP_MODE']) { console.error('🛑 Shutting down agent manager...'); } this.isShuttingDown = true; // Cancel all running tasks for (const [, task] of this.tasks.entries()) { if (task.status === 'running') { task.status = 'cancelled'; task.endTime = Date.now(); task.error = 'Cancelled due to shutdown'; } } // Terminate all processes await this.processPool.terminateAll(); // Wait for all async operations to complete (with timeout) if (this.runningAsyncOperations.size > 0) { if (!process.env['MCP_MODE']) { console.error(`Waiting for ${this.runningAsyncOperations.size} async operations to complete...`); } const timeout = new Promise(resolve => setTimeout(resolve, 5000)); // 5 second timeout const operations = Promise.allSettled(Array.from(this.runningAsyncOperations)); await Promise.race([operations, timeout]); } if (!process.env['MCP_MODE']) { console.error('✅ Shutdown complete'); } process.exit(0); } } //# sourceMappingURL=manager.js.map