devflow-ai
Version:
Enterprise-grade AI agent orchestration with swarm management UI dashboard
426 lines (371 loc) • 12.5 kB
text/typescript
/**
* Enhanced Task Executor v2.0 with improved environment handling
*/
import { spawn, ChildProcess } 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 chalk from 'chalk';
import { Logger } from '../core/logger.js';
import { generateId } from '../utils/helpers.js';
import {
detectExecutionEnvironment,
applySmartDefaults,
} from '../cli/utils/environment-detector.js';
import {
TaskDefinition,
AgentState,
TaskResult,
SwarmEvent,
EventType,
SWARM_CONSTANTS,
} from './types.js';
export interface ClaudeExecutionOptionsV2 extends ClaudeExecutionOptions {
nonInteractive?: boolean;
autoApprove?: boolean;
promptDefaults?: Record<string, any>;
environmentOverride?: Record<string, string>;
retryOnInteractiveError?: boolean;
}
export class TaskExecutorV2 extends TaskExecutor {
private environment = detectExecutionEnvironment();
constructor(config: Partial<ExecutionConfig> = {}) {
super(config);
// Log environment info on initialization
this.logger.info('Task Executor v2.0 initialized', {
environment: this.environment.terminalType,
interactive: this.environment.isInteractive,
recommendations: this.environment.recommendedFlags,
});
}
async executeClaudeTask(
task: TaskDefinition,
agent: AgentState,
claudeOptions: ClaudeExecutionOptionsV2 = {},
): Promise<ExecutionResult> {
// Apply smart defaults based on environment
const enhancedOptions = applySmartDefaults(claudeOptions, this.environment);
// Log if defaults were applied
if (enhancedOptions.appliedDefaults.length > 0) {
this.logger.info('Applied environment-based defaults', {
defaults: enhancedOptions.appliedDefaults,
environment: this.environment.terminalType,
});
}
try {
return await this.executeClaudeWithTimeoutV2(
generateId('claude-execution'),
task,
agent,
await this.createExecutionContext(task, agent),
enhancedOptions,
);
} catch (error) {
// Handle interactive errors with retry
if (this.isInteractiveError(error) && enhancedOptions.retryOnInteractiveError) {
this.logger.warn('Interactive error detected, retrying with non-interactive mode', {
error: error.message,
});
// Force non-interactive mode and retry
enhancedOptions.nonInteractive = true;
enhancedOptions.dangerouslySkipPermissions = true;
return await this.executeClaudeWithTimeoutV2(
generateId('claude-execution-retry'),
task,
agent,
await this.createExecutionContext(task, agent),
enhancedOptions,
);
}
throw error;
}
}
private async executeClaudeWithTimeoutV2(
sessionId: string,
task: TaskDefinition,
agent: AgentState,
context: ExecutionContext,
options: ClaudeExecutionOptionsV2,
): Promise<ExecutionResult> {
const startTime = Date.now();
const timeout = options.timeout || this.config.timeoutMs;
// Build Claude command with v2 enhancements
const command = this.buildClaudeCommandV2(task, agent, options);
// Create execution environment with enhancements
const env = {
...process.env,
...context.environment,
...options.environmentOverride,
CLAUDE_TASK_ID: task.id.id,
CLAUDE_AGENT_ID: agent.id.id,
CLAUDE_SESSION_ID: sessionId,
CLAUDE_WORKING_DIR: context.workingDirectory,
CLAUDE_NON_INTERACTIVE: options.nonInteractive ? '1' : '0',
CLAUDE_AUTO_APPROVE: options.autoApprove ? '1' : '0',
};
// Add prompt defaults if provided
if (options.promptDefaults) {
env.CLAUDE_PROMPT_DEFAULTS = JSON.stringify(options.promptDefaults);
}
this.logger.debug('Executing Claude command v2', {
sessionId,
command: command.command,
args: command.args,
workingDir: context.workingDirectory,
nonInteractive: options.nonInteractive,
environment: this.environment.terminalType,
});
return new Promise((resolve, reject) => {
let outputBuffer = '';
let errorBuffer = '';
let isTimeout = false;
let process: ChildProcess | null = null;
// Setup timeout
const timeoutHandle = setTimeout(() => {
isTimeout = true;
if (process) {
this.logger.warn('Claude execution timeout, killing process', {
sessionId,
pid: process.pid,
timeout,
});
process.kill('SIGTERM');
setTimeout(() => {
if (process && !process.killed) {
process.kill('SIGKILL');
}
}, this.config.killTimeout);
}
}, timeout);
try {
// Spawn Claude process with enhanced options
process = spawn(command.command, command.args, {
cwd: context.workingDirectory,
env,
stdio: options.nonInteractive ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],
detached: options.detached || false,
// Disable shell to avoid shell-specific issues
shell: false,
});
if (!process.pid) {
clearTimeout(timeoutHandle);
reject(new Error('Failed to spawn Claude process'));
return;
}
this.logger.info('Claude process started (v2)', {
sessionId,
pid: process.pid,
command: command.command,
mode: options.nonInteractive ? 'non-interactive' : 'interactive',
});
// Handle process output
if (process.stdout) {
process.stdout.on('data', (data: Buffer) => {
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: Buffer) => {
const chunk = data.toString();
errorBuffer += chunk;
// Check for interactive mode errors
if (this.isInteractiveErrorMessage(chunk)) {
this.logger.warn('Interactive mode error detected in stderr', {
sessionId,
error: chunk.trim(),
});
}
if (this.config.streamOutput) {
this.emit('output', {
sessionId,
type: 'stderr',
data: chunk,
});
}
});
}
// Handle process errors
process.on('error', (error: Error) => {
clearTimeout(timeoutHandle);
this.logger.error('Process error', {
sessionId,
error: error.message,
code: (error as any).code,
});
reject(error);
});
// Handle process completion
process.on('close', async (code: number | null, signal: string | null) => {
clearTimeout(timeoutHandle);
const duration = Date.now() - startTime;
const exitCode = code || 0;
this.logger.info('Claude process completed (v2)', {
sessionId,
exitCode,
signal,
duration,
isTimeout,
hasErrors: errorBuffer.length > 0,
});
try {
// Collect resource usage
const resourceUsage = await this.collectResourceUsage(sessionId);
// Collect artifacts
const artifacts = await this.collectArtifacts(context);
const result: ExecutionResult = {
success: !isTimeout && exitCode === 0,
output: outputBuffer,
error: errorBuffer,
exitCode,
duration,
resourcesUsed: resourceUsage,
artifacts,
metadata: {
environment: this.environment.terminalType,
nonInteractive: options.nonInteractive || false,
appliedDefaults: (options as any).appliedDefaults || [],
},
};
if (isTimeout) {
reject(new Error(`Execution timed out after ${timeout}ms`));
} else if (exitCode !== 0 && this.isInteractiveErrorMessage(errorBuffer)) {
reject(new Error(`Interactive mode error: ${errorBuffer.trim()}`));
} else {
resolve(result);
}
} catch (collectionError) {
this.logger.error('Error collecting execution results', {
sessionId,
error: collectionError.message,
});
// Still resolve with basic result
resolve({
success: !isTimeout && exitCode === 0,
output: outputBuffer,
error: errorBuffer,
exitCode,
duration,
resourcesUsed: this.getDefaultResourceUsage(),
artifacts: {},
metadata: {},
});
}
});
} catch (spawnError) {
clearTimeout(timeoutHandle);
this.logger.error('Failed to spawn process', {
sessionId,
error: spawnError.message,
});
reject(spawnError);
}
});
}
private buildClaudeCommandV2(
task: TaskDefinition,
agent: AgentState,
options: ClaudeExecutionOptionsV2,
): ClaudeCommand {
const args: string[] = [];
let input = '';
// Build prompt
const prompt = this.buildClaudePrompt(task, agent);
if (options.useStdin) {
input = prompt;
} else {
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 non-interactive environments
if (
options.nonInteractive ||
options.dangerouslySkipPermissions ||
this.environment.recommendedFlags.includes('--dangerously-skip-permissions')
) {
args.push('--dangerously-skip-permissions');
}
// Add non-interactive flag if needed
if (options.nonInteractive) {
args.push('--non-interactive');
}
// Add auto-approve if specified
if (options.autoApprove) {
args.push('--auto-approve');
}
// Add output format
if (options.outputFormat) {
args.push('--output-format', options.outputFormat);
} else if (options.nonInteractive) {
// Default to JSON for non-interactive mode
args.push('--output-format', 'json');
}
// Add environment info for debugging
args.push(
'--metadata',
JSON.stringify({
environment: this.environment.terminalType,
interactive: this.environment.isInteractive,
executor: 'v2',
}),
);
return {
command: options.claudePath || 'claude',
args,
input,
};
}
private isInteractiveError(error: any): boolean {
if (!(error instanceof Error)) return false;
const errorMessage = error.message.toLowerCase();
return (
errorMessage.includes('raw mode') ||
errorMessage.includes('stdin') ||
errorMessage.includes('interactive') ||
errorMessage.includes('tty') ||
errorMessage.includes('terminal')
);
}
private isInteractiveErrorMessage(message: string): boolean {
const lowerMessage = message.toLowerCase();
return (
lowerMessage.includes('raw mode is not supported') ||
lowerMessage.includes('stdin is not a tty') ||
lowerMessage.includes('requires interactive terminal') ||
lowerMessage.includes('manual ui agreement needed')
);
}
private getDefaultResourceUsage(): ResourceUsage {
return {
cpuTime: 0,
maxMemory: 0,
diskIO: 0,
networkIO: 0,
fileHandles: 0,
};
}
}
export default TaskExecutorV2;