import { mkdirSync, readFileSync, existsSync, unlinkSync } from 'node:fs' import { atomicWriteFileSync } from '../shared/atomic-write' import { join } from 'node:path' import { ExperienceRuleExtractor, type ExperienceRule } from './experience-rules.js' import { miphamHome } from '../core/paths.ts' const MAX_EXPERIENCES = 20 export class AgentExperience { private readonly expFile: string private readonly expDir: string constructor( private readonly agentName: string, baseDir: string = miphamHome('agent-memory'), ) { this.expDir = join(baseDir, agentName) this.expFile = join(this.expDir, 'experience.md') } logSuccess(description: string, whenToApply: string): void { const date = new Date().toISOString().slice(0, 10) const entry = `- [${date}] ${description}\n **When to apply:** ${whenToApply}\n` this.appendToSection('## Success Patterns', entry, 'success') } logFailure(description: string, whenToAvoid: string): void { const date = new Date().toISOString().slice(0, 10) const entry = `- [${date}] ${description}\n **When to avoid:** ${whenToAvoid}\n` this.appendToSection('## Failure Patterns', entry, 'failure') } getExperience(): string { if (!existsSync(this.expFile)) return '' try { return readFileSync(this.expFile, 'utf-8') } catch { return '' } } reset(): void { if (existsSync(this.expFile)) { try { unlinkSync(this.expFile) } catch { /* ok */ } } } getRules(): ExperienceRule[] { const content = this.getExperience() if (!content) return [] const extractor = new ExperienceRuleExtractor() return extractor.extract(content, this.agentName) } private appendToSection(section: string, entry: string, type: 'success' | 'failure'): void { mkdirSync(this.expDir, { recursive: true }) let content = this.getExperience() if (!content) { content = `# Agent Experience — ${this.agentName}\n\n## Success Patterns\n\n## Failure Patterns\n\n## Stats\n- 总执行: 0 次 | 成功: 0 | 失败: 0\n` } // Find section and append entry const sectionIndex = content.indexOf(section) if (sectionIndex === -1) { // Section missing — add before Stats const statsIndex = content.indexOf('## Stats') if (statsIndex !== -1) { content = content.slice(0, statsIndex) + `${section}\n${entry}\n` + content.slice(statsIndex) } else { content += `\n${section}\n${entry}\n` } } else { // Find next section header after this one const nextSection = content.indexOf('\n## ', sectionIndex + section.length) const insertAt = nextSection !== -1 ? nextSection : content.length content = content.slice(0, insertAt) + entry + content.slice(insertAt) } // Trim old entries if over limit const lines = content.split('\n') let finalEntries = lines.filter((l) => l.startsWith('- [')) while (finalEntries.length > MAX_EXPERIENCES) { // Remove oldest entry (first one found) const firstEntryIdx = lines.findIndex((l) => l.startsWith('- [')) if (firstEntryIdx !== -1) { const nextEntryLine = lines[firstEntryIdx + 1] const removeCount = nextEntryLine?.startsWith(' **') ? 2 : 1 lines.splice(firstEntryIdx, removeCount) finalEntries = lines.filter((l) => l.startsWith('- [')) } else { break } } content = lines.join('\n') // Update stats (merged from incrementStat — single file write) content = content.replace( /- 总执行: (\d+) 次 \| 成功: (\d+) \| 失败: (\d+)/, (_match, total: string, success: string, failure: string) => { const newTotal = parseInt(total) + 1 const newSuccess = type === 'success' ? parseInt(success) + 1 : parseInt(success) const newFailure = type === 'failure' ? parseInt(failure) + 1 : parseInt(failure) return `- 总执行: ${newTotal} 次 | 成功: ${newSuccess} | 失败: ${newFailure}` }, ) atomicWriteFileSync(this.expFile, content, { mode: 0o644 }) } }