@sethdouglasford/claude-flow
Version:
Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology
794 lines • 29.6 kB
JavaScript
/**
* Advanced Task Executor with timeout handling and process management
*/
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
import { Logger } from "../core/logger.js";
import { generateId } from "../utils/helpers.js";
import { SWARM_CONSTANTS, } from "./types.js";
export class TaskExecutor extends EventEmitter {
logger;
config;
activeExecutions = new Map();
resourceMonitor;
processPool;
constructor(config = {}) {
super();
this.config = this.mergeWithDefaults(config);
this.logger = new Logger({ level: (this.config.logLevel ?? "info"), format: "text", destination: "console" }, { component: "TaskExecutor" });
this.resourceMonitor = new ResourceMonitor();
this.processPool = new ProcessPool(this.config);
this.setupEventHandlers();
}
async initialize() {
this.logger.info("Initializing task executor...");
await this.resourceMonitor.initialize();
await this.processPool.initialize();
this.logger.info("Task executor initialized");
}
async shutdown() {
this.logger.info("Shutting down task executor...");
// Stop all active executions
const stopPromises = Array.from(this.activeExecutions.values())
.map(session => this.stopExecution(session.id, "Executor shutdown"));
await Promise.allSettled(stopPromises);
await this.processPool.shutdown();
await this.resourceMonitor.shutdown();
this.logger.info("Task executor shut down");
}
async executeTask(task, agent, options = {}) {
const sessionId = generateId("execution");
const context = await this.createExecutionContext(task, agent);
const config = { ...this.config, ...options };
this.logger.info("Starting task execution", {
sessionId,
taskId: task.id.id,
agentId: agent.id.id,
timeout: config.timeoutMs,
});
const session = new ExecutionSession(sessionId, task, agent, context, config, this.logger);
this.activeExecutions.set(sessionId, session);
try {
// Setup monitoring
this.resourceMonitor.startMonitoring(sessionId, context.resources);
// Execute with timeout protection
const result = await this.executeWithTimeout(session);
// Cleanup
await this.cleanupExecution(session);
this.logger.info("Task execution completed", {
sessionId,
success: result.success,
duration: result.duration,
});
return result;
}
catch (error) {
this.logger.error("Task execution failed", {
sessionId,
error: error.message,
stack: error.stack,
});
await this.cleanupExecution(session);
throw error;
}
finally {
this.activeExecutions.delete(sessionId);
this.resourceMonitor.stopMonitoring(sessionId);
}
}
async stopExecution(sessionId, reason) {
const session = this.activeExecutions.get(sessionId);
if (!session) {
return;
}
this.logger.info("Stopping execution", { sessionId, reason });
try {
await session.stop(reason);
}
catch (error) {
this.logger.error("Error stopping execution", { sessionId, error: error.message });
}
}
async executeClaudeTask(task, agent, claudeOptions = {}) {
const sessionId = generateId("claude-execution");
const context = await this.createExecutionContext(task, agent);
this.logger.info("Starting Claude task execution", {
sessionId,
taskId: task.id.id,
agentId: agent.id.id,
});
try {
return await this.executeClaudeWithTimeout(sessionId, task, agent, context, claudeOptions);
}
catch (error) {
this.logger.error("Claude task execution failed", {
sessionId,
error: error.message,
});
throw error;
}
}
getActiveExecutions() {
return Array.from(this.activeExecutions.values());
}
getExecutionMetrics() {
return {
activeExecutions: this.activeExecutions.size,
totalExecutions: this.processPool.getTotalExecutions(),
averageDuration: this.processPool.getAverageDuration(),
successRate: this.processPool.getSuccessRate(),
resourceUtilization: this.resourceMonitor.getUtilization(),
errorRate: this.processPool.getErrorRate(),
};
}
async executeWithTimeout(session) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.logger.warn("Execution timeout", {
sessionId: session.id,
timeout: session.config.timeoutMs,
});
void session.stop("Timeout").then(() => {
reject(new Error(`Execution timed out after ${session.config.timeoutMs}ms`));
});
}, session.config.timeoutMs);
session.execute()
.then(result => {
clearTimeout(timeout);
resolve(result);
})
.catch(error => {
clearTimeout(timeout);
reject(error);
});
});
}
async executeClaudeWithTimeout(sessionId, task, agent, context, options) {
const startTime = Date.now();
const timeout = options.timeout ?? this.config.timeoutMs;
// Build Claude command
const command = this.buildClaudeCommand(task, agent, options);
// Create execution environment
const env = {
...process.env,
...context.environment,
CLAUDE_TASK_ID: task.id.id,
CLAUDE_AGENT_ID: agent.id.id,
CLAUDE_SESSION_ID: sessionId,
CLAUDE_WORKING_DIR: context.workingDirectory,
};
this.logger.debug("Executing Claude command", {
sessionId,
command: command.command,
args: command.args,
workingDir: context.workingDirectory,
});
return new Promise((resolve, reject) => {
let outputBuffer = "";
let errorBuffer = "";
let isTimeout = false;
let process = null;
// Setup timeout
const timeoutHandle = setTimeout(() => {
isTimeout = true;
if (process) {
this.logger.warn("Claude execution timeout, killing process", {
sessionId,
pid: process.pid,
timeout,
});
// Graceful shutdown first
process.kill("SIGTERM");
// Force kill after grace period
setTimeout(() => {
if (process && !process.killed) {
process.kill("SIGKILL");
}
}, this.config.killTimeout);
}
}, timeout);
try {
// Spawn Claude process
process = spawn(command.command, command.args, {
cwd: context.workingDirectory,
env,
stdio: ["pipe", "pipe", "pipe"],
detached: options.detached ?? false,
});
if (!process.pid) {
clearTimeout(timeoutHandle);
reject(new Error("Failed to spawn Claude process"));
return;
}
this.logger.info("Claude process started", {
sessionId,
pid: process.pid,
command: command.command,
});
// Handle process output
if (process.stdout) {
process.stdout.on("data", (data) => {
const chunk = data.toString();
outputBuffer += chunk;
if (this.config.streamOutput) {
this.emit("output", {
sessionId,
type: "stdout",
data: chunk,
});
}
});
}
if (process.stderr) {
process.stderr.on("data", (data) => {
const chunk = data.toString();
errorBuffer += chunk;
if (this.config.streamOutput) {
this.emit("output", {
sessionId,
type: "stderr",
data: chunk,
});
}
});
}
// Handle process completion
process.on("close", async (code, signal) => {
clearTimeout(timeoutHandle);
const duration = Date.now() - startTime;
const exitCode = code ?? 0;
this.logger.info("Claude process completed", {
sessionId,
exitCode,
signal,
duration,
isTimeout,
});
try {
// Collect resource usage
const resourceUsage = await this.collectResourceUsage(sessionId);
// Collect artifacts
const artifacts = await this.collectArtifacts(context);
const result = {
success: !isTimeout && exitCode === 0,
output: outputBuffer,
error: errorBuffer,
exitCode,
duration,
resourcesUsed: resourceUsage,
artifacts,
metadata: {
sessionId,
timeout: isTimeout,
signal,
command: command.command,
args: command.args,
},
};
if (isTimeout) {
reject(new Error(`Claude execution timed out after ${timeout}ms`));
}
else if (exitCode !== 0) {
reject(new Error(`Claude execution failed with exit code ${exitCode}: ${errorBuffer}`));
}
else {
resolve(result);
}
}
catch (error) {
reject(error);
}
});
// Handle process errors
process.on("error", (error) => {
clearTimeout(timeoutHandle);
this.logger.error("Claude process error", {
sessionId,
error: (error).message,
});
reject(error);
});
// Send input if provided
if (command.input && process.stdin) {
process.stdin.write(command.input);
process.stdin.end();
}
// If detached, unreference to allow parent to exit
if (options.detached) {
process.unref();
}
}
catch (error) {
clearTimeout(timeoutHandle);
reject(error);
}
});
}
buildClaudeCommand(task, agent, options) {
const args = [];
let input = "";
// Build prompt
const prompt = this.buildClaudePrompt(task, agent);
if (options.useStdin) {
// Send prompt via stdin
input = prompt;
}
else {
// Send prompt as argument
args.push("-p", prompt);
}
// Add tools
if (task.requirements.tools.length > 0) {
args.push("--allowedTools", task.requirements.tools.join(","));
}
// Add model if specified
if (options.model) {
args.push("--model", options.model);
}
// Add max tokens if specified
if (options.maxTokens) {
args.push("--max-tokens", options.maxTokens.toString());
}
// Add temperature if specified
if (options.temperature !== undefined) {
args.push("--temperature", options.temperature.toString());
}
// Skip permissions check for swarm execution
args.push("--dangerously-skip-permissions");
// Add output format
if (options.outputFormat) {
args.push("--output-format", options.outputFormat);
}
return {
command: options.claudePath ?? "claude",
args,
input,
};
}
buildClaudePrompt(task, agent) {
const sections = [];
// Agent identification
sections.push(`You are ${agent.name}, a ${agent.type} agent in a swarm system.`);
sections.push(`Agent ID: ${agent.id.id}`);
sections.push(`Swarm ID: ${agent.id.swarmId}`);
sections.push("");
// Task information
sections.push(`TASK: ${task.name}`);
sections.push(`Type: ${task.type}`);
sections.push(`Priority: ${task.priority}`);
sections.push("");
// Task description
sections.push("DESCRIPTION:");
sections.push(task.description);
sections.push("");
// Task instructions
sections.push("INSTRUCTIONS:");
sections.push(task.instructions);
sections.push("");
// Context if provided
if (Object.keys(task.context).length > 0) {
sections.push("CONTEXT:");
sections.push(JSON.stringify(task.context, null, 2));
sections.push("");
}
// Input data if provided
if (task.input && Object.keys(task.input).length > 0) {
sections.push("INPUT DATA:");
sections.push(JSON.stringify(task.input, null, 2));
sections.push("");
}
// Examples if provided
if (task.examples && task.examples.length > 0) {
sections.push("EXAMPLES:");
task.examples.forEach((example, index) => {
sections.push(`Example ${index + 1}:`);
sections.push(JSON.stringify(example, null, 2));
sections.push("");
});
}
// Expected output format
sections.push("EXPECTED OUTPUT:");
if (task.expectedOutput) {
sections.push(JSON.stringify(task.expectedOutput, null, 2));
}
else {
sections.push("Provide a structured response with:");
sections.push("- Summary of what was accomplished");
sections.push("- Any artifacts created (files, data, etc.)");
sections.push("- Recommendations or next steps");
sections.push("- Any issues encountered");
}
sections.push("");
// Quality requirements
sections.push("QUALITY REQUIREMENTS:");
sections.push(`- Quality threshold: ${task.requirements.minReliability ?? 0.8}`);
if (task.requirements.reviewRequired) {
sections.push("- Review required before completion");
}
if (task.requirements.testingRequired) {
sections.push("- Testing required before completion");
}
if (task.requirements.documentationRequired) {
sections.push("- Documentation required");
}
sections.push("");
// Capabilities and constraints
sections.push("CAPABILITIES:");
const capabilities = Object.entries(agent.capabilities)
.filter(([key, value]) => typeof value === "boolean" && value)
.map(([key]) => key);
sections.push(capabilities.join(", "));
sections.push("");
sections.push("CONSTRAINTS:");
sections.push(`- Maximum execution time: ${task.constraints.timeoutAfter || SWARM_CONSTANTS.DEFAULT_TASK_TIMEOUT}ms`);
sections.push(`- Maximum retries: ${task.constraints.maxRetries || SWARM_CONSTANTS.MAX_RETRIES}`);
if (task.constraints.deadline) {
sections.push(`- Deadline: ${task.constraints.deadline.toISOString()}`);
}
sections.push("");
// Final instructions
sections.push("EXECUTION GUIDELINES:");
sections.push("1. Read and understand the task completely before starting");
sections.push("2. Use your capabilities efficiently and effectively");
sections.push("3. Provide detailed output about your progress and results");
sections.push("4. Handle errors gracefully and report issues clearly");
sections.push("5. Ensure your work meets the quality requirements");
sections.push("6. When complete, provide a clear summary of what was accomplished");
sections.push("");
sections.push("Begin your task execution now.");
return sections.join("\n");
}
async createExecutionContext(task, agent) {
const baseDir = path.join(os.tmpdir(), "swarm-execution", task.id.id);
const workingDir = path.join(baseDir, "work");
const tempDir = path.join(baseDir, "temp");
const logDir = path.join(baseDir, "logs");
// Create directories
await fs.mkdir(workingDir, { recursive: true });
await fs.mkdir(tempDir, { recursive: true });
await fs.mkdir(logDir, { recursive: true });
return {
task,
agent,
workingDirectory: workingDir,
tempDirectory: tempDir,
logDirectory: logDir,
environment: {
NODE_ENV: "production",
SWARM_MODE: "execution",
AGENT_TYPE: agent.type,
TASK_TYPE: task.type,
...agent.environment.credentials,
},
resources: {
maxMemory: task.requirements.memoryRequired || SWARM_CONSTANTS.DEFAULT_MEMORY_LIMIT,
maxCpuTime: task.requirements.maxDuration || SWARM_CONSTANTS.DEFAULT_TASK_TIMEOUT,
maxDiskSpace: 1024 * 1024 * 1024, // 1GB
maxNetworkConnections: 10,
maxFileHandles: 100,
priority: this.getPriorityNumber(task.priority),
},
};
}
async cleanupExecution(session) {
try {
await session.cleanup();
this.logger.debug("Execution cleanup completed", { sessionId: session.id });
}
catch (error) {
this.logger.warn("Error during execution cleanup", {
sessionId: session.id,
error: error.message,
});
}
}
collectResourceUsage(sessionId) {
return Promise.resolve(this.resourceMonitor.getUsage(sessionId));
}
async collectArtifacts(context) {
const artifacts = {};
try {
// Scan working directory for artifacts
const files = await this.scanDirectory(context.workingDirectory);
artifacts.files = files;
// Check for specific artifact types
artifacts.logs = await this.collectLogs(context.logDirectory);
artifacts.outputs = await this.collectOutputs(context.workingDirectory);
}
catch (error) {
this.logger.warn("Error collecting artifacts", {
workingDir: context.workingDirectory,
error: error.message,
});
}
return artifacts;
}
async scanDirectory(dirPath) {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isFile()) {
files.push(fullPath);
}
else if (entry.isDirectory()) {
const subFiles = await this.scanDirectory(fullPath);
files.push(...subFiles);
}
}
return files;
}
catch (error) {
return [];
}
}
async collectLogs(logDir) {
const logs = {};
try {
const files = await fs.readdir(logDir);
for (const file of files) {
if (file.endsWith(".log")) {
const filePath = path.join(logDir, file);
const content = await fs.readFile(filePath, "utf-8");
logs[file] = content;
}
}
}
catch (error) {
// Log directory might not exist
}
return logs;
}
async collectOutputs(workingDir) {
const outputs = {};
try {
// Look for common output files
const outputFiles = ["output.json", "result.json", "response.json"];
for (const fileName of outputFiles) {
const filePath = path.join(workingDir, fileName);
try {
const content = await fs.readFile(filePath, "utf-8");
outputs[fileName] = JSON.parse(content);
}
catch (error) {
// File doesn't exist or isn't valid JSON
}
}
}
catch (error) {
// Working directory might not exist
}
return outputs;
}
getPriorityNumber(priority) {
switch (priority) {
case "critical": return 0;
case "high": return 1;
case "normal": return 2;
case "low": return 3;
case "background": return 4;
default: return 2;
}
}
mergeWithDefaults(config) {
return {
timeoutMs: SWARM_CONSTANTS.DEFAULT_TASK_TIMEOUT,
retryAttempts: SWARM_CONSTANTS.MAX_RETRIES,
killTimeout: 5000, // 5 seconds
resourceLimits: {
maxMemory: SWARM_CONSTANTS.DEFAULT_MEMORY_LIMIT,
maxCpuTime: SWARM_CONSTANTS.DEFAULT_TASK_TIMEOUT,
maxDiskSpace: 1024 * 1024 * 1024, // 1GB
maxNetworkConnections: 10,
maxFileHandles: 100,
priority: 2,
},
sandboxed: true,
logLevel: "info",
captureOutput: true,
streamOutput: false,
enableMetrics: true,
...config,
};
}
setupEventHandlers() {
// Handle resource limit violations
this.resourceMonitor.on("limit-violation", (data) => {
this.logger.warn("Resource limit violation", data);
const violationData = data;
const session = this.activeExecutions.get(violationData.sessionId);
if (session) {
session.stop("Resource limit violation").catch(error => {
this.logger.error("Error stopping session due to resource violation", {
sessionId: violationData.sessionId,
error: error.message,
});
});
}
});
// Handle process pool events
this.processPool.on("process-failed", (data) => {
this.logger.error("Process failed in pool", data);
});
}
}
// ===== SUPPORTING CLASSES =====
class ExecutionSession {
id;
task;
agent;
context;
config;
logger;
process;
startTime;
endTime;
constructor(id, task, agent, context, config, logger) {
this.id = id;
this.task = task;
this.agent = agent;
this.context = context;
this.config = config;
this.logger = logger;
}
async execute() {
this.startTime = new Date();
// Implementation would go here for actual task execution
// This is a placeholder that simulates execution
await new Promise(resolve => setTimeout(resolve, 1000));
this.endTime = new Date();
return {
success: true,
output: "Task completed successfully",
exitCode: 0,
duration: this.endTime.getTime() - this.startTime.getTime(),
resourcesUsed: {
cpuTime: 1000,
maxMemory: 50 * 1024 * 1024,
diskIO: 1024,
networkIO: 0,
fileHandles: 5,
},
artifacts: {},
metadata: {
sessionId: this.id,
agentId: this.agent.id.id,
taskId: this.task.id.id,
},
};
}
stop(reason) {
this.logger.info("Stopping execution session", { sessionId: this.id, reason });
if (this.process) {
this.process.kill("SIGTERM");
// Force kill after timeout
setTimeout(() => {
if (this.process && !this.process.killed) {
this.process.kill("SIGKILL");
}
}, 5000);
}
return Promise.resolve();
}
async cleanup() {
// Cleanup temporary files and resources
try {
await fs.rm(this.context.tempDirectory, { recursive: true, force: true });
}
catch (error) {
// Ignore cleanup errors
}
}
}
class ResourceMonitor extends EventEmitter {
activeMonitors = new Map();
usage = new Map();
async initialize() {
// Initialize resource monitoring
}
shutdown() {
// Stop all monitors
for (const [sessionId, timer] of this.activeMonitors) {
clearInterval(timer);
}
this.activeMonitors.clear();
return Promise.resolve();
}
startMonitoring(sessionId, limits) {
const timer = setInterval(() => {
this.checkResources(sessionId, limits);
}, 1000);
this.activeMonitors.set(sessionId, timer);
}
stopMonitoring(sessionId) {
const timer = this.activeMonitors.get(sessionId);
if (timer) {
clearInterval(timer);
this.activeMonitors.delete(sessionId);
}
}
getUsage(sessionId) {
return this.usage.get(sessionId) ?? {
cpuTime: 0,
maxMemory: 0,
diskIO: 0,
networkIO: 0,
fileHandles: 0,
};
}
getUtilization() {
// Return overall system utilization
return {
cpu: 0.1,
memory: 0.2,
disk: 0.05,
network: 0.01,
};
}
checkResources(sessionId, limits) {
// Check if any limits are exceeded
const usage = this.collectCurrentUsage(sessionId);
this.usage.set(sessionId, usage);
if (usage.maxMemory > limits.maxMemory) {
this.emit("limit-violation", {
sessionId,
type: "memory",
current: usage.maxMemory,
limit: limits.maxMemory,
});
}
if (usage.cpuTime > limits.maxCpuTime) {
this.emit("limit-violation", {
sessionId,
type: "cpu",
current: usage.cpuTime,
limit: limits.maxCpuTime,
});
}
}
collectCurrentUsage(sessionId) {
// Collect actual resource usage - this would interface with system APIs
return {
cpuTime: Math.random() * 1000,
maxMemory: Math.random() * 100 * 1024 * 1024,
diskIO: Math.random() * 1024,
networkIO: Math.random() * 1024,
fileHandles: Math.floor(Math.random() * 10),
};
}
}
class ProcessPool extends EventEmitter {
config;
totalExecutions = 0;
totalDuration = 0;
successCount = 0;
errorCount = 0;
constructor(config) {
super();
this.config = config;
}
async initialize() {
// Initialize process pool
}
async shutdown() {
// Shutdown process pool
}
getTotalExecutions() {
return this.totalExecutions;
}
getAverageDuration() {
return this.totalExecutions > 0 ? this.totalDuration / this.totalExecutions : 0;
}
getSuccessRate() {
return this.totalExecutions > 0 ? this.successCount / this.totalExecutions : 0;
}
getErrorRate() {
return this.totalExecutions > 0 ? this.errorCount / this.totalExecutions : 0;
}
}
export default TaskExecutor;
//# sourceMappingURL=executor.js.map