abyss-ai
Version:
Autonomous AI coding agent - enhanced OpenCode with autonomous capabilities
392 lines (332 loc) ⢠12.1 kB
text/typescript
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
}
}