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
JavaScript
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