UNPKG

context-forge

Version:

AI orchestration platform with autonomous teams, enhancement planning, migration tools, 25+ slash commands, checkpoints & hooks. Multi-IDE: Claude, Cursor, Windsurf, Cline, Copilot

278 lines (270 loc) 10.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SelfSchedulingService = void 0; const events_1 = require("events"); const chalk_1 = __importDefault(require("chalk")); const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); const uuid_1 = require("uuid"); class SelfSchedulingService extends events_1.EventEmitter { constructor(config, tmux) { super(); this.schedules = new Map(); this.stats = { totalScheduled: 0, executed: 0, failed: 0, cancelled: 0, averageInterval: 0, adaptiveAdjustments: 0, }; this.workloadMetrics = new Map(); // agentId -> workload score this.config = config; this.tmux = tmux; } /** * Schedule next check-in for an agent */ async scheduleAgentCheckIn(agentSession, interval, note) { // Determine interval let checkInterval = interval || this.config.defaultCheckInterval; // Apply adaptive scheduling if enabled if (this.config.adaptiveScheduling) { checkInterval = this.calculateAdaptiveInterval(agentSession.agentId); } // Ensure within bounds checkInterval = Math.max(this.config.minCheckInterval, Math.min(this.config.maxCheckInterval, checkInterval)); const scheduleEntry = { id: (0, uuid_1.v4)(), agentId: agentSession.agentId, sessionName: agentSession.sessionName, windowIndex: agentSession.windowIndex, scheduledTime: new Date(Date.now() + checkInterval * 60 * 1000), interval: checkInterval, note: note || `Regular check-in for ${agentSession.agentId}`, status: 'pending', }; // Execute scheduling command const processId = await this.executeSchedule(scheduleEntry); if (processId) { scheduleEntry.processId = processId; } // Track schedule this.schedules.set(agentSession.agentId, scheduleEntry); this.stats.totalScheduled++; this.updateAverageInterval(); // Emit event this.emit('scheduled', scheduleEntry); return scheduleEntry; } /** * Calculate adaptive interval based on agent workload */ calculateAdaptiveInterval(agentId) { const workload = this.workloadMetrics.get(agentId) || 50; // 0-100 scale // High workload = shorter intervals, low workload = longer intervals const range = this.config.maxCheckInterval - this.config.minCheckInterval; const adaptedInterval = this.config.maxCheckInterval - (workload / 100) * range; this.stats.adaptiveAdjustments++; console.log(chalk_1.default.gray(`Adaptive scheduling for ${agentId}: workload=${workload}, interval=${Math.round(adaptedInterval)}min`)); return Math.round(adaptedInterval); } /** * Update agent workload metrics */ updateWorkloadMetrics(agentId, metrics) { // Calculate workload score (0-100) const workload = Math.min(100, metrics.tasksPending * 10 + metrics.blockers * 20 + metrics.messagesReceived * 2 - metrics.tasksCompleted * 5); this.workloadMetrics.set(agentId, Math.max(0, workload)); } /** * Execute schedule using scheduling script */ async executeSchedule(schedule) { try { // Create schedule command const scheduleScript = this.config.scheduleScript || path_1.default.join(process.cwd(), '.claude', 'orchestration', 'schedule.sh'); if (!(await fs_extra_1.default.pathExists(scheduleScript))) { throw new Error(`Schedule script not found: ${scheduleScript}`); } // Build command const target = `${schedule.sessionName}:${schedule.windowIndex}`; const command = `${scheduleScript} ${schedule.interval} "${schedule.note}" "${target}"`; // Execute in background const { stdout } = await this.tmux.executeShellCommand(command); // Extract PID from output if available const pidMatch = stdout.match(/PID:\s*(\d+)/); const pid = pidMatch ? parseInt(pidMatch[1]) : undefined; console.log(chalk_1.default.green(`✓ Scheduled check-in for ${schedule.agentId} in ${schedule.interval} minutes`)); return pid; } catch (error) { console.error(chalk_1.default.red(`Failed to schedule: ${error}`)); schedule.status = 'failed'; this.stats.failed++; this.emit('schedule-failed', { schedule, error }); return undefined; } } /** * Cancel a scheduled check-in */ async cancelSchedule(agentId) { const schedule = this.schedules.get(agentId); if (!schedule || schedule.status !== 'pending') { return false; } try { // Kill the scheduled process if we have PID if (schedule.processId) { await this.tmux.executeShellCommand(`kill ${schedule.processId}`); } schedule.status = 'cancelled'; this.stats.cancelled++; this.emit('cancelled', schedule); return true; } catch (error) { console.error(chalk_1.default.red(`Failed to cancel schedule: ${error}`)); return false; } } /** * Reschedule an agent with new interval */ async reschedule(agentSession, newInterval, note) { // Cancel existing schedule await this.cancelSchedule(agentSession.agentId); // Create new schedule return this.scheduleAgentCheckIn(agentSession, newInterval, note); } /** * Handle schedule execution (called when schedule fires) */ async handleScheduleExecution(agentId) { const schedule = this.schedules.get(agentId); if (!schedule) { return; } schedule.status = 'executed'; this.stats.executed++; // Emit event for orchestrator to handle this.emit('check-in', { agentId, scheduledTime: schedule.scheduledTime, actualTime: new Date(), note: schedule.note, }); // Log execution console.log(chalk_1.default.blue(`Agent ${agentId} check-in executed (scheduled: ${schedule.scheduledTime.toISOString()})`)); } /** * Get schedule for agent */ getAgentSchedule(agentId) { return this.schedules.get(agentId); } /** * Get all active schedules */ getActiveSchedules() { return Array.from(this.schedules.values()).filter((s) => s.status === 'pending'); } /** * Update average interval statistic */ updateAverageInterval() { const intervals = Array.from(this.schedules.values()).map((s) => s.interval); if (intervals.length > 0) { this.stats.averageInterval = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length; } } /** * Get scheduler statistics */ getStats() { return { ...this.stats }; } /** * Cancel all active schedules */ async cancelAllSchedules() { const activeSchedules = this.getActiveSchedules(); for (const schedule of activeSchedules) { await this.cancelSchedule(schedule.agentId); } console.log(chalk_1.default.yellow(`Cancelled all ${activeSchedules.length} active schedules`)); } /** * Create recovery schedule for crashed agent */ async createRecoverySchedule(agentSession, error) { const recoveryStrategy = this.config.recoveryStrategy; console.log(chalk_1.default.yellow(`Creating recovery schedule for ${agentSession.agentId} (strategy: ${recoveryStrategy})`)); switch (recoveryStrategy) { case 'restart': // Schedule immediate restart await this.scheduleAgentCheckIn(agentSession, 1, // 1 minute `Recovery restart after error: ${error.message}`); break; case 'resume': // Schedule with shorter interval await this.scheduleAgentCheckIn(agentSession, Math.min(5, this.config.minCheckInterval), `Resume after error: ${error.message}`); break; case 'escalate': // Don't reschedule, escalate to orchestrator this.emit('escalate-recovery', { agentSession, error, message: 'Agent requires manual intervention', }); break; } } /** * Generate scheduling script content */ generateSchedulingScript() { return `#!/bin/bash # Self-scheduling script for orchestration agents MINUTES=\${1:-${this.config.defaultCheckInterval}} NOTE=\${2:-"Regular orchestration check"} TARGET=\${3:-"cf-orchestrator:0"} # Validate interval if [ \$MINUTES -lt ${this.config.minCheckInterval} ]; then MINUTES=${this.config.minCheckInterval} elif [ \$MINUTES -gt ${this.config.maxCheckInterval} ]; then MINUTES=${this.config.maxCheckInterval} fi # Create note file ORCHESTRATION_DIR=".claude/orchestration" mkdir -p "\$ORCHESTRATION_DIR" echo "=== Next Check Note ($(date)) ===" > "\$ORCHESTRATION_DIR/next_check.txt" echo "Scheduled for: \$MINUTES minutes" >> "\$ORCHESTRATION_DIR/next_check.txt" echo "" >> "\$ORCHESTRATION_DIR/next_check.txt" echo "\$NOTE" >> "\$ORCHESTRATION_DIR/next_check.txt" # Calculate seconds SECONDS=\$(( \$MINUTES * 60 )) # Schedule the check using nohup for background execution nohup bash -c "sleep \$SECONDS && tmux send-keys -t \$TARGET 'Time for orchestration check! cat \$ORCHESTRATION_DIR/next_check.txt' && sleep 1 && tmux send-keys -t \$TARGET Enter" > /dev/null 2>&1 & # Get the PID SCHEDULE_PID=$! # Log the schedule echo "Scheduled check in \$MINUTES minutes (PID: \$SCHEDULE_PID)" echo "Target: \$TARGET" echo "Note: \$NOTE" # Save PID for potential cancellation echo "\$SCHEDULE_PID" > "\$ORCHESTRATION_DIR/schedule_\${TARGET//:/}_pid.txt" `; } } exports.SelfSchedulingService = SelfSchedulingService; //# sourceMappingURL=selfScheduler.js.map