@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
402 lines (401 loc) • 14.9 kB
JavaScript
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
};
//# sourceMappingURL=pattern-learner.js.map