UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

436 lines (435 loc) 15.2 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../../../core/monitoring/logger.js"; import { FrameManager } from "../../../core/context/index.js"; import { sharedContextLayer } from "../../../core/context/shared-context-layer.js"; import { sessionManager } from "../../../core/session/index.js"; class PatternLearner { frameManager; config; constructor(config) { this.config = { minLoopCountForPattern: 3, confidenceThreshold: 0.7, maxPatternsPerType: 10, analysisDepth: "deep", ...config }; logger.info("Pattern learner initialized", this.config); } async initialize() { try { await sessionManager.initialize(); await sharedContextLayer.initialize(); const session = await sessionManager.getOrCreateSession({}); if (session.database) { this.frameManager = new FrameManager( session.database, session.projectId ); } logger.info("Pattern learner initialized successfully"); } catch (error) { logger.error("Failed to initialize pattern learner", error); throw error; } } /** * Learn patterns from all completed Ralph loops */ async learnFromCompletedLoops() { logger.info("Starting pattern learning from completed loops"); try { const completedLoops = await this.getCompletedRalphLoops(); logger.info( `Found ${completedLoops.length} completed loops for analysis` ); if (completedLoops.length < this.config.minLoopCountForPattern) { logger.info("Not enough loops for pattern extraction"); return []; } const patterns = []; const successPatterns = await this.extractSuccessPatterns(completedLoops); patterns.push(...successPatterns); const failurePatterns = await this.extractFailurePatterns(completedLoops); patterns.push(...failurePatterns); const iterationPatterns = await this.extractIterationPatterns(completedLoops); patterns.push(...iterationPatterns); const taskPatterns = await this.extractTaskPatterns(completedLoops); patterns.push(...taskPatterns); await this.saveLearnedPatterns(patterns); logger.info( `Learned ${patterns.length} patterns from ${completedLoops.length} loops` ); return patterns; } catch (error) { logger.error("Failed to learn patterns", error); throw error; } } /** * Learn patterns specific to a task type */ async learnForTaskType(taskType) { logger.info(`Learning patterns for task type: ${taskType}`); const completedLoops = await this.getCompletedRalphLoops(); const relevantLoops = completedLoops.filter( (loop) => this.classifyTaskType(loop.task) === taskType ); if (relevantLoops.length < this.config.minLoopCountForPattern) { return []; } return this.extractSpecializedPatterns(relevantLoops, taskType); } /** * Get all completed Ralph loops from StackMemory */ async getCompletedRalphLoops() { if (!this.frameManager) { throw new Error("Frame manager not initialized"); } try { const ralphFrames = await this.frameManager.searchFrames({ type: "task", namePattern: "ralph-*", state: "closed" }); const analyses = []; for (const frame of ralphFrames) { try { const analysis = await this.analyzeCompletedLoop(frame); if (analysis) { analyses.push(analysis); } } catch (error) { logger.warn( `Failed to analyze loop ${frame.frame_id}`, error ); } } return analyses; } catch (error) { logger.error("Failed to get completed loops", error); return []; } } /** * Analyze a completed loop for patterns */ async analyzeCompletedLoop(ralphFrame) { if (!this.frameManager) return null; try { const loopState = ralphFrame.inputs; const iterationFrames = await this.frameManager.searchFrames({ type: "subtask", namePattern: "iteration-*", parentId: ralphFrame.frame_id }); const successMetrics = this.calculateSuccessMetrics(iterationFrames); const iterationAnalysis = this.analyzeIterations(iterationFrames); const outcome = this.determineLoopOutcome(ralphFrame, iterationFrames); return { loopId: loopState.loopId, task: loopState.task, criteria: loopState.criteria, taskType: this.classifyTaskType(loopState.task), iterationCount: iterationFrames.length, outcome, successMetrics, iterationAnalysis, duration: ralphFrame.updated_at - ralphFrame.created_at, startTime: ralphFrame.created_at, endTime: ralphFrame.updated_at }; } catch (error) { logger.error("Failed to analyze loop", error); return null; } } /** * Extract patterns from successful loops */ async extractSuccessPatterns(loops) { const successfulLoops = loops.filter((l) => l.outcome === "success"); if (successfulLoops.length < this.config.minLoopCountForPattern) { return []; } const patterns = []; const avgIterations = successfulLoops.reduce((sum, l) => sum + l.iterationCount, 0) / successfulLoops.length; patterns.push({ id: "optimal-iterations", type: "iteration_strategy", pattern: `Successful tasks typically complete in ${Math.round(avgIterations)} iterations`, confidence: this.calculateConfidence(successfulLoops.length), frequency: successfulLoops.length, strategy: `Target ${Math.round(avgIterations)} iterations for similar tasks`, examples: successfulLoops.slice(0, 3).map((l) => l.task), metadata: { avgIterations, minIterations: Math.min( ...successfulLoops.map((l) => l.iterationCount) ), maxIterations: Math.max( ...successfulLoops.map((l) => l.iterationCount) ) } }); const criteriaPatterns = this.extractCriteriaPatterns(successfulLoops); patterns.push(...criteriaPatterns); const successFactors = this.extractSuccessFactors(successfulLoops); patterns.push(...successFactors); return patterns.filter( (p) => p.confidence >= this.config.confidenceThreshold ); } /** * Extract patterns from failed loops to avoid */ async extractFailurePatterns(loops) { const failedLoops = loops.filter((l) => l.outcome === "failure"); if (failedLoops.length < this.config.minLoopCountForPattern) { return []; } const patterns = []; const commonFailures = this.analyzeFailurePoints(failedLoops); for (const failure of commonFailures) { patterns.push({ id: `avoid-${failure.type}`, type: "failure_avoidance", pattern: `Avoid: ${failure.pattern}`, confidence: this.calculateConfidence(failure.frequency), frequency: failure.frequency, strategy: failure.avoidanceStrategy, examples: failure.examples, metadata: { failureType: failure.type } }); } return patterns.filter( (p) => p.confidence >= this.config.confidenceThreshold ); } /** * Extract iteration-specific patterns */ async extractIterationPatterns(loops) { const patterns = []; const iterationSequences = this.analyzeIterationSequences(loops); for (const sequence of iterationSequences) { if (sequence.frequency >= this.config.minLoopCountForPattern) { patterns.push({ id: `iteration-sequence-${sequence.id}`, type: "iteration_sequence", pattern: sequence.description, confidence: this.calculateConfidence(sequence.frequency), frequency: sequence.frequency, strategy: sequence.strategy, examples: sequence.examples, metadata: { sequenceType: sequence.type } }); } } return patterns; } /** * Extract task-specific patterns */ async extractTaskPatterns(loops) { const taskGroups = this.groupByTaskType(loops); const patterns = []; for (const [taskType, taskLoops] of Object.entries(taskGroups)) { if (taskLoops.length >= this.config.minLoopCountForPattern) { const taskSpecificPatterns = await this.extractSpecializedPatterns( taskLoops, taskType ); patterns.push(...taskSpecificPatterns); } } return patterns; } /** * Extract specialized patterns for specific task types */ async extractSpecializedPatterns(loops, taskType) { const patterns = []; const successful = loops.filter((l) => l.outcome === "success"); if (successful.length === 0) return patterns; patterns.push({ id: `${taskType}-success-pattern`, type: "task_specific", pattern: `${taskType} tasks: ${this.summarizeSuccessPattern(successful)}`, confidence: this.calculateConfidence(successful.length), frequency: successful.length, strategy: this.generateTaskStrategy(successful), examples: successful.slice(0, 2).map((l) => l.task), metadata: { taskType, totalAttempts: loops.length } }); return patterns; } /** * Calculate success metrics for iterations */ calculateSuccessMetrics(iterations) { const total = iterations.length; const successful = iterations.filter((i) => i.outputs?.success).length; return { iterationCount: total, successRate: total > 0 ? successful / total : 0, averageProgress: this.calculateAverageProgress(iterations), timeToCompletion: total > 0 ? iterations[total - 1].updated_at - iterations[0].created_at : 0 }; } /** * Classify task type based on description */ classifyTaskType(task) { const taskLower = task.toLowerCase(); if (taskLower.includes("test") || taskLower.includes("unit")) return "testing"; if (taskLower.includes("fix") || taskLower.includes("bug")) return "bugfix"; if (taskLower.includes("refactor")) return "refactoring"; if (taskLower.includes("add") || taskLower.includes("implement")) return "feature"; if (taskLower.includes("document")) return "documentation"; if (taskLower.includes("optimize") || taskLower.includes("performance")) return "optimization"; return "general"; } /** * Determine loop outcome */ determineLoopOutcome(ralphFrame, iterations) { if (ralphFrame.digest_json?.status === "completed") return "success"; if (iterations.length === 0) return "unknown"; const lastIteration = iterations[iterations.length - 1]; if (lastIteration.outputs?.success) return "success"; return "failure"; } /** * Calculate confidence based on frequency */ calculateConfidence(frequency) { return Math.min(0.95, Math.log(frequency + 1) / Math.log(10)); } /** * Save learned patterns to shared context */ async saveLearnedPatterns(patterns) { try { const context = await sharedContextLayer.getSharedContext(); if (!context) return; const contextPatterns = patterns.map((p) => ({ pattern: p.pattern, type: this.mapPatternType(p.type), frequency: p.frequency, lastSeen: Date.now(), resolution: p.strategy })); context.globalPatterns.push(...contextPatterns); context.globalPatterns.sort((a, b) => b.frequency - a.frequency); context.globalPatterns = context.globalPatterns.slice(0, 100); await sharedContextLayer.updateSharedContext(context); logger.info(`Saved ${patterns.length} patterns to shared context`); } catch (error) { logger.error("Failed to save patterns", error); } } /** * Map pattern types to shared context types */ mapPatternType(patternType) { switch (patternType) { case "failure_avoidance": return "error"; case "success_strategy": return "success"; case "task_specific": return "learning"; default: return "learning"; } } // Additional helper methods for pattern analysis analyzeIterations(iterations) { return { avgDuration: iterations.length > 0 ? iterations.reduce( (sum, i) => sum + (i.updated_at - i.created_at), 0 ) / iterations.length : 0, progressPattern: this.extractProgressPattern(iterations), commonIssues: this.extractCommonIssues(iterations) }; } extractProgressPattern(iterations) { const progressSteps = iterations.map((_, i) => { const progress = i / iterations.length; return Math.round(progress * 100); }); return progressSteps.join(" \u2192 ") + "%"; } extractCommonIssues(iterations) { return iterations.filter((i) => i.outputs?.errors?.length > 0).flatMap((i) => i.outputs.errors).slice(0, 3); } extractCriteriaPatterns(loops) { const criteriaWords = loops.flatMap( (l) => l.criteria.toLowerCase().split(/\s+/) ); const wordCounts = criteriaWords.reduce( (acc, word) => { acc[word] = (acc[word] || 0) + 1; return acc; }, {} ); const commonCriteria = Object.entries(wordCounts).filter(([_, count]) => count >= this.config.minLoopCountForPattern).sort((a, b) => b[1] - a[1]).slice(0, 3); return commonCriteria.map(([word, count]) => ({ id: `criteria-${word}`, type: "success_strategy", pattern: `Successful tasks often include "${word}" in completion criteria`, confidence: this.calculateConfidence(count), frequency: count, strategy: `Consider including "${word}" in task completion criteria`, examples: loops.filter((l) => l.criteria.toLowerCase().includes(word)).slice(0, 2).map((l) => l.task), metadata: { criteriaWord: word } })); } extractSuccessFactors(_loops) { return []; } analyzeFailurePoints(_loops) { return []; } analyzeIterationSequences(_loops) { return []; } groupByTaskType(loops) { return loops.reduce( (acc, loop) => { const type = loop.taskType; if (!acc[type]) acc[type] = []; acc[type].push(loop); return acc; }, {} ); } summarizeSuccessPattern(loops) { const avgIterations = loops.reduce((sum, l) => sum + l.iterationCount, 0) / loops.length; return `typically complete in ${Math.round(avgIterations)} iterations with ${Math.round(loops[0]?.successMetrics?.successRate * 100 || 0)}% success rate`; } generateTaskStrategy(loops) { const avgIterations = loops.reduce((sum, l) => sum + l.iterationCount, 0) / loops.length; return `Plan for approximately ${Math.round(avgIterations)} iterations and focus on iterative improvement`; } calculateAverageProgress(iterations) { return iterations.length > 0 ? iterations.length / 10 : 0; } } const patternLearner = new PatternLearner(); export { PatternLearner, patternLearner };