cns-mcp-server
Version:
Central Nervous System MCP Server for Autonomous Multi-Agent Orchestration with free local embeddings
420 lines โข 18.5 kB
JavaScript
/**
* Agent Runner - Spawns isolated Claude agents to execute tasks autonomously
* Phase 1: Single agent isolation with spawn loop prevention
*/
import { spawn } from 'child_process';
import { appendFileSync } from 'fs';
import { logger } from '../utils/logger.js';
export class AgentRunner {
cnsServer;
runningAgents = new Map();
isRunning = false;
pollInterval = null;
maxConcurrentAgents = 3; // Configurable via env
constructor(cnsServer) {
this.cnsServer = cnsServer;
// No CNSClient needed - we're INSIDE the CNS server!
this.maxConcurrentAgents = parseInt(process.env.CNS_MAX_AGENTS || '3');
}
async start() {
logger.info('Starting CNS Agent Runner', {
maxConcurrentAgents: this.maxConcurrentAgents
});
// No client connection needed - we're internal!
this.isRunning = true;
// Start polling for pending tasks every 10 seconds
this.pollInterval = setInterval(async () => {
await this.processPendingTasks();
}, 10000);
// Initial poll
await this.processPendingTasks();
logger.info('CNS Agent Runner started successfully');
}
async stop() {
logger.info('Stopping CNS Agent Runner');
this.isRunning = false;
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
// Gracefully stop running agents
for (const [taskId, agent] of this.runningAgents) {
logger.info('Terminating running agent', { taskId, agentType: agent.agentType });
if (agent.process && !agent.process.killed) {
agent.process.kill('SIGTERM');
}
}
// No client to disconnect - we're internal
logger.info('CNS Agent Runner stopped');
}
async processPendingTasks() {
if (!this.isRunning)
return;
try {
// Check if we can run more agents
const availableSlots = this.maxConcurrentAgents - this.runningAgents.size;
if (availableSlots <= 0) {
logger.debug('Max concurrent agents reached, skipping poll', {
running: this.runningAgents.size,
max: this.maxConcurrentAgents
});
return;
}
// Get pending tasks from orchestration engine
const response = await this.cnsServer.orchestration.getPendingTasks();
const tasksData = JSON.parse(response.content[0].text);
const pendingTasks = tasksData.tasks || [];
if (pendingTasks.length === 0) {
return; // No tasks to process
}
logger.info('Processing pending tasks', {
count: pendingTasks.length,
availableSlots
});
// Execute tasks up to available slots
const tasksToExecute = pendingTasks.slice(0, availableSlots);
for (const task of tasksToExecute) {
// ๐ซ CRITICAL FIX: Check if workflow is still active before spawning agents
const workflowStatus = await this.validateWorkflowStatus(task.workflow_id);
if (workflowStatus.shouldStop) {
logger.warn('๐ซ Skipping task - workflow stopped', {
taskId: `${task.agent_type}-${task.workflow_id}`,
workflowStatus: workflowStatus.status,
reason: workflowStatus.reason
});
// Remove the pending task from queue since workflow is stopped
await this.removePendingTask(task);
continue;
}
await this.executeAgent(task);
}
}
catch (error) {
logger.error('Error processing pending tasks', { error });
}
}
async executeAgent(task) {
const taskId = `${task.agent_type}-${task.workflow_id}-${Date.now()}`;
let workspacePath = null;
try {
// Determine workspace strategy based on agent type
const isManagerAgent = this.isManagerAgent(task.agent_type);
const workspaceId = isManagerAgent ? task.workflow_id : `${task.workflow_id}-shared`;
logger.info('๐ Team Workspace Strategy:', {
taskId,
agentType: task.agent_type,
workflowId: task.workflow_id,
isManager: isManagerAgent,
workspaceStrategy: isManagerAgent ? 'Create new workflow workspace' : 'Reuse existing workflow workspace'
});
// Step 1: Create or reuse workflow-based workspace
const workspaceResponse = await this.cnsServer.workspaces.create({
agent_id: workspaceId, // Use workflow ID as workspace identifier
base_ref: 'main',
resources: {
workflow_id: task.workflow_id,
primary_agent: isManagerAgent ? task.agent_type : 'inherited',
team_members: isManagerAgent ? [task.agent_type] : 'joining_existing'
}
});
const workspaceData = JSON.parse(workspaceResponse.content[0].text);
workspacePath = workspaceData.workspace_path;
if (!workspacePath) {
throw new Error('Workspace creation returned invalid path');
}
logger.info('โ
Workspace created', { taskId, workspacePath });
// Step 2: Prompt will be sent via stdin
logger.info('๐ DEBUG: About to spawn agent', {
taskId,
command: 'claude',
args: ['--print', '--permission-mode', 'bypassPermissions'],
cwd: workspacePath,
promptLength: task.prompt.length
});
// Debug file logging
const debugInfo = `[${new Date().toISOString()}] About to spawn: ${taskId}\n` +
` Command: claude --print --permission-mode bypassPermissions\n` +
` CWD: ${workspacePath}\n` +
` Prompt length: ${task.prompt.length}\n\n`;
appendFileSync('/home/ari1110/projects/cns-mcp-server/agent-debug.log', debugInfo);
// Step 3: Spawn Claude Code process in isolated workspace
const childProcess = spawn('claude', [
'--print',
'--permission-mode', 'bypassPermissions'
], {
cwd: workspacePath, // CRITICAL: Run in isolated workspace
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
// Phase 1.5 Workspace Isolation
PWD: workspacePath, // Ensure correct working directory
CNS_MODE: 'isolated_agent', // Tells Claude it's a worker
CNS_WORKSPACE_PATH: workspacePath, // Agent knows its workspace
CNS_WORKFLOW_ID: task.workflow_id, // Context for the agent
CNS_AGENT_TYPE: task.agent_type, // Role specification
CNS_AGENT_ID: taskId, // Unique identifier
// Team Coordination: Manager agents get MCP tools, associates isolated
CNS_DISABLE_MCP_CNS: isManagerAgent ? 'false' : 'true',
MCP_SERVERS_CONFIG: isManagerAgent ? JSON.stringify({
cns: {
command: 'npx',
args: ['-y', 'cns-mcp-server']
}
}) : JSON.stringify({}), // Associates get no MCP servers
}
});
logger.info('๐ DEBUG: Spawn attempted', {
taskId,
pid: childProcess.pid,
killed: childProcess.killed,
connected: childProcess.connected
});
// Debug file logging for spawn result
const spawnInfo = `[${new Date().toISOString()}] Spawn result: ${taskId}\n` +
` PID: ${childProcess.pid}\n` +
` Killed: ${childProcess.killed}\n` +
` Connected: ${childProcess.connected}\n\n`;
appendFileSync('/home/ari1110/projects/cns-mcp-server/agent-debug.log', spawnInfo);
// Track running agent with workspace
const runningAgent = {
process: childProcess,
taskId,
workflowId: task.workflow_id,
agentType: task.agent_type,
startTime: new Date(),
workspacePath: workspacePath
};
this.runningAgents.set(taskId, runningAgent);
// Handle process completion
childProcess.on('exit', (code, signal) => {
this.handleAgentCompletion(taskId, code, signal);
});
childProcess.on('error', (error) => {
logger.error('Agent process error', { taskId, error });
this.runningAgents.delete(taskId);
});
// Enhanced agent output logging for oversight
if (childProcess.stdout) {
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim();
logger.info(`๐ค Agent ${taskId} stdout:`, output);
// Log to oversight file for real-time monitoring
appendFileSync('/home/ari1110/projects/cns-mcp-server/agent-oversight.log', `[${new Date().toISOString()}] ${taskId} STDOUT: ${output}\n`);
});
}
if (childProcess.stderr) {
childProcess.stderr.on('data', (data) => {
const output = data.toString().trim();
logger.warn(`โ ๏ธ Agent ${taskId} stderr:`, output);
// Log to oversight file for real-time monitoring
appendFileSync('/home/ari1110/projects/cns-mcp-server/agent-oversight.log', `[${new Date().toISOString()}] ${taskId} STDERR: ${output}\n`);
});
}
// Send prompt via stdin
if (childProcess.stdin) {
childProcess.stdin.write(task.prompt);
childProcess.stdin.end();
}
logger.info('โ
Agent spawned successfully', { taskId, pid: childProcess.pid });
}
catch (error) {
logger.error('Failed to spawn agent', {
taskId,
agentType: task.agent_type,
error
});
// Cleanup workspace if it was created
if (workspacePath) {
try {
await this.cnsServer.workspaces.cleanup({
agent_id: taskId,
force: true
});
logger.info('๐งน Workspace cleaned up after error', { taskId, workspacePath });
}
catch (cleanupError) {
logger.error('Failed to cleanup workspace after error', { taskId, cleanupError });
}
}
}
}
async handleAgentCompletion(taskId, code, signal) {
const agent = this.runningAgents.get(taskId);
if (!agent) {
logger.warn('๐ DEBUG: handleAgentCompletion called for unknown agent', { taskId });
return;
}
const duration = Date.now() - agent.startTime.getTime();
logger.info('๐ Agent completed', {
taskId,
agentType: agent.agentType,
workflowId: agent.workflowId,
exitCode: code,
signal,
duration: `${duration}ms`,
workspacePath: agent.workspacePath
});
// Debug file logging for completion
const completionInfo = `[${new Date().toISOString()}] Agent completed: ${taskId}\n` +
` Exit code: ${code}\n` +
` Signal: ${signal}\n` +
` Duration: ${duration}ms\n` +
` Workspace: ${agent.workspacePath}\n\n`;
appendFileSync('/home/ari1110/projects/cns-mcp-server/agent-debug.log', completionInfo);
// Remove from tracking
this.runningAgents.delete(taskId);
try {
// Signal completion to orchestration engine
await this.cnsServer.orchestration.signalCompletion({
agent_id: taskId,
workflow_id: agent.workflowId,
result: code === 0 ? 'Agent completed successfully' : `Agent failed with code ${code}`,
artifacts: []
});
logger.info('โ
Agent completion signaled to orchestration', { taskId });
// Cleanup workspace after successful completion
if (agent.workspacePath) {
try {
await this.cnsServer.workspaces.cleanup({
agent_id: taskId,
force: false // Gentle cleanup for successful completion
});
logger.info('๐งน Workspace cleaned up after completion', {
taskId,
workspacePath: agent.workspacePath
});
}
catch (cleanupError) {
logger.error('Failed to cleanup workspace after completion', { taskId, cleanupError });
}
}
}
catch (error) {
logger.error('Failed to signal agent completion', {
taskId,
error
});
}
}
isManagerAgent(agentType) {
const managerAgentTypes = [
'team-manager', 'feature-team-lead', 'project-lead', 'tech-lead',
'release-manager', 'qa-manager', 'devops-manager', 'architect'
];
return managerAgentTypes.some(managerType => agentType.includes(managerType) || agentType.includes('manager') || agentType.includes('lead'));
}
async stopWorkflow(workflowId, reason = 'Manual stop', force = false) {
const agentsToStop = Array.from(this.runningAgents.values())
.filter(agent => agent.workflowId === workflowId);
if (agentsToStop.length === 0) {
return {
status: 'no_agents_found',
workflow_id: workflowId,
message: 'No running agents found for this workflow'
};
}
logger.warn(`โน๏ธ Stopping workflow ${workflowId}: ${reason}`, {
agentsToStop: agentsToStop.length,
force
});
const stoppedAgents = [];
for (const agent of agentsToStop) {
try {
if (agent.process && !agent.process.killed) {
const signal = force ? 'SIGKILL' : 'SIGTERM';
agent.process.kill(signal);
stoppedAgents.push({
taskId: agent.taskId,
agentType: agent.agentType,
runtime: Date.now() - agent.startTime.getTime(),
signal: signal
});
// Remove from tracking
this.runningAgents.delete(agent.taskId);
}
}
catch (error) {
logger.error(`Failed to stop agent ${agent.taskId}:`, error);
}
}
return {
status: 'workflow_stopped',
workflow_id: workflowId,
reason: reason,
agents_stopped: stoppedAgents.length,
stopped_agents: stoppedAgents,
force: force
};
}
getStatus() {
return {
isRunning: this.isRunning,
runningAgents: this.runningAgents.size,
maxConcurrentAgents: this.maxConcurrentAgents,
agents: Array.from(this.runningAgents.values()).map(agent => ({
taskId: agent.taskId,
agentType: agent.agentType,
workflowId: agent.workflowId,
runtime: Date.now() - agent.startTime.getTime()
}))
};
}
/**
* Validate if a workflow is still active and should continue spawning agents
*/
async validateWorkflowStatus(workflowId) {
try {
const response = await this.cnsServer.orchestration.getWorkflowStatus(workflowId);
const workflowData = JSON.parse(response.content[0].text);
if (workflowData.error) {
return {
shouldStop: true,
status: 'not_found',
reason: 'Workflow not found'
};
}
const workflow = workflowData.workflow;
const stoppedStatuses = ['failed', 'completed', 'stale'];
if (stoppedStatuses.includes(workflow.status)) {
return {
shouldStop: true,
status: workflow.status,
reason: `Workflow status is ${workflow.status}`
};
}
return {
shouldStop: false,
status: workflow.status,
reason: 'Workflow is active'
};
}
catch (error) {
logger.error('Failed to validate workflow status', { workflowId, error });
// In case of error, allow the task to proceed but log warning
return {
shouldStop: false,
status: 'unknown',
reason: 'Status validation failed - allowing execution'
};
}
}
/**
* Remove a pending task from the orchestration queue
*/
async removePendingTask(task) {
try {
// Remove from orchestration engine's pending tasks
// This would need to be implemented in the orchestration engine
logger.info('๐๏ธ Removing pending task from queue', {
taskId: `${task.agent_type}-${task.workflow_id}`,
workflowId: task.workflow_id
});
// For now, we'll just log - the orchestration engine should
// handle cleanup of stopped workflow tasks automatically
}
catch (error) {
logger.error('Failed to remove pending task', { task, error });
}
}
}
//# sourceMappingURL=index.js.map