UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

443 lines (383 loc) 13.7 kB
/** * Session Context Loader - Multi-Agent Session Continuity * * Provides session context when transitioning between agents. * Solves the problem: "@po approved story, @dev doesn't know about it" * * Features: * - Detects previous agent in session * - Tracks last N commands * - Identifies active workflow * - Provides natural language summary for new agent * * @module core/session/context-loader * @migrated Story 2.2 - Core Module Creation * Part of Story 6.1.2.5 UX Improvements */ const fs = require('fs'); const path = require('path'); const ContextDetector = require('./context-detector'); const SESSION_STATE_PATH = path.join(process.cwd(), '.aios', 'session-state.json'); const MAX_COMMANDS_HISTORY = 10; class SessionContextLoader { constructor() { this.detector = new ContextDetector(); this.sessionStatePath = SESSION_STATE_PATH; } /** * Load session context for current agent activation * * @param {string} currentAgentId - ID of agent being activated * @returns {Object} Session context */ loadContext(currentAgentId) { // Pass sessionStatePath to detector so it uses the correct file (important for testing) const sessionType = this.detector.detectSessionType([], this.sessionStatePath); const sessionState = this.loadSessionState(); if (sessionType === 'new') { // Fresh session - no context return { sessionType: 'new', message: null, previousAgent: null, lastCommands: [], workflowActive: null, }; } // Extract context information const previousAgent = this.getPreviousAgent(sessionState, currentAgentId); const lastCommands = sessionState.lastCommands || []; const workflowActive = sessionState.workflowActive || null; // Generate natural language summary const message = this.generateContextMessage({ sessionType, previousAgent, lastCommands, workflowActive, currentAgentId, }); return { sessionType, message, previousAgent, lastCommands, workflowActive, currentStory: sessionState.currentStory || null, sessionId: sessionState.sessionId, sessionStartTime: sessionState.startTime, }; } /** * Load session state from file * * @returns {Object} Session state */ loadSessionState() { try { if (!fs.existsSync(this.sessionStatePath)) { return {}; } const content = fs.readFileSync(this.sessionStatePath, 'utf8'); return JSON.parse(content); } catch (error) { console.warn('[SessionContext] Failed to load session state:', error.message); return {}; } } /** * Get previous agent from session * * @param {Object} sessionState - Session state * @param {string} currentAgentId - Current agent ID * @returns {Object|null} Previous agent info */ getPreviousAgent(sessionState, currentAgentId) { const agentSequence = sessionState.agentSequence || []; if (agentSequence.length === 0) { return null; } // Get last agent that's different from current for (let i = agentSequence.length - 1; i >= 0; i--) { const agent = agentSequence[i]; if (agent.agentId !== currentAgentId) { return { agentId: agent.agentId, agentName: agent.agentName, activatedAt: agent.activatedAt, lastCommand: agent.lastCommand, }; } } return null; } /** * Generate natural language context message * * @param {Object} context - Context data * @returns {string|null} Context message */ generateContextMessage(context) { const { sessionType, previousAgent, lastCommands, workflowActive } = context; if (sessionType === 'new') { return null; } const parts = []; // Previous agent context if (previousAgent) { const agentName = previousAgent.agentName || previousAgent.agentId; const minutesAgo = Math.floor((Date.now() - previousAgent.activatedAt) / 60000); const timeAgo = minutesAgo < 1 ? 'just now' : minutesAgo === 1 ? '1 minute ago' : `${minutesAgo} minutes ago`; parts.push(`📍 **Session Context**: Continuing from @${previousAgent.agentId} (${agentName}) activated ${timeAgo}`); if (previousAgent.lastCommand) { parts.push(` Last action: *${previousAgent.lastCommand}`); } } // Recent commands if (lastCommands.length > 0) { const recentCmds = lastCommands.slice(-5).join(', *'); parts.push(` Recent commands: *${recentCmds}`); } // Active workflow if (workflowActive) { parts.push(` ⚡ Active Workflow: ${workflowActive}`); } return parts.length > 0 ? parts.join('\n') : null; } /** * Update session state with current agent * * @param {string} agentId - Agent ID * @param {string} agentName - Agent name * @param {string} lastCommand - Last command executed * @param {Object} options - Update options */ updateSession(agentId, agentName, lastCommand = null, options = {}) { try { const sessionState = this.loadSessionState(); // Initialize if new session if (!sessionState.sessionId) { sessionState.sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; sessionState.startTime = Date.now(); } // Update activity timestamp sessionState.lastActivity = Date.now(); // Update agent sequence if (!sessionState.agentSequence) { sessionState.agentSequence = []; } sessionState.agentSequence.push({ agentId, agentName, activatedAt: Date.now(), lastCommand, }); // Keep last 20 agent activations if (sessionState.agentSequence.length > 20) { sessionState.agentSequence = sessionState.agentSequence.slice(-20); } // Update command history if (lastCommand) { if (!sessionState.lastCommands) { sessionState.lastCommands = []; } sessionState.lastCommands.push(lastCommand); // Keep last N commands if (sessionState.lastCommands.length > MAX_COMMANDS_HISTORY) { sessionState.lastCommands = sessionState.lastCommands.slice(-MAX_COMMANDS_HISTORY); } } // Update workflow status if (options.workflowActive !== undefined) { sessionState.workflowActive = options.workflowActive; } // Save to file this.detector.updateSessionState(sessionState, this.sessionStatePath); } catch (error) { console.warn('[SessionContext] Failed to update session:', error.message); } } /** * Clear session (start fresh) */ clearSession() { try { if (fs.existsSync(this.sessionStatePath)) { fs.unlinkSync(this.sessionStatePath); } } catch (error) { console.warn('[SessionContext] Failed to clear session:', error.message); } } /** * Format context for display in agent greeting * * @param {string} currentAgentId - Current agent ID * @returns {string} Formatted context message */ formatForGreeting(currentAgentId) { const context = this.loadContext(currentAgentId); if (!context.message) { return ''; } return `\n${context.message}\n`; } /** * Hook called when a task completes * Updates session state with task result and workflow transition * * @param {string} taskName - Name of the completed task * @param {Object} result - Task execution result * @param {boolean} result.success - Whether task succeeded * @param {string} result.agentId - Agent that executed the task * @param {string} result.storyPath - Associated story path (optional) * @param {Object} result.metadata - Additional metadata (optional) * @story WIS-3 - Task Completion Hook */ onTaskComplete(taskName, result = {}) { try { const sessionState = this.loadSessionState(); // Initialize if new session if (!sessionState.sessionId) { sessionState.sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; sessionState.startTime = Date.now(); } // Update timestamp sessionState.lastActivity = Date.now(); // Add command to history if (!sessionState.lastCommands) { sessionState.lastCommands = []; } const commandEntry = taskName.startsWith('*') ? taskName : `*${taskName}`; sessionState.lastCommands.push(commandEntry); // Keep last N commands if (sessionState.lastCommands.length > MAX_COMMANDS_HISTORY) { sessionState.lastCommands = sessionState.lastCommands.slice(-MAX_COMMANDS_HISTORY); } // Update task completion history if (!sessionState.taskHistory) { sessionState.taskHistory = []; } sessionState.taskHistory.push({ task: taskName, completedAt: Date.now(), success: result.success !== false, agentId: result.agentId || null, storyPath: result.storyPath || null, }); // Keep last 20 task completions if (sessionState.taskHistory.length > 20) { sessionState.taskHistory = sessionState.taskHistory.slice(-20); } // Update current story if provided if (result.storyPath) { sessionState.currentStory = result.storyPath; } // Infer workflow state transition const workflowState = this._inferWorkflowState(taskName, result); if (workflowState) { sessionState.workflowActive = workflowState.workflow; sessionState.workflowState = workflowState.state; } // Save updated state this.detector.updateSessionState(sessionState, this.sessionStatePath); return { success: true, sessionId: sessionState.sessionId, workflowState: sessionState.workflowState, }; } catch (error) { console.warn('[SessionContext] Failed to record task completion:', error.message); return { success: false, error: error.message }; } } /** * Infer workflow state from completed task * * @param {string} taskName - Completed task name * @param {Object} _result - Task result (unused, reserved for future use) * @returns {Object|null} Workflow state info * @private */ _inferWorkflowState(taskName, _result) { const normalizedTask = taskName.toLowerCase().replace(/^\*/, ''); // Map task completions to workflow states const stateMap = { 'validate-story-draft': { workflow: 'story_development', state: 'validated' }, 'validate-next-story': { workflow: 'story_development', state: 'validated' }, 'develop': { workflow: 'story_development', state: 'in_development' }, 'develop-yolo': { workflow: 'story_development', state: 'in_development' }, 'develop-interactive': { workflow: 'story_development', state: 'in_development' }, 'review-qa': { workflow: 'story_development', state: 'qa_reviewed' }, 'apply-qa-fixes': { workflow: 'story_development', state: 'qa_reviewed' }, 'pre-push-quality-gate': { workflow: 'git_workflow', state: 'staged' }, 'create-epic': { workflow: 'epic_creation', state: 'epic_drafted' }, 'create-story': { workflow: 'epic_creation', state: 'stories_created' }, 'create-next-story': { workflow: 'epic_creation', state: 'stories_created' }, 'backlog-review': { workflow: 'backlog_management', state: 'reviewed' }, 'backlog-prioritize': { workflow: 'backlog_management', state: 'prioritized' }, 'analyze-impact': { workflow: 'architecture_review', state: 'analyzed' }, 'create-doc': { workflow: 'documentation_workflow', state: 'drafted' }, 'db-domain-modeling': { workflow: 'database_workflow', state: 'designed' }, 'db-apply-migration': { workflow: 'database_workflow', state: 'migrated' }, }; return stateMap[normalizedTask] || null; } /** * Get current workflow state from session * * @returns {Object|null} Current workflow state */ getWorkflowState() { try { const sessionState = this.loadSessionState(); if (sessionState.workflowActive) { return { workflow: sessionState.workflowActive, state: sessionState.workflowState || null, lastActivity: sessionState.lastActivity, }; } } catch { // Ignore } return null; } /** * Get task completion history * * @param {number} limit - Maximum number of entries to return * @returns {Object[]} Task history entries */ getTaskHistory(limit = 10) { try { const sessionState = this.loadSessionState(); const history = sessionState.taskHistory || []; return history.slice(-limit); } catch { return []; } } } // CLI Interface if (require.main === module) { const loader = new SessionContextLoader(); const command = process.argv[2]; const agentId = process.argv[3] || 'dev'; if (command === 'load') { const context = loader.loadContext(agentId); console.log(JSON.stringify(context, null, 2)); } else if (command === 'clear') { loader.clearSession(); console.log('✅ Session cleared'); } else if (command === 'update') { const agentName = process.argv[4] || agentId.toUpperCase(); const lastCommand = process.argv[5] || null; loader.updateSession(agentId, agentName, lastCommand); console.log('✅ Session updated'); } else { // Default: show greeting format const message = loader.formatForGreeting(agentId); console.log(message || '(No session context)'); } } module.exports = SessionContextLoader;