shipdeck
Version:
Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.
550 lines (469 loc) • 14.7 kB
JavaScript
/**
* Task Orchestrator for Shipdeck Ultimate
* Routes tasks to appropriate agents and manages execution
*/
const { AgentExecutor, AGENT_ROLES } = require('../anthropic/agent-executor');
const EventEmitter = require('events');
const { logger } = require('./logger');
/**
* Task type to agent mapping
* Maps high-level task types to specific agents
*/
const TASK_AGENT_MAPPING = {
// Backend tasks
'api': 'backend-architect',
'database': 'backend-architect',
'auth': 'backend-architect',
'security': 'backend-architect',
'server': 'backend-architect',
'backend': 'backend-architect',
'microservice': 'backend-architect',
// Frontend tasks
'ui': 'frontend-developer',
'component': 'frontend-developer',
'react': 'frontend-developer',
'frontend': 'frontend-developer',
'interface': 'frontend-developer',
'dashboard': 'frontend-developer',
'form': 'frontend-developer',
// AI/ML tasks
'ai': 'ai-engineer',
'ml': 'ai-engineer',
'llm': 'ai-engineer',
'prompt': 'ai-engineer',
'intelligence': 'ai-engineer',
'chatbot': 'ai-engineer',
// Testing tasks
'test': 'test-writer-fixer',
'testing': 'test-writer-fixer',
'unittest': 'test-writer-fixer',
'integration': 'test-writer-fixer',
'coverage': 'test-writer-fixer',
// DevOps tasks
'deploy': 'devops-automator',
'deployment': 'devops-automator',
'ci': 'devops-automator',
'cd': 'devops-automator',
'docker': 'devops-automator',
'infrastructure': 'devops-automator',
'pipeline': 'devops-automator'
};
/**
* Task complexity estimation
*/
const COMPLEXITY_INDICATORS = {
simple: ['button', 'link', 'text', 'basic', 'simple', 'quick'],
medium: ['form', 'table', 'chart', 'integration', 'auth', 'crud'],
complex: ['dashboard', 'analytics', 'workflow', 'payment', 'system', 'architecture']
};
class TaskOrchestrator extends EventEmitter {
constructor(options = {}) {
super();
this.agentExecutor = new AgentExecutor(options.agentExecutor);
this.config = {
defaultAgent: 'backend-architect',
maxRetries: 2,
retryDelay: 1000,
timeout: 300000, // 5 minutes default
enableLogging: true,
...options.config
};
this.executionHistory = new Map();
this.activeExecutions = new Map();
if (this.config.enableLogging) {
this.setupLogging();
}
}
/**
* Setup execution logging
*/
setupLogging() {
this.on('task:started', (data) => {
logger.info(`Task started: ${data.taskType} (Agent: ${data.agent})`);
});
this.on('task:completed', (data) => {
logger.info(`Task completed: ${data.taskType} in ${data.duration}ms`);
});
this.on('task:failed', (data) => {
console.error(`❌ Task failed: ${data.taskType} - ${data.error}`);
});
this.on('task:retry', (data) => {
logger.info(`Retrying task: ${data.taskType} (Attempt ${data.attempt}/${data.maxRetries})`);
});
}
/**
* Execute a task by routing it to the appropriate agent
* @param {string} taskType - Type of task (e.g., 'api', 'ui', 'test')
* @param {Object} params - Task parameters
* @param {string} params.description - Task description/prompt
* @param {Object} params.context - Additional context
* @param {Object} options - Execution options
* @returns {Object} Execution result
*/
async executeTask(taskType, params = {}, options = {}) {
const startTime = Date.now();
const executionId = this.generateExecutionId();
// Validate inputs
if (!taskType || typeof taskType !== 'string') {
throw new Error('Task type is required and must be a string');
}
if (!params.description) {
throw new Error('Task description is required');
}
// Determine agent
const agent = this.selectAgent(taskType, params);
const complexity = this.estimateComplexity(params.description);
// Prepare execution context
const executionContext = {
id: executionId,
taskType,
agent,
complexity,
params,
options: {
...this.config,
...options
},
startTime,
attempts: 0
};
this.activeExecutions.set(executionId, executionContext);
try {
this.emit('task:started', {
id: executionId,
taskType,
agent,
complexity,
description: params.description
});
const result = await this.executeWithRetry(executionContext);
const duration = Date.now() - startTime;
const executionResult = {
success: true,
result: result.content?.[0]?.text || result,
agent,
taskType,
complexity,
duration,
metadata: {
...result.metadata,
executionId,
hasTests: result.hasTests || false
},
tests: result.tests
};
// Store in history
this.executionHistory.set(executionId, {
...executionResult,
params,
timestamp: new Date().toISOString()
});
this.emit('task:completed', {
id: executionId,
taskType,
agent,
duration,
hasTests: executionResult.metadata.hasTests
});
return executionResult;
} catch (error) {
const duration = Date.now() - startTime;
const executionResult = {
success: false,
error: error.message,
agent,
taskType,
complexity,
duration,
metadata: {
executionId,
attempts: executionContext.attempts
}
};
// Store in history
this.executionHistory.set(executionId, {
...executionResult,
params,
timestamp: new Date().toISOString()
});
this.emit('task:failed', {
id: executionId,
taskType,
agent,
error: error.message,
duration
});
return executionResult;
} finally {
this.activeExecutions.delete(executionId);
}
}
/**
* Execute task with retry logic
*/
async executeWithRetry(executionContext) {
const { taskType, agent, params, options } = executionContext;
let lastError;
for (let attempt = 1; attempt <= options.maxRetries + 1; attempt++) {
executionContext.attempts = attempt;
try {
// Add retry context to prompt if this is a retry
let prompt = params.description;
if (attempt > 1) {
prompt = `${prompt}\n\nNote: This is retry attempt ${attempt}. Previous attempt failed with: ${lastError?.message}`;
}
// Add context if provided
if (params.context) {
prompt = `Context: ${JSON.stringify(params.context, null, 2)}\n\nTask: ${prompt}`;
}
// Execute with timeout
const result = await this.executeWithTimeout(
() => this.agentExecutor.executeAgent(agent, prompt, {
sessionId: executionContext.id,
...options.agentOptions
}),
options.timeout
);
return result;
} catch (error) {
lastError = error;
if (attempt <= options.maxRetries) {
this.emit('task:retry', {
id: executionContext.id,
taskType,
agent,
attempt,
maxRetries: options.maxRetries,
error: error.message
});
// Wait before retry
await this.sleep(options.retryDelay * attempt);
}
}
}
throw lastError;
}
/**
* Execute with timeout wrapper
*/
async executeWithTimeout(promise, timeoutMs) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Task timed out after ${timeoutMs}ms`)), timeoutMs);
});
return Promise.race([promise(), timeout]);
}
/**
* Select appropriate agent based on task type and parameters
* @param {string} taskType - The task type
* @param {Object} params - Task parameters
* @returns {string} Agent type
*/
selectAgent(taskType, params) {
// Direct mapping first
if (TASK_AGENT_MAPPING[taskType.toLowerCase()]) {
return TASK_AGENT_MAPPING[taskType.toLowerCase()];
}
// Content-based detection
const description = params.description.toLowerCase();
// Check for keywords in description
for (const [keywords, agent] of Object.entries(TASK_AGENT_MAPPING)) {
if (description.includes(keywords)) {
return agent;
}
}
// Pattern-based detection
if (description.includes('test') || description.includes('spec')) {
return 'test-writer-fixer';
}
if (description.includes('deploy') || description.includes('build')) {
return 'devops-automator';
}
if (description.includes('component') || description.includes('ui') || description.includes('interface')) {
return 'frontend-developer';
}
if (description.includes('api') || description.includes('server') || description.includes('database')) {
return 'backend-architect';
}
if (description.includes('ai') || description.includes('intelligent') || description.includes('llm')) {
return 'ai-engineer';
}
// Default fallback
return this.config.defaultAgent;
}
/**
* Estimate task complexity
* @param {string} description - Task description
* @returns {string} Complexity level
*/
estimateComplexity(description) {
const desc = description.toLowerCase();
// Check for complexity indicators
for (const [level, indicators] of Object.entries(COMPLEXITY_INDICATORS)) {
if (indicators.some(indicator => desc.includes(indicator))) {
return level;
}
}
// Length-based estimation
if (desc.length < 50) return 'simple';
if (desc.length < 200) return 'medium';
return 'complex';
}
/**
* Execute multiple tasks sequentially
* @param {Array} tasks - Array of task definitions
* @returns {Array} Array of execution results
*/
async executeTasks(tasks) {
const results = [];
for (const task of tasks) {
try {
const result = await this.executeTask(
task.type,
task.params,
task.options
);
results.push(result);
} catch (error) {
results.push({
success: false,
error: error.message,
taskType: task.type
});
}
}
return results;
}
/**
* Get available agents
* @returns {Array} List of available agents
*/
getAvailableAgents() {
return Object.keys(AGENT_ROLES).map(agentType => ({
type: agentType,
description: AGENT_ROLES[agentType].systemPrompt.split('\n')[0],
config: {
temperature: AGENT_ROLES[agentType].temperature,
maxTokens: AGENT_ROLES[agentType].maxTokens
}
}));
}
/**
* Get task execution history
* @param {Object} filters - Optional filters
* @returns {Array} Execution history
*/
getExecutionHistory(filters = {}) {
let history = Array.from(this.executionHistory.values());
if (filters.agent) {
history = history.filter(h => h.agent === filters.agent);
}
if (filters.taskType) {
history = history.filter(h => h.taskType === filters.taskType);
}
if (filters.success !== undefined) {
history = history.filter(h => h.success === filters.success);
}
if (filters.limit) {
history = history.slice(-filters.limit);
}
return history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
}
/**
* Get active executions
* @returns {Array} Currently running executions
*/
getActiveExecutions() {
return Array.from(this.activeExecutions.values()).map(ctx => ({
id: ctx.id,
taskType: ctx.taskType,
agent: ctx.agent,
complexity: ctx.complexity,
attempts: ctx.attempts,
duration: Date.now() - ctx.startTime
}));
}
/**
* Get execution statistics
* @returns {Object} Statistics
*/
getStats() {
const history = Array.from(this.executionHistory.values());
const successful = history.filter(h => h.success);
const failed = history.filter(h => !h.success);
const agentStats = {};
const taskTypeStats = {};
for (const execution of history) {
// Agent stats
if (!agentStats[execution.agent]) {
agentStats[execution.agent] = { total: 0, successful: 0, failed: 0 };
}
agentStats[execution.agent].total++;
if (execution.success) {
agentStats[execution.agent].successful++;
} else {
agentStats[execution.agent].failed++;
}
// Task type stats
if (!taskTypeStats[execution.taskType]) {
taskTypeStats[execution.taskType] = { total: 0, successful: 0, failed: 0 };
}
taskTypeStats[execution.taskType].total++;
if (execution.success) {
taskTypeStats[execution.taskType].successful++;
} else {
taskTypeStats[execution.taskType].failed++;
}
}
return {
total: history.length,
successful: successful.length,
failed: failed.length,
successRate: history.length > 0 ? (successful.length / history.length) * 100 : 0,
averageDuration: history.length > 0 ?
history.reduce((sum, h) => sum + h.duration, 0) / history.length : 0,
agentStats,
taskTypeStats,
activeExecutions: this.activeExecutions.size
};
}
/**
* Clear execution history
*/
clearHistory() {
this.executionHistory.clear();
}
/**
* Generate unique execution ID
*/
generateExecutionId() {
return `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Sleep utility for retry delays
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Shutdown orchestrator
*/
async shutdown() {
logger.info('Shutting down task orchestrator...');
// Wait for active executions to complete (with timeout)
const activeIds = Array.from(this.activeExecutions.keys());
if (activeIds.length > 0) {
logger.info(`Waiting for ${activeIds.length} active executions to complete...`);
// Wait up to 30 seconds for executions to complete
let waited = 0;
while (this.activeExecutions.size > 0 && waited < 30000) {
await this.sleep(1000);
waited += 1000;
}
if (this.activeExecutions.size > 0) {
console.log(`⚠️ ${this.activeExecutions.size} executions did not complete in time`);
}
}
console.log('✅ Task orchestrator shutdown complete');
}
}
module.exports = { TaskOrchestrator, TASK_AGENT_MAPPING, COMPLEXITY_INDICATORS };