UNPKG

aios-core

Version:

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

300 lines (251 loc) 8.11 kB
/** * Elicitation Session Manager * Handles saving and loading elicitation sessions * @module elicitation-session-manager */ const fs = require('fs-extra'); const path = require('path'); const crypto = require('crypto'); class ElicitationSessionManager { constructor(sessionDir = '.aios-sessions') { this.sessionDir = path.resolve(process.cwd(), sessionDir); this.activeSession = null; } /** * Initialize session storage */ async init() { await fs.ensureDir(this.sessionDir); } /** * Create a new session * @param {string} type - Component type (agent, task, workflow) * @param {Object} metadata - Additional session metadata * @returns {Promise<string>} Session ID */ async createSession(type, metadata = {}) { const sessionId = this.generateSessionId(); const session = { id: sessionId, type, version: '1.0', created: new Date().toISOString(), updated: new Date().toISOString(), status: 'active', currentStep: 0, totalSteps: 0, answers: {}, metadata: { ...metadata, user: process.env.USER || 'unknown', hostname: require('os').hostname() } }; this.activeSession = session; await this.saveSession(session); return sessionId; } /** * Save current session state * @param {Object} session - Session data to save */ async saveSession(session = null) { const sessionToSave = session || this.activeSession; if (!sessionToSave) { throw new Error('No active session to save'); } sessionToSave.updated = new Date().toISOString(); const sessionPath = this.getSessionPath(sessionToSave.id); await fs.writeJson(sessionPath, sessionToSave, { spaces: 2 }); } /** * Load an existing session * @param {string} sessionId - Session ID to load * @returns {Promise<Object>} Session data */ async loadSession(sessionId) { const sessionPath = this.getSessionPath(sessionId); if (!await fs.pathExists(sessionPath)) { throw new Error(`Session ${sessionId} not found`); } const session = await fs.readJson(sessionPath); this.activeSession = session; return session; } /** * Update session answers * @param {Object} answers - New answers to merge * @param {number} stepIndex - Current step index */ async updateAnswers(answers, stepIndex = null) { if (!this.activeSession) { throw new Error('No active session'); } // Merge answers Object.assign(this.activeSession.answers, answers); // Update step index if provided if (stepIndex !== null) { this.activeSession.currentStep = stepIndex; } await this.saveSession(); } /** * List all sessions * @param {Object} filters - Filter options * @returns {Promise<Array>} List of sessions */ async listSessions(filters = {}) { const files = await fs.readdir(this.sessionDir); const sessions = []; for (const file of files) { if (file.endsWith('.json')) { try { const sessionPath = path.join(this.sessionDir, file); const session = await fs.readJson(sessionPath); // Apply filters if (filters.type && session.type !== filters.type) continue; if (filters.status && session.status !== filters.status) continue; if (filters.after && new Date(session.created) < new Date(filters.after)) continue; sessions.push({ id: session.id, type: session.type, created: session.created, updated: session.updated, status: session.status, progress: session.totalSteps > 0 ? Math.round((session.currentStep / session.totalSteps) * 100) : 0 }); } catch (_error) { // Skip invalid session files console.warn(`Invalid session file: ${file}`); } } } // Sort by updated date (newest first) sessions.sort((a, b) => new Date(b.updated) - new Date(a.updated)); return sessions; } /** * Resume a session * @param {string} sessionId - Session ID to resume * @returns {Promise<Object>} Session data with resume info */ async resumeSession(sessionId) { const session = await this.loadSession(sessionId); // Calculate resume information const resumeInfo = { ...session, resumeFrom: session.currentStep, completedSteps: Object.keys(session.answers).length, remainingSteps: session.totalSteps - session.currentStep, percentComplete: session.totalSteps > 0 ? Math.round((session.currentStep / session.totalSteps) * 100) : 0 }; return resumeInfo; } /** * Complete a session * @param {string} result - Completion result (success, cancelled, error) */ async completeSession(result = 'success') { if (!this.activeSession) { throw new Error('No active session'); } this.activeSession.status = 'completed'; this.activeSession.completedAt = new Date().toISOString(); this.activeSession.result = result; await this.saveSession(); // Move to completed directory if success if (result === 'success') { const completedDir = path.join(this.sessionDir, 'completed'); await fs.ensureDir(completedDir); const oldPath = this.getSessionPath(this.activeSession.id); const newPath = path.join(completedDir, path.basename(oldPath)); await fs.move(oldPath, newPath, { overwrite: true }); } this.activeSession = null; } /** * Delete a session * @param {string} sessionId - Session ID to delete */ async deleteSession(sessionId) { const sessionPath = this.getSessionPath(sessionId); const completedPath = path.join(this.sessionDir, 'completed', `${sessionId}.json`); // Check both active and completed directories if (await fs.pathExists(sessionPath)) { await fs.remove(sessionPath); } else if (await fs.pathExists(completedPath)) { await fs.remove(completedPath); } else { throw new Error(`Session ${sessionId} not found`); } // Clear active session if it matches if (this.activeSession && this.activeSession.id === sessionId) { this.activeSession = null; } } /** * Export session data * @param {string} sessionId - Session ID to export * @param {string} format - Export format (json, yaml) * @returns {Promise<string>} Exported data */ async exportSession(sessionId, format = 'json') { const session = await this.loadSession(sessionId); switch (format) { case 'json': return JSON.stringify(session, null, 2); case 'yaml': const yaml = require('js-yaml'); return yaml.dump(session); default: throw new Error(`Unsupported export format: ${format}`); } } /** * Clean up old sessions * @param {number} daysOld - Delete sessions older than this many days */ async cleanupOldSessions(daysOld = 30) { const sessions = await this.listSessions(); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysOld); let deletedCount = 0; for (const session of sessions) { if (new Date(session.updated) < cutoffDate && session.status !== 'active') { await this.deleteSession(session.id); deletedCount++; } } return deletedCount; } /** * Generate a unique session ID * @private */ generateSessionId() { return crypto.randomBytes(8).toString('hex'); } /** * Get session file path * @private */ getSessionPath(sessionId) { return path.join(this.sessionDir, `${sessionId}.json`); } /** * Get active session * @returns {Object|null} Active session or null */ getActiveSession() { return this.activeSession; } /** * Clear active session */ clearActiveSession() { this.activeSession = null; } } module.exports = ElicitationSessionManager;