UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

391 lines (321 loc) 10.7 kB
/** * Task scheduler implementation */ import { Task, TaskStatus, CoordinationConfig, SystemEvents } from '../utils/types.js'; import type { IEventBus } from '../core/event-bus.js'; import type { ILogger } from '../core/logger.js'; import { TaskError, TaskTimeoutError, TaskDependencyError } from '../utils/errors.js'; import { delay } from '../utils/helpers.js'; interface ScheduledTask { task: Task; agentId: string; attempts: number; lastAttempt?: Date; timeout?: number; } /** * Task scheduler for managing task assignment and execution */ export class TaskScheduler { protected tasks = new Map<string, ScheduledTask>(); protected agentTasks = new Map<string, Set<string>>(); // agentId -> taskIds protected taskDependencies = new Map<string, Set<string>>(); // taskId -> dependent taskIds protected completedTasks = new Set<string>(); constructor( protected config: CoordinationConfig, protected eventBus: IEventBus, protected logger: ILogger, ) {} async initialize(): Promise<void> { this.logger.info('Initializing task scheduler'); // Set up periodic cleanup setInterval(() => this.cleanup(), 60000); // Every minute } async shutdown(): Promise<void> { this.logger.info('Shutting down task scheduler'); // Cancel all active tasks const taskIds = Array.from(this.tasks.keys()); await Promise.all(taskIds.map((id) => this.cancelTask(id, 'Scheduler shutdown'))); this.tasks.clear(); this.agentTasks.clear(); this.taskDependencies.clear(); this.completedTasks.clear(); } async assignTask(task: Task, agentId: string): Promise<void> { this.logger.info('Assigning task', { taskId: task.id, agentId }); // Check dependencies if (task.dependencies.length > 0) { const unmetDependencies = task.dependencies.filter( (depId) => !this.completedTasks.has(depId), ); if (unmetDependencies.length > 0) { throw new TaskDependencyError(task.id, unmetDependencies); } } // Create scheduled task const scheduledTask: ScheduledTask = { task: { ...task, status: 'assigned', assignedAgent: agentId }, agentId, attempts: 0, }; // Store task this.tasks.set(task.id, scheduledTask); // Update agent tasks if (!this.agentTasks.has(agentId)) { this.agentTasks.set(agentId, new Set()); } this.agentTasks.get(agentId)!.add(task.id); // Update dependencies for (const depId of task.dependencies) { if (!this.taskDependencies.has(depId)) { this.taskDependencies.set(depId, new Set()); } this.taskDependencies.get(depId)!.add(task.id); } // Start task execution this.startTask(task.id); } async completeTask(taskId: string, result: unknown): Promise<void> { const scheduled = this.tasks.get(taskId); if (!scheduled) { throw new TaskError(`Task not found: ${taskId}`); } this.logger.info('Task completed', { taskId, agentId: scheduled.agentId }); // Update task status scheduled.task.status = 'completed'; scheduled.task.output = result as Record<string, unknown>; scheduled.task.completedAt = new Date(); // Clear timeout if (scheduled.timeout) { clearTimeout(scheduled.timeout); } // Remove from active tasks this.tasks.delete(taskId); this.agentTasks.get(scheduled.agentId)?.delete(taskId); // Add to completed tasks this.completedTasks.add(taskId); // Check and start dependent tasks const dependents = this.taskDependencies.get(taskId); if (dependents) { for (const dependentId of dependents) { const dependent = this.tasks.get(dependentId); if (dependent && this.canStartTask(dependent.task)) { this.startTask(dependentId); } } } } async failTask(taskId: string, error: Error): Promise<void> { const scheduled = this.tasks.get(taskId); if (!scheduled) { throw new TaskError(`Task not found: ${taskId}`); } this.logger.error('Task failed', { taskId, agentId: scheduled.agentId, attempt: scheduled.attempts, error, }); // Clear timeout if (scheduled.timeout) { clearTimeout(scheduled.timeout); } scheduled.attempts++; scheduled.lastAttempt = new Date(); // Check if we should retry if (scheduled.attempts < this.config.maxRetries) { this.logger.info('Retrying task', { taskId, attempt: scheduled.attempts, maxRetries: this.config.maxRetries, }); // Schedule retry with exponential backoff const retryDelay = this.config.retryDelay * Math.pow(2, scheduled.attempts - 1); setTimeout(() => { this.startTask(taskId); }, retryDelay); } else { // Max retries exceeded, mark as failed scheduled.task.status = 'failed'; scheduled.task.error = error; scheduled.task.completedAt = new Date(); // Remove from active tasks this.tasks.delete(taskId); this.agentTasks.get(scheduled.agentId)?.delete(taskId); // Cancel dependent tasks await this.cancelDependentTasks(taskId, 'Parent task failed'); } } async cancelTask(taskId: string, reason: string): Promise<void> { const scheduled = this.tasks.get(taskId); if (!scheduled) { return; // Already cancelled or completed } this.logger.info('Cancelling task', { taskId, reason }); // Clear timeout if (scheduled.timeout) { clearTimeout(scheduled.timeout); } // Update task status scheduled.task.status = 'cancelled'; scheduled.task.completedAt = new Date(); // Emit cancellation event this.eventBus.emit(SystemEvents.TASK_CANCELLED, { taskId, reason }); // Remove from active tasks this.tasks.delete(taskId); this.agentTasks.get(scheduled.agentId)?.delete(taskId); // Cancel dependent tasks await this.cancelDependentTasks(taskId, 'Parent task cancelled'); } async cancelAgentTasks(agentId: string): Promise<void> { const taskIds = this.agentTasks.get(agentId); if (!taskIds) { return; } this.logger.info('Cancelling all tasks for agent', { agentId, taskCount: taskIds.size, }); const promises = Array.from(taskIds).map((taskId) => this.cancelTask(taskId, 'Agent terminated'), ); await Promise.all(promises); this.agentTasks.delete(agentId); } async rescheduleAgentTasks(agentId: string): Promise<void> { const taskIds = this.agentTasks.get(agentId); if (!taskIds || taskIds.size === 0) { return; } this.logger.info('Rescheduling tasks for agent', { agentId, taskCount: taskIds.size, }); for (const taskId of taskIds) { const scheduled = this.tasks.get(taskId); if (scheduled && scheduled.task.status === 'running') { // Reset task status scheduled.task.status = 'queued'; scheduled.attempts = 0; // Re-emit task created event for reassignment this.eventBus.emit(SystemEvents.TASK_CREATED, { task: scheduled.task, }); } } } getAgentTaskCount(agentId: string): number { return this.agentTasks.get(agentId)?.size || 0; } async getHealthStatus(): Promise<{ healthy: boolean; error?: string; metrics?: Record<string, number>; }> { const activeTasks = this.tasks.size; const completedTasks = this.completedTasks.size; const agentsWithTasks = this.agentTasks.size; const tasksByStatus: Record<TaskStatus, number> = { pending: 0, queued: 0, assigned: 0, running: 0, completed: completedTasks, failed: 0, cancelled: 0, }; for (const scheduled of this.tasks.values()) { tasksByStatus[scheduled.task.status]++; } return { healthy: true, metrics: { activeTasks, completedTasks, agentsWithTasks, ...tasksByStatus, }, }; } async getAgentTasks(agentId: string): Promise<Task[]> { const taskIds = this.agentTasks.get(agentId); if (!taskIds) { return []; } const tasks: Task[] = []; for (const taskId of taskIds) { const scheduled = this.tasks.get(taskId); if (scheduled) { tasks.push(scheduled.task); } } return tasks; } async performMaintenance(): Promise<void> { this.logger.debug('Performing task scheduler maintenance'); // Cleanup old completed tasks this.cleanup(); // Check for stuck tasks const now = new Date(); for (const [taskId, scheduled] of this.tasks) { if (scheduled.task.status === 'running' && scheduled.task.startedAt) { const runtime = now.getTime() - scheduled.task.startedAt.getTime(); if (runtime > this.config.resourceTimeout * 2) { this.logger.warn('Found stuck task', { taskId, runtime, agentId: scheduled.agentId, }); // Force fail the task await this.failTask(taskId, new TaskTimeoutError(taskId, runtime)); } } } } private startTask(taskId: string): void { const scheduled = this.tasks.get(taskId); if (!scheduled) { return; } // Update status scheduled.task.status = 'running'; scheduled.task.startedAt = new Date(); // Emit task started event this.eventBus.emit(SystemEvents.TASK_STARTED, { taskId, agentId: scheduled.agentId, }); // Set timeout for task execution const timeoutMs = this.config.resourceTimeout; scheduled.timeout = setTimeout(() => { this.failTask(taskId, new TaskTimeoutError(taskId, timeoutMs)); }, timeoutMs); } private canStartTask(task: Task): boolean { // Check if all dependencies are completed return task.dependencies.every((depId) => this.completedTasks.has(depId)); } private async cancelDependentTasks(taskId: string, reason: string): Promise<void> { const dependents = this.taskDependencies.get(taskId); if (!dependents) { return; } for (const dependentId of dependents) { await this.cancelTask(dependentId, reason); } } private cleanup(): void { // Clean up old completed tasks (keep last 1000) if (this.completedTasks.size > 1000) { const toRemove = this.completedTasks.size - 1000; const iterator = this.completedTasks.values(); for (let i = 0; i < toRemove; i++) { const result = iterator.next(); if (!result.done && result.value) { this.completedTasks.delete(result.value); this.taskDependencies.delete(result.value); } } } } }