UNPKG

mega-minds

Version:

Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration

1,193 lines (1,009 loc) 46.1 kB
// lib/memory/AgentStateTracker.js const fs = require('fs-extra'); const path = require('path'); /** * AgentStateTracker - Tracks what agents are doing, their progress, and coordination state * This enables the AI dev team to know who's working on what and coordinate handoffs */ class AgentStateTracker { constructor(projectPath) { this.projectPath = projectPath; this.memoryPath = path.join(projectPath, '.mega-minds'); this.agentStateFile = path.join(this.memoryPath, 'agents', 'state.json'); this.agentHistoryFile = path.join(this.memoryPath, 'agents', 'history.json'); this.initializeAgentState(); } async initializeAgentState() { await fs.ensureDir(path.join(this.memoryPath, 'agents')); // Initialize empty state if doesn't exist if (!await fs.pathExists(this.agentStateFile)) { await this.saveAgentState({ activeAgents: {}, completedTasks: [], handoffQueue: [], lastUpdate: new Date().toISOString() }); } } /** * Initialize the agent state system */ async initialize() { await this.initializeAgentState(); // Use existing method console.log('🤖 Agent state tracking initialized'); } /** * Get all current agent states */ async getAllAgentStates() { const state = await this.loadAgentState(); return state.activeAgents || {}; } /** * Restore a single agent state * @param {string} agentName - Name of the agent * @param {object} agentData - Agent state data to restore */ async restoreAgentState(agentName, agentData) { const state = await this.loadAgentState(); // Restore the agent with the provided data state.activeAgents[agentName] = { ...agentData, status: agentData.status || 'active', lastUpdate: new Date().toISOString(), restored: true }; state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`🔄 Restored agent state: ${agentName}`); } /** * Restore agent states from a previous session */ async restoreFromSession(sessionData) { if (sessionData && sessionData.agentStates) { const currentState = await this.loadAgentState(); // Merge session agent states with current state currentState.activeAgents = { ...currentState.activeAgents, ...sessionData.agentStates }; await this.saveAgentState(currentState); console.log('🔄 Agent states restored from previous session'); } } async activateAgent(agentName, task, context = {}) { const state = await this.loadAgentState(); // CRITICAL: Check current active agent count const currentActiveCount = Object.keys(state.activeAgents).length; const MAX_AGENTS = 2; // Hard-coded safety limit if (currentActiveCount >= MAX_AGENTS) { throw new Error(`🚨 AGENT LIMIT REACHED: Cannot activate ${agentName}. Currently ${currentActiveCount}/${MAX_AGENTS} agents active. Deactivate agents first or save session.`); } state.activeAgents[agentName] = { taskId: this.generateTaskId(), task: task, status: 'active', startTime: new Date().toISOString(), context: context, progress: 0, blockedOn: null, dependencies: context.dependencies || [], estimatedCompletion: context.estimatedCompletion || null }; state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`🤖 ${agentName} activated for: ${task} (${currentActiveCount + 1}/${MAX_AGENTS} active)`); return state.activeAgents[agentName].taskId; } /** * ENHANCED: Force deactivate agent to free memory - now with handoff handling */ async forceDeactivateAgent(agentName, reason = "Memory limit enforcement", options = {}) { const { handleHandoffsFirst = true, maxHandoffWaitTime = 10000, // 10 seconds for force mode preserveWork = true } = options; console.log(`🚨 Force deactivating ${agentName}: ${reason}`); // First attempt: Try graceful deactivation with short timeout if handoff handling enabled if (handleHandoffsFirst) { console.log(`⏳ Attempting quick handoff resolution before force deactivation...`); try { const gracefulResult = await this.gracefulDeactivateAgent(agentName, reason, { forceAfterTimeout: maxHandoffWaitTime, waitForHandoffs: true, transferToAgent: null, // Don't transfer in force mode preserveWork: preserveWork }); if (gracefulResult.success) { console.log(`✅ Force deactivation completed gracefully for ${agentName}`); return { success: true, method: 'graceful', handoffsSummary: gracefulResult.handoffsSummary, message: `${agentName} force deactivated gracefully` }; } console.log(`⚠️ Graceful deactivation failed, proceeding with immediate force deactivation`); } catch (error) { console.warn(`⚠️ Graceful deactivation attempt failed:`, error.message); } } // Force deactivation: Handle handoffs abruptly const dependencyCheck = await this.checkAgentHandoffDependencies(agentName); if (dependencyCheck.pendingHandoffs.length > 0) { console.log(`🚨 Force deactivating ${agentName} with ${dependencyCheck.pendingHandoffs.length} pending handoffs`); await this.forceCompleteHandoffs(agentName, dependencyCheck.pendingHandoffs, reason); } // Preserve work if requested if (preserveWork) { await this.preserveAgentWork(agentName, `Force deactivation: ${reason}`); } // Perform immediate deactivation const state = await this.loadAgentState(); if (state.activeAgents[agentName]) { const completedTask = { ...state.activeAgents[agentName], endTime: new Date().toISOString(), completionReason: reason, status: 'force_completed', handoffsSummary: { blocking: dependencyCheck.blockingHandoffs.length, orphaned: dependencyCheck.orphanedHandoffs.length, total: dependencyCheck.pendingHandoffs.length, forceClosed: dependencyCheck.pendingHandoffs.length } }; state.completedTasks.push(completedTask); delete state.activeAgents[agentName]; state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`⚠️ Force deactivated ${agentName}: ${reason}`); return { success: true, method: 'force', handoffsSummary: completedTask.handoffsSummary, message: `${agentName} force deactivated immediately with ${dependencyCheck.pendingHandoffs.length} handoffs closed` }; } return { success: false, method: 'none', message: `${agentName} was not in active state` }; } /** * Force complete all handoffs for an agent during emergency deactivation * @param {string} agentName - Name of the agent being deactivated * @param {array} handoffs - List of handoffs to force complete * @param {string} reason - Reason for force completion */ async forceCompleteHandoffs(agentName, handoffs, reason) { console.log(`🚨 Force completing ${handoffs.length} handoffs for ${agentName}`); const state = await this.loadAgentState(); for (const handoff of handoffs) { try { // Handle different handoff states if (handoff.toAgent === agentName) { // Incoming handoff - force complete it if (!handoff.acknowledgmentReceived) { await this.recordHandoffAcknowledged(handoff.id, agentName, { understoodRequirements: ['Auto-acknowledged during force deactivation'], concerns: ['Agent being force deactivated'] }); } if (!handoff.workStarted) { await this.recordHandoffWorkStarted(handoff.id, agentName); } if (!handoff.completed) { await this.recordHandoffCompleted(handoff.id, agentName, { summary: `Force completed due to agent deactivation: ${reason}`, deliverables: ['Work interrupted - see preserved agent state'], nextSteps: ['Review interrupted work', 'Reassign to appropriate agent'], qualityGatesPassed: false, nextAgentRecommendation: 'project-orchestrator-agent' }); } } else if (handoff.fromAgent === agentName) { // Outgoing handoff - mark as cancelled const handoffIndex = state.handoffEvents.findIndex(event => event.id === handoff.id); if (handoffIndex !== -1) { state.handoffEvents[handoffIndex].status = 'force_cancelled'; state.handoffEvents[handoffIndex].cancellationReason = `Source agent force deactivated: ${reason}`; state.handoffEvents[handoffIndex].cancellationTimestamp = new Date().toISOString(); } } console.log(`🚨 Force processed handoff ${handoff.id}`); } catch (error) { console.error(`❌ Error force completing handoff ${handoff.id}:`, error.message); // Mark as failed if we can't complete it const handoffIndex = state.handoffEvents.findIndex(event => event.id === handoff.id); if (handoffIndex !== -1) { state.handoffEvents[handoffIndex].status = 'failed'; state.handoffEvents[handoffIndex].failureReason = `Force deactivation error: ${error.message}`; state.handoffEvents[handoffIndex].failureTimestamp = new Date().toISOString(); } } } await this.saveAgentState(state); } /** * Update agent progress and status */ async updateAgentProgress(agentName, progress, status = 'active', blockedOn = null) { const state = await this.loadAgentState(); if (state.activeAgents[agentName]) { state.activeAgents[agentName].progress = progress; state.activeAgents[agentName].status = status; state.activeAgents[agentName].blockedOn = blockedOn; state.activeAgents[agentName].lastUpdate = new Date().toISOString(); if (status === 'blocked' && blockedOn) { console.log(`⚠️ ${agentName} blocked on: ${blockedOn}`); } } state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); } /** * Update agent status and current task description * @param {string} agentName - Name of the agent * @param {string} status - Agent status (active, blocked, completed, etc.) * @param {string} currentTask - Current task description */ async updateAgentStatus(agentName, status = 'active', currentTask = null) { const state = await this.loadAgentState(); if (state.activeAgents[agentName]) { state.activeAgents[agentName].status = status; if (currentTask) { state.activeAgents[agentName].task = currentTask; state.activeAgents[agentName].currentTask = currentTask; } state.activeAgents[agentName].lastUpdate = new Date().toISOString(); console.log(`🔄 Updated ${agentName} status to "${status}"`); if (currentTask) { console.log(`📋 Task: ${currentTask}`); } } else { console.warn(`⚠️ Agent ${agentName} not found in active agents`); } state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); } /** * Complete an agent's task and prepare for handoff */ async completeAgentTask(agentName, result, nextAgent = null) { const state = await this.loadAgentState(); if (state.activeAgents[agentName]) { const completedTask = { ...state.activeAgents[agentName], status: 'completed', endTime: new Date().toISOString(), result: result, nextAgent: nextAgent }; // Move to completed tasks state.completedTasks.push(completedTask); // Remove from active agents delete state.activeAgents[agentName]; // Add to handoff queue if next agent specified if (nextAgent) { state.handoffQueue.push({ from: agentName, to: nextAgent, task: result.nextTask || 'Continue from previous work', context: result.handoffContext || {}, priority: result.priority || 'normal', createdAt: new Date().toISOString() }); } state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`✅ ${agentName} completed task: ${completedTask.task}`); if (nextAgent) { console.log(`🔄 Handoff queued to ${nextAgent}`); } } } /** * Get next handoff from queue */ async getNextHandoff() { const state = await this.loadAgentState(); if (state.handoffQueue.length > 0) { // Sort by priority and creation time state.handoffQueue.sort((a, b) => { const priorityOrder = { urgent: 3, high: 2, normal: 1, low: 0 }; const aPriority = priorityOrder[a.priority] || 1; const bPriority = priorityOrder[b.priority] || 1; if (aPriority !== bPriority) { return bPriority - aPriority; // Higher priority first } return new Date(a.createdAt) - new Date(b.createdAt); // Older first for same priority }); const nextHandoff = state.handoffQueue.shift(); await this.saveAgentState(state); return nextHandoff; } return null; } /** * Get current agent workload and availability */ async getAgentWorkload() { const state = await this.loadAgentState(); const workload = {}; // Analyze active agents for (const [agentName, agentState] of Object.entries(state.activeAgents)) { workload[agentName] = { status: agentState.status, currentTask: agentState.task, progress: agentState.progress, blockedOn: agentState.blockedOn, workingTime: this.calculateWorkingTime(agentState.startTime), availability: agentState.status === 'blocked' ? 'blocked' : 'busy' }; } // Find available agents (not in active list) const allAgents = this.getAllKnownAgents(); for (const agentName of allAgents) { if (!workload[agentName]) { workload[agentName] = { status: 'available', currentTask: null, progress: 0, blockedOn: null, workingTime: 0, availability: 'available' }; } } return workload; } /** * Get agent performance metrics */ async getAgentMetrics(timeRange = '7d') { const state = await this.loadAgentState(); const history = await this.loadAgentHistory(); const metrics = { totalTasks: state.completedTasks.length, averageCompletionTime: this.calculateAverageCompletionTime(state.completedTasks), tasksByAgent: this.groupTasksByAgent(state.completedTasks), handoffEfficiency: this.calculateHandoffEfficiency(state.completedTasks), blockedTime: this.calculateBlockedTime(history, timeRange) }; return metrics; } /** * Generate task execution report */ async generateStatusReport() { const state = await this.loadAgentState(); const workload = await this.getAgentWorkload(); const metrics = await this.getAgentMetrics(); const report = { timestamp: new Date().toISOString(), summary: { activeAgents: Object.keys(state.activeAgents).length, pendingHandoffs: state.handoffQueue.length, completedTasks: state.completedTasks.length, blockedAgents: Object.values(workload).filter(a => a.availability === 'blocked').length }, activeWork: state.activeAgents, upcomingHandoffs: state.handoffQueue.slice(0, 5), // Next 5 handoffs recentCompletions: state.completedTasks.slice(-10), // Last 10 completed tasks metrics: metrics }; return report; } // Private helper methods generateTaskId() { return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } calculateWorkingTime(startTime) { const start = new Date(startTime); const now = new Date(); return Math.round((now - start) / (1000 * 60)); // Minutes } getAllKnownAgents() { // Return list of all agents from your project structure return [ 'project-orchestrator-agent', 'requirements-analysis-agent', 'market-research-agent', 'risk-assessment-agent', 'technical-architecture-agent', 'ux-ui-design-agent', 'database-schema-agent', 'api-design-agent', 'security-architecture-agent', 'frontend-development-agent', 'backend-development-agent', 'database-agent', 'authentication-agent', 'testing-agent', 'code-review-agent', 'performance-testing-agent', 'security-testing-agent', 'ci-cd-pipeline-agent', 'infrastructure-agent', 'monitoring-agent', 'backup-recovery-agent' ]; } calculateAverageCompletionTime(tasks) { if (tasks.length === 0) return 0; const totalTime = tasks.reduce((acc, task) => { if (task.startTime && task.endTime) { const duration = new Date(task.endTime) - new Date(task.startTime); return acc + duration; } return acc; }, 0); return Math.round(totalTime / tasks.length / (1000 * 60)); // Average minutes } groupTasksByAgent(tasks) { const grouped = {}; tasks.forEach(task => { const agentName = task.agent || 'unknown'; if (!grouped[agentName]) { grouped[agentName] = []; } grouped[agentName].push(task); }); return grouped; } calculateHandoffEfficiency(tasks) { const handoffs = tasks.filter(task => task.nextAgent); const successfulHandoffs = handoffs.filter(task => tasks.some(nextTask => nextTask.previousAgent === task.nextAgent) ); return handoffs.length > 0 ? (successfulHandoffs.length / handoffs.length) * 100 : 100; } calculateBlockedTime(history, timeRange) { // Implementation would analyze agent history for blocked time // This is a simplified version return 0; } async loadAgentState() { try { const data = await fs.readFile(this.agentStateFile, 'utf8'); return JSON.parse(data); } catch (error) { return { activeAgents: {}, completedTasks: [], handoffQueue: [], lastUpdate: new Date().toISOString() }; } } async saveAgentState(state) { await fs.writeFile(this.agentStateFile, JSON.stringify(state, null, 2)); } async loadAgentHistory() { try { const data = await fs.readFile(this.agentHistoryFile, 'utf8'); return JSON.parse(data); } catch (error) { return { events: [] }; } } async saveAgentHistory(history) { await fs.writeFile(this.agentHistoryFile, JSON.stringify(history, null, 2)); } /** * Archive old completed tasks to history */ async archiveOldTasks(maxTasks = 100) { const state = await this.loadAgentState(); if (state.completedTasks.length > maxTasks) { const history = await this.loadAgentHistory(); const tasksToArchive = state.completedTasks.splice(0, state.completedTasks.length - maxTasks); history.events.push({ type: 'task_archive', timestamp: new Date().toISOString(), archivedTasks: tasksToArchive }); await this.saveAgentHistory(history); await this.saveAgentState(state); console.log(`📦 Archived ${tasksToArchive.length} old tasks to history`); } } // ===== NEW SMART AGENT DEACTIVATION METHODS ===== /** * Check if an agent has pending handoff dependencies that would block deactivation * @param {string} agentName - Name of the agent to check * @returns {object} Dependency check result with blocking handoffs */ async checkAgentHandoffDependencies(agentName) { const state = await this.loadAgentState(); if (!state.handoffEvents) { return { canDeactivate: true, blockingHandoffs: [], pendingHandoffs: [], warnings: [] }; } const activeHandoffs = state.handoffEvents.filter(event => !event.completed && event.status !== 'failed' ); // Find handoffs where this agent is the source (initiated by this agent) const outgoingHandoffs = activeHandoffs.filter(event => event.fromAgent === agentName ); // Find handoffs where this agent is the target (received by this agent) const incomingHandoffs = activeHandoffs.filter(event => event.toAgent === agentName ); // Find handoffs that would be orphaned if this agent deactivates const orphanedHandoffs = activeHandoffs.filter(event => event.fromAgent === agentName && !event.acknowledgmentReceived ); const blockingHandoffs = []; const warnings = []; // Critical blocking conditions if (incomingHandoffs.some(h => h.acknowledgmentReceived && !h.workStarted)) { const acknowledgedButNotStarted = incomingHandoffs.filter(h => h.acknowledgmentReceived && !h.workStarted ); blockingHandoffs.push(...acknowledgedButNotStarted); warnings.push(`Agent has ${acknowledgedButNotStarted.length} acknowledged handoffs not yet started`); } if (incomingHandoffs.some(h => h.workStarted && !h.completed)) { const inProgressHandoffs = incomingHandoffs.filter(h => h.workStarted && !h.completed ); blockingHandoffs.push(...inProgressHandoffs); warnings.push(`Agent has ${inProgressHandoffs.length} handoffs in progress`); } // Warn about orphaned outgoing handoffs if (orphanedHandoffs.length > 0) { warnings.push(`Deactivating will orphan ${orphanedHandoffs.length} unacknowledged outgoing handoffs`); } return { canDeactivate: blockingHandoffs.length === 0, blockingHandoffs: blockingHandoffs, pendingHandoffs: [...incomingHandoffs, ...outgoingHandoffs], orphanedHandoffs: orphanedHandoffs, warnings: warnings, summary: { incoming: incomingHandoffs.length, outgoing: outgoingHandoffs.length, acknowledged: incomingHandoffs.filter(h => h.acknowledgmentReceived).length, inProgress: incomingHandoffs.filter(h => h.workStarted).length } }; } /** * Gracefully deactivate an agent after checking and handling handoff dependencies * @param {string} agentName - Name of the agent to deactivate * @param {string} reason - Reason for deactivation * @param {object} options - Deactivation options * @returns {object} Deactivation result */ async gracefulDeactivateAgent(agentName, reason = "Graceful deactivation", options = {}) { const { forceAfterTimeout = 30000, // 30 seconds default timeout waitForHandoffs = true, transferToAgent = null, preserveWork = true } = options; console.log(`🔄 Starting graceful deactivation of ${agentName}...`); // Check handoff dependencies const dependencyCheck = await this.checkAgentHandoffDependencies(agentName); if (!dependencyCheck.canDeactivate && waitForHandoffs) { console.log(`⚠️ ${agentName} has blocking handoff dependencies:`); dependencyCheck.warnings.forEach(warning => console.log(` - ${warning}`)); if (dependencyCheck.blockingHandoffs.length > 0) { // Attempt to complete blocking handoffs gracefully const completionResults = await this.attemptHandoffCompletion( agentName, dependencyCheck.blockingHandoffs, forceAfterTimeout ); if (!completionResults.allCompleted) { return { success: false, reason: 'Handoff dependencies could not be resolved', blockingHandoffs: completionResults.remainingHandoffs, message: `Cannot gracefully deactivate ${agentName}. ${completionResults.remainingHandoffs.length} handoffs still blocking.` }; } } } // Handle orphaned outgoing handoffs if (dependencyCheck.orphanedHandoffs.length > 0) { await this.handleOrphanedHandoffs(agentName, dependencyCheck.orphanedHandoffs, transferToAgent); } // Preserve agent work if requested if (preserveWork) { await this.preserveAgentWork(agentName, reason); } // Perform the actual deactivation const state = await this.loadAgentState(); if (state.activeAgents[agentName]) { const completedTask = { ...state.activeAgents[agentName], endTime: new Date().toISOString(), completionReason: reason, status: 'gracefully_deactivated', handoffsSummary: { blocking: dependencyCheck.blockingHandoffs.length, orphaned: dependencyCheck.orphanedHandoffs.length, total: dependencyCheck.pendingHandoffs.length } }; state.completedTasks.push(completedTask); delete state.activeAgents[agentName]; state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`✅ ${agentName} gracefully deactivated: ${reason}`); return { success: true, reason: reason, handoffsSummary: completedTask.handoffsSummary, message: `Successfully deactivated ${agentName} after handling dependencies` }; } return { success: false, reason: 'Agent was not active', message: `${agentName} was not in active state` }; } /** * Attempt to complete blocking handoffs within a timeout period * @param {string} agentName - Name of the agent * @param {array} blockingHandoffs - List of blocking handoffs * @param {number} timeoutMs - Timeout in milliseconds * @returns {object} Completion attempt result */ async attemptHandoffCompletion(agentName, blockingHandoffs, timeoutMs = 30000) { console.log(`⏳ Attempting to complete ${blockingHandoffs.length} blocking handoffs for ${agentName} (timeout: ${timeoutMs}ms)`); const startTime = Date.now(); const remainingHandoffs = [...blockingHandoffs]; // Try to complete each handoff for (const handoff of blockingHandoffs) { const elapsed = Date.now() - startTime; if (elapsed >= timeoutMs) { console.log(`⏰ Timeout reached while completing handoffs for ${agentName}`); break; } try { // If handoff is acknowledged but work not started, try to start it if (handoff.acknowledgmentReceived && !handoff.workStarted) { await this.recordHandoffWorkStarted(handoff.id, agentName); console.log(`🔄 Auto-started work on handoff ${handoff.id}`); } // If work is in progress, attempt quick completion with minimal deliverables if (handoff.workStarted && !handoff.completed) { await this.recordHandoffCompleted(handoff.id, agentName, { summary: `Handoff completed during graceful deactivation: ${agentName}`, deliverables: ['Work preserved in agent state'], nextSteps: ['Review preserved work', 'Continue with next agent'], qualityGatesPassed: false, // Mark as not fully quality checked nextAgentRecommendation: null }); console.log(`✅ Auto-completed handoff ${handoff.id} for graceful deactivation`); // Remove from remaining handoffs const index = remainingHandoffs.findIndex(h => h.id === handoff.id); if (index !== -1) { remainingHandoffs.splice(index, 1); } } } catch (error) { console.warn(`⚠️ Could not auto-complete handoff ${handoff.id}:`, error.message); } } return { allCompleted: remainingHandoffs.length === 0, remainingHandoffs: remainingHandoffs, completedCount: blockingHandoffs.length - remainingHandoffs.length, timeElapsed: Date.now() - startTime }; } /** * Handle orphaned outgoing handoffs by transferring or canceling them * @param {string} agentName - Name of the deactivating agent * @param {array} orphanedHandoffs - List of orphaned handoffs * @param {string|null} transferToAgent - Agent to transfer handoffs to */ async handleOrphanedHandoffs(agentName, orphanedHandoffs, transferToAgent = null) { console.log(`🔄 Handling ${orphanedHandoffs.length} orphaned handoffs from ${agentName}`); const state = await this.loadAgentState(); for (const handoff of orphanedHandoffs) { if (transferToAgent) { // Transfer handoff to another agent const handoffIndex = state.handoffEvents.findIndex(event => event.id === handoff.id); if (handoffIndex !== -1) { state.handoffEvents[handoffIndex].fromAgent = transferToAgent; state.handoffEvents[handoffIndex].data.context += `\n\nTransferred from ${agentName} during deactivation.`; console.log(`📤 Transferred handoff ${handoff.id} from ${agentName} to ${transferToAgent}`); } } else { // Mark handoff as cancelled const handoffIndex = state.handoffEvents.findIndex(event => event.id === handoff.id); if (handoffIndex !== -1) { state.handoffEvents[handoffIndex].status = 'cancelled'; state.handoffEvents[handoffIndex].cancellationReason = `Source agent ${agentName} deactivated`; state.handoffEvents[handoffIndex].cancellationTimestamp = new Date().toISOString(); console.log(`❌ Cancelled orphaned handoff ${handoff.id} from ${agentName}`); } } } await this.saveAgentState(state); } /** * Preserve agent work state before deactivation * @param {string} agentName - Name of the agent * @param {string} reason - Reason for preservation */ async preserveAgentWork(agentName, reason) { const state = await this.loadAgentState(); if (state.activeAgents[agentName]) { const agentData = state.activeAgents[agentName]; // Create a detailed work preservation record const workPreservation = { agentName: agentName, preservationTimestamp: new Date().toISOString(), reason: reason, task: agentData.task, progress: agentData.progress, context: agentData.context, status: agentData.status, workingTime: this.calculateWorkingTime(agentData.startTime), blockedOn: agentData.blockedOn, dependencies: agentData.dependencies, estimatedCompletion: agentData.estimatedCompletion }; // Add to completed tasks with special preservation marker if (!state.preservedWork) { state.preservedWork = []; } state.preservedWork.push(workPreservation); // Keep only last 20 preserved work items if (state.preservedWork.length > 20) { state.preservedWork = state.preservedWork.slice(-20); } await this.saveAgentState(state); console.log(`💾 Preserved work state for ${agentName}`); } } // ===== NEW HANDOFF EVENT TRACKING METHODS ===== /** * Record that a handoff has been initiated from one agent to another * @param {string} fromAgent - Source agent name * @param {string} toAgent - Target agent name * @param {object} handoffData - Handoff details * @returns {string} Handoff ID for tracking */ async recordHandoffInitiated(fromAgent, toAgent, handoffData) { const state = await this.loadAgentState(); // Ensure handoffEvents array exists if (!state.handoffEvents) { state.handoffEvents = []; } const handoffId = this.generateHandoffId(fromAgent, toAgent); const handoffEvent = { id: handoffId, type: 'handoff_initiated', fromAgent: fromAgent, toAgent: toAgent, timestamp: new Date().toISOString(), status: 'initiated', data: { taskDescription: handoffData.taskDescription || '', context: handoffData.context || '', requirements: handoffData.requirements || '', successCriteria: handoffData.successCriteria || '', priority: handoffData.priority || 'normal', estimatedDuration: handoffData.estimatedDuration || null }, acknowledgmentReceived: false, workStarted: false, completed: false }; state.handoffEvents.push(handoffEvent); state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`📤 Handoff initiated: ${fromAgent}${toAgent} (ID: ${handoffId})`); return handoffId; } /** * Record that a handoff has been acknowledged by the receiving agent * @param {string} handoffId - ID of the handoff * @param {string} acknowledgingAgent - Agent acknowledging the handoff * @param {object} acknowledgmentData - Acknowledgment details */ async recordHandoffAcknowledged(handoffId, acknowledgingAgent, acknowledgmentData = {}) { const state = await this.loadAgentState(); if (!state.handoffEvents) { state.handoffEvents = []; } const handoffEvent = state.handoffEvents.find(event => event.id === handoffId); if (!handoffEvent) { throw new Error(`Handoff not found: ${handoffId}`); } if (handoffEvent.toAgent !== acknowledgingAgent) { throw new Error(`Agent ${acknowledgingAgent} cannot acknowledge handoff meant for ${handoffEvent.toAgent}`); } handoffEvent.acknowledgmentReceived = true; handoffEvent.acknowledgmentTimestamp = new Date().toISOString(); handoffEvent.acknowledgmentData = { understoodRequirements: acknowledgmentData.understoodRequirements || [], questions: acknowledgmentData.questions || [], estimatedCompletion: acknowledgmentData.estimatedCompletion || null, concerns: acknowledgmentData.concerns || [] }; handoffEvent.status = 'acknowledged'; state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`✅ Handoff acknowledged: ${handoffId} by ${acknowledgingAgent}`); } /** * Record that work has started on a handoff * @param {string} handoffId - ID of the handoff * @param {string} workingAgent - Agent starting work */ async recordHandoffWorkStarted(handoffId, workingAgent) { const state = await this.loadAgentState(); if (!state.handoffEvents) { state.handoffEvents = []; } const handoffEvent = state.handoffEvents.find(event => event.id === handoffId); if (!handoffEvent) { throw new Error(`Handoff not found: ${handoffId}`); } if (handoffEvent.toAgent !== workingAgent) { throw new Error(`Agent ${workingAgent} cannot start work on handoff meant for ${handoffEvent.toAgent}`); } handoffEvent.workStarted = true; handoffEvent.workStartTimestamp = new Date().toISOString(); handoffEvent.status = 'in_progress'; state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`🔄 Work started: ${handoffId} by ${workingAgent}`); } /** * Record that a handoff has been completed * @param {string} handoffId - ID of the handoff * @param {string} completingAgent - Agent completing the handoff * @param {object} completionData - Completion details */ async recordHandoffCompleted(handoffId, completingAgent, completionData = {}) { const state = await this.loadAgentState(); if (!state.handoffEvents) { state.handoffEvents = []; } const handoffEvent = state.handoffEvents.find(event => event.id === handoffId); if (!handoffEvent) { throw new Error(`Handoff not found: ${handoffId}`); } if (handoffEvent.toAgent !== completingAgent) { throw new Error(`Agent ${completingAgent} cannot complete handoff meant for ${handoffEvent.toAgent}`); } handoffEvent.completed = true; handoffEvent.completionTimestamp = new Date().toISOString(); handoffEvent.completionData = { deliverables: completionData.deliverables || [], summary: completionData.summary || '', nextSteps: completionData.nextSteps || [], qualityGatesPassed: completionData.qualityGatesPassed || false, nextAgentRecommendation: completionData.nextAgentRecommendation || null }; handoffEvent.status = 'completed'; // Calculate completion time if (handoffEvent.workStartTimestamp) { const workStart = new Date(handoffEvent.workStartTimestamp); const completion = new Date(handoffEvent.completionTimestamp); handoffEvent.completionTimeMinutes = Math.round((completion - workStart) / (1000 * 60)); } state.lastUpdate = new Date().toISOString(); await this.saveAgentState(state); console.log(`🏁 Handoff completed: ${handoffId} by ${completingAgent}`); } /** * Get all active handoffs (initiated but not completed) * @returns {array} Array of active handoff events */ async getActiveHandoffs() { const state = await this.loadAgentState(); if (!state.handoffEvents) { return []; } return state.handoffEvents.filter(event => !event.completed && event.status !== 'failed' ); } /** * Get handoff by ID * @param {string} handoffId - ID of the handoff * @returns {object|null} Handoff event or null if not found */ async getHandoffById(handoffId) { const state = await this.loadAgentState(); if (!state.handoffEvents) { return null; } return state.handoffEvents.find(event => event.id === handoffId) || null; } /** * Get handoffs for a specific agent * @param {string} agentName - Name of the agent * @param {string} role - 'from', 'to', or 'both' (default: 'both') * @returns {array} Array of handoff events */ async getHandoffsForAgent(agentName, role = 'both') { const state = await this.loadAgentState(); if (!state.handoffEvents) { return []; } return state.handoffEvents.filter(event => { switch (role) { case 'from': return event.fromAgent === agentName; case 'to': return event.toAgent === agentName; case 'both': default: return event.fromAgent === agentName || event.toAgent === agentName; } }); } /** * Generate a unique handoff ID * @param {string} fromAgent - Source agent * @param {string} toAgent - Target agent * @returns {string} Unique handoff ID */ generateHandoffId(fromAgent, toAgent) { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `handoff_${fromAgent}_to_${toAgent}_${timestamp}_${random}`; } /** * Get handoff performance metrics * @param {number} days - Number of days to analyze (default: 7) * @returns {object} Performance metrics */ async getHandoffMetrics(days = 7) { const state = await this.loadAgentState(); if (!state.handoffEvents) { return { totalHandoffs: 0, completedHandoffs: 0, averageCompletionTime: 0, acknowledgmentRate: 0, completionRate: 0 }; } const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - days); const recentHandoffs = state.handoffEvents.filter(event => new Date(event.timestamp) >= cutoffDate ); const completedHandoffs = recentHandoffs.filter(event => event.completed); const acknowledgedHandoffs = recentHandoffs.filter(event => event.acknowledgmentReceived); const totalCompletionTime = completedHandoffs.reduce((sum, event) => sum + (event.completionTimeMinutes || 0), 0 ); return { totalHandoffs: recentHandoffs.length, completedHandoffs: completedHandoffs.length, averageCompletionTime: completedHandoffs.length > 0 ? Math.round(totalCompletionTime / completedHandoffs.length) : 0, acknowledgmentRate: recentHandoffs.length > 0 ? Math.round((acknowledgedHandoffs.length / recentHandoffs.length) * 100) : 0, completionRate: recentHandoffs.length > 0 ? Math.round((completedHandoffs.length / recentHandoffs.length) * 100) : 0, pendingHandoffs: recentHandoffs.filter(event => !event.completed && event.status !== 'failed' ).length }; } } module.exports = AgentStateTracker;