UNPKG

abyss-ai

Version:

Autonomous AI coding agent - enhanced OpenCode with autonomous capabilities

392 lines (332 loc) • 12.1 kB
import path from "path" export interface MemoryEntry { id: string timestamp: number filePath: string fileType: string issuesFound: string[] changesApplied: string[] successMetrics: { syntaxValid: boolean testsPass: boolean lintClean: boolean } patterns: { commonIssues: string[] effectiveFixes: string[] riskySections: string[] } context: { language: string framework?: string projectType?: string complexity: number } } export interface LearningPattern { pattern: string frequency: number successRate: number contexts: string[] recommendation: string } export interface ProjectInsight { projectPath: string lastAnalyzed: number commonIssues: LearningPattern[] bestPractices: string[] riskAreas: string[] preferences: { codingStyle: string[] testingFramework?: string lintingRules?: string[] } } export class AgentMemory { private memoryFile: string private memories: MemoryEntry[] = [] private insights: Map<string, ProjectInsight> = new Map() private maxMemories: number = 1000 private adviceCache: Map<string, {advice: string, timestamp: number}> = new Map() private cacheTimeout: number = 10 * 60 * 1000 // 10 minutes constructor(projectPath: string = process.cwd()) { this.memoryFile = path.join(projectPath, '.abyss', 'agent-memory.json') this.loadMemory() } async loadMemory(): Promise<void> { try { const memoryData = await Bun.file(this.memoryFile).text() const data = JSON.parse(memoryData) this.memories = data.memories || [] this.insights = new Map(data.insights || []) } catch (error) { // Memory file doesn't exist or is corrupted, start fresh this.memories = [] this.insights = new Map() } } async saveMemory(): Promise<void> { try { // Ensure .abyss directory exists const abyssDir = path.dirname(this.memoryFile) await Bun.write(path.join(abyssDir, '.keep'), '') const data = { memories: this.memories.slice(-this.maxMemories), // Keep only recent memories insights: Array.from(this.insights.entries()), lastUpdated: Date.now() } await Bun.write(this.memoryFile, JSON.stringify(data, null, 2)) } catch (error) { console.warn(`Failed to save agent memory: ${error}`) } } addMemory(memory: Omit<MemoryEntry, 'id' | 'timestamp'>): void { const entry: MemoryEntry = { id: `memory-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: Date.now(), ...memory } this.memories.push(entry) // Update project insights this.updateProjectInsights(entry) // Save to disk asynchronously this.saveMemory().catch(console.warn) } getRelevantMemories(filePath: string, fileType: string, limit: number = 10): MemoryEntry[] { const fileName = path.basename(filePath) const fileExt = path.extname(filePath) const dirName = path.basename(path.dirname(filePath)) return this.memories .filter(memory => { // Same file type gets highest priority if (memory.fileType === fileType) return true // Same file extension gets medium priority if (path.extname(memory.filePath) === fileExt) return true // Same directory gets low priority if (path.basename(path.dirname(memory.filePath)) === dirName) return true return false }) .sort((a, b) => { // Sort by relevance and recency let scoreA = 0 let scoreB = 0 // Exact file match if (a.filePath === filePath) scoreA += 100 if (b.filePath === filePath) scoreB += 100 // Same file type if (a.fileType === fileType) scoreA += 50 if (b.fileType === fileType) scoreB += 50 // Same file name if (path.basename(a.filePath) === fileName) scoreA += 25 if (path.basename(b.filePath) === fileName) scoreB += 25 // Recency (more recent = higher score) scoreA += (a.timestamp / 1000000) // Convert to manageable number scoreB += (b.timestamp / 1000000) return scoreB - scoreA }) .slice(0, limit) } getLearningPatterns(language: string): LearningPattern[] { const patterns = new Map<string, { frequency: number, successes: number, contexts: Set<string> }>() this.memories .filter(memory => memory.context.language === language) .forEach(memory => { memory.issuesFound.forEach(issue => { if (!patterns.has(issue)) { patterns.set(issue, { frequency: 0, successes: 0, contexts: new Set() }) } const pattern = patterns.get(issue)! pattern.frequency++ if (memory.successMetrics.syntaxValid) { pattern.successes++ } pattern.contexts.add(memory.context.framework || 'general') }) }) return Array.from(patterns.entries()) .map(([pattern, data]) => ({ pattern, frequency: data.frequency, successRate: data.frequency > 0 ? data.successes / data.frequency : 0, contexts: Array.from(data.contexts), recommendation: this.generateRecommendation(pattern, data.frequency, data.successes / data.frequency) })) .filter(p => p.frequency >= 2) // Only include patterns seen multiple times .sort((a, b) => b.frequency - a.frequency) } getProjectInsight(projectPath: string): ProjectInsight | undefined { return this.insights.get(projectPath) } generateContextualAdvice(filePath: string, fileType: string): string { // Check cache first const cacheKey = `${filePath}:${fileType}` const cached = this.adviceCache.get(cacheKey) if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) { return cached.advice } const relevantMemories = this.getRelevantMemories(filePath, fileType, 5) const language = this.detectLanguage(filePath) const patterns = this.getLearningPatterns(language) if (relevantMemories.length === 0 && patterns.length === 0) { const noDataAdvice = "No previous analysis data available for this file type." this.adviceCache.set(cacheKey, { advice: noDataAdvice, timestamp: Date.now() }) return noDataAdvice } let advice = "Based on previous analysis:\n" // Add pattern-based advice if (patterns.length > 0) { advice += "\nšŸ” Common Issues to Watch For:\n" patterns.slice(0, 3).forEach(pattern => { advice += `• ${pattern.pattern} (seen ${pattern.frequency} times, ${Math.round(pattern.successRate * 100)}% fix success)\n` }) } // Add file-specific advice if (relevantMemories.length > 0) { const commonChanges = this.extractCommonChanges(relevantMemories) if (commonChanges.length > 0) { advice += "\nšŸ”§ Effective Changes for Similar Files:\n" commonChanges.slice(0, 3).forEach(change => { advice += `• ${change}\n` }) } } // Add framework-specific advice const projectInsight = this.getProjectInsight(path.dirname(filePath)) if (projectInsight) { advice += "\nšŸ“‹ Project-Specific Guidelines:\n" projectInsight.bestPractices.slice(0, 3).forEach(practice => { advice += `• ${practice}\n` }) } // Cache the result this.adviceCache.set(cacheKey, { advice, timestamp: Date.now() }) return advice } private updateProjectInsights(memory: MemoryEntry): void { const projectPath = path.dirname(memory.filePath) let insight = this.insights.get(projectPath) if (!insight) { insight = { projectPath, lastAnalyzed: memory.timestamp, commonIssues: [], bestPractices: [], riskAreas: [], preferences: { codingStyle: [] } } } insight.lastAnalyzed = memory.timestamp // Update common issues memory.issuesFound.forEach(issue => { let pattern = insight!.commonIssues.find(p => p.pattern === issue) if (!pattern) { pattern = { pattern: issue, frequency: 0, successRate: 0, contexts: [], recommendation: "" } insight!.commonIssues.push(pattern) } pattern.frequency++ if (memory.successMetrics.syntaxValid) { pattern.successRate = (pattern.successRate * (pattern.frequency - 1) + 1) / pattern.frequency } // Add context if not already present const context = memory.context.framework || memory.context.language if (!pattern.contexts.includes(context)) { pattern.contexts.push(context) } }) // Update best practices from successful changes if (memory.successMetrics.syntaxValid && memory.successMetrics.testsPass) { memory.changesApplied.forEach(change => { if (!insight!.bestPractices.includes(change)) { insight!.bestPractices.push(change) } }) } this.insights.set(projectPath, insight) } private extractCommonChanges(memories: MemoryEntry[]): string[] { const changeFreq = new Map<string, number>() memories.forEach(memory => { memory.changesApplied.forEach(change => { changeFreq.set(change, (changeFreq.get(change) || 0) + 1) }) }) return Array.from(changeFreq.entries()) .filter(([_, freq]) => freq >= 2) .sort((a, b) => b[1] - a[1]) .map(([change, _]) => change) } private generateRecommendation(pattern: string, frequency: number, successRate: number): string { if (successRate > 0.8) { return `High-confidence fix available (${Math.round(successRate * 100)}% success rate)` } else if (successRate > 0.5) { return `Moderate-confidence fix available (${Math.round(successRate * 100)}% success rate)` } else if (frequency > 5) { return `Common issue - consider manual review (${Math.round(successRate * 100)}% auto-fix success)` } else { return `Uncommon issue - proceed with caution` } } private detectLanguage(filePath: string): string { const ext = path.extname(filePath).toLowerCase() const languageMap: { [key: string]: string } = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.java': 'java', '.cpp': 'cpp', '.c': 'c', '.cs': 'csharp', '.rb': 'ruby', '.php': 'php', '.go': 'go', '.rs': 'rust', '.swift': 'swift', '.kt': 'kotlin' } return languageMap[ext] || 'unknown' } getMemoryStats(): { totalMemories: number languageBreakdown: { [language: string]: number } successRate: number averageComplexity: number } { const languageBreakdown: { [language: string]: number } = {} let totalSuccesses = 0 let totalComplexity = 0 this.memories.forEach(memory => { const language = memory.context.language languageBreakdown[language] = (languageBreakdown[language] || 0) + 1 if (memory.successMetrics.syntaxValid) { totalSuccesses++ } totalComplexity += memory.context.complexity }) return { totalMemories: this.memories.length, languageBreakdown, successRate: this.memories.length > 0 ? totalSuccesses / this.memories.length : 0, averageComplexity: this.memories.length > 0 ? totalComplexity / this.memories.length : 0 } } clearOldMemories(daysOld: number = 30): number { const cutoffTime = Date.now() - (daysOld * 24 * 60 * 60 * 1000) const initialCount = this.memories.length this.memories = this.memories.filter(memory => memory.timestamp > cutoffTime) const removedCount = initialCount - this.memories.length if (removedCount > 0) { this.saveMemory().catch(console.warn) } return removedCount } }