| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import fs from "node:fs/promises"; |
| import path from "node:path"; |
| import type { OmegaRecoveryEpisode } from "./episodic-recall.js"; |
| import { loadOmegaRecoveryEpisodeRecall } from "./episodic-recall.js"; |
|
|
| export type LearnedRule = { |
| errorPattern: string; |
| solution: string; |
| successCount: number; |
| failureCount: number; |
| confidence: number; |
| lastUpdated: string; |
| source: string; |
| }; |
|
|
| |
| |
| |
| async function loadRecentEpisodes( |
| workspaceRoot: string, |
| sessionKey: string, |
| maxEpisodes: number = 20, |
| ): Promise<Map<string, OmegaRecoveryEpisode[]>> { |
| try { |
| const episodes = await loadOmegaRecoveryEpisodeRecall({ |
| workspaceRoot, |
| sessionKey, |
| task: "", |
| validation: { expectsJson: false, expectedKeys: [], expectedPaths: [] }, |
| maxResults: maxEpisodes, |
| }); |
|
|
| |
| const grouped = new Map<string, OmegaRecoveryEpisode[]>(); |
| for (const episode of episodes) { |
| if (!episode.errorKind) continue; |
| if (!grouped.has(episode.errorKind)) { |
| grouped.set(episode.errorKind, []); |
| } |
| grouped.get(episode.errorKind)!.push(episode); |
| } |
| return grouped; |
| } catch { |
| return new Map(); |
| } |
| } |
|
|
| |
| |
| |
| function calculateConfidence(successCount: number, failureCount: number): number { |
| if (successCount + failureCount === 0) return 0; |
| const rate = successCount / (successCount + failureCount); |
| |
| const sampleSizeFactor = Math.min(1, (successCount + failureCount) / 10); |
| return rate * sampleSizeFactor; |
| } |
|
|
| |
| |
| |
| function suggestSolution(episodes: OmegaRecoveryEpisode[]): string { |
| const successful = episodes.filter((e) => e.status === "completed"); |
| if (successful.length === 0) return "Unknown solution (no successes yet)"; |
|
|
| |
| const latestSuccess = successful.sort((a, b) => b.updatedAt - a.updatedAt)[0]; |
|
|
| if (latestSuccess?.lastRoute) { |
| return `Apply route: ${latestSuccess.lastRoute}`; |
| } |
|
|
| if (latestSuccess?.nextRecoveryStep.reason) { |
| return latestSuccess.nextRecoveryStep.reason; |
| } |
|
|
| return "Review successful recovery steps"; |
| } |
|
|
| |
| |
| |
| function synthesizeRule(errorKind: string, episodes: OmegaRecoveryEpisode[]): LearnedRule { |
| const successful = episodes.filter((e) => e.status === "completed").length; |
| const failed = episodes.filter((e) => e.status !== "completed").length; |
| const confidence = calculateConfidence(successful, failed); |
|
|
| return { |
| errorPattern: errorKind, |
| solution: suggestSolution(episodes), |
| successCount: successful, |
| failureCount: failed, |
| confidence, |
| lastUpdated: new Date().toISOString(), |
| source: `episode-synthesis:${episodes.length}`, |
| }; |
| } |
|
|
| |
| |
| |
| function formatRuleMarkdown(rule: LearnedRule, ruleId: number): string { |
| const tier = |
| rule.confidence > 0.8 |
| ? "High Confidence" |
| : rule.confidence > 0.4 |
| ? "Medium Confidence" |
| : "Low Confidence"; |
|
|
| const lines = [ |
| `## Rule #${ruleId}: ${rule.errorPattern} (${tier})`, |
| "", |
| `**Error Pattern:** ${rule.errorPattern}`, |
| `**Solution:** ${rule.solution}`, |
| `**Evidence:** ${rule.successCount} successes, ${rule.failureCount} failures`, |
| `**Confidence:** ${(rule.confidence * 100).toFixed(1)}%`, |
| `**Last Updated:** ${rule.lastUpdated}`, |
| `**Source:** ${rule.source}`, |
| "", |
| ]; |
|
|
| return lines.join("\n"); |
| } |
|
|
| |
| |
| |
| |
| async function writeLearnedRules(workspaceRoot: string, rules: LearnedRule[]): Promise<void> { |
| const filePath = path.join(workspaceRoot, "memory", "omega-learned-rules.md"); |
|
|
| |
| await fs.mkdir(path.dirname(filePath), { recursive: true }); |
|
|
| |
| let currentContent = ""; |
| try { |
| currentContent = await fs.readFile(filePath, "utf-8"); |
| } catch { |
| |
| currentContent = ""; |
| } |
|
|
| |
| const newRules = rules |
| .sort((a, b) => b.confidence - a.confidence) |
| .map((rule, idx) => formatRuleMarkdown(rule, idx + 1)) |
| .join(""); |
|
|
| const timestamp = new Date().toISOString(); |
| const header = |
| currentContent.length === 0 |
| ? `# Learned Rules (OpenSkyNet)\n\nAuto-generated on ${timestamp}\n\n` |
| : ""; |
|
|
| const reflectionSection = `## Reflection Cycle (${timestamp})\n\n${newRules}\n`; |
|
|
| |
| const finalContent = header + currentContent + reflectionSection; |
| await fs.writeFile(filePath, finalContent, "utf-8"); |
| } |
|
|
| |
| |
| |
| |
| export async function executeAutoReflection(params: { |
| workspaceRoot: string; |
| sessionKey: string; |
| }): Promise<{ rulesGenerated: number; timestamp: string }> { |
| const timestamp = new Date().toISOString(); |
|
|
| |
| const groupedEpisodes = await loadRecentEpisodes( |
| params.workspaceRoot, |
| params.sessionKey, |
| 20, |
| ); |
|
|
| |
| const rules: LearnedRule[] = []; |
| for (const [errorKind, episodes] of groupedEpisodes) { |
| if (episodes.length >= 2) { |
| |
| const rule = synthesizeRule(errorKind, episodes); |
| if (rule.confidence > 0.3) { |
| |
| rules.push(rule); |
| } |
| } |
| } |
|
|
| |
| if (rules.length > 0) { |
| await writeLearnedRules(params.workspaceRoot, rules); |
| } |
|
|
| return { |
| rulesGenerated: rules.length, |
| timestamp, |
| }; |
| } |
|
|
| |
| |
| |
| export function shouldReflect(turnCount: number, reflectionIntervalTurns: number = 100): boolean { |
| return turnCount % reflectionIntervalTurns === 0 && turnCount > 0; |
| } |
|
|