/** * pi-loom: Dream Engine * * Offline insight generation from stored memories: * Step 1: Low-cost multi-round weighted sampling * Step 2: Conflict-driven sampling * Step 3: Shuffle & non-causal combination * Step 4: LLM-driven insight generation โ†’ Pattern Memory Bank */ import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { callLLMForJSON, hasLLM, resolveMcpAuth, resolveModelForLLM } from "./model.js"; import type { LoomStore, MemRow, VisibilityFilter } from "./store.js"; export interface DreamConfig { samplerRoundCount: number; samplerRounds: number; conflictPairs: number; modelProvider?: string; modelId?: string; /** Filter sampling to a specific ESR entity. */ entityId?: string; /** Visibility boundary. Defaults to project/shared, excluding private. */ visibility?: VisibilityFilter; } export const DEFAULT_DREAM_CONFIG: DreamConfig = { samplerRoundCount: 10, samplerRounds: 2, conflictPairs: 5, }; export interface DreamResult { steps: number; sampled_count: number; insight_count: number; insights: MemRow[]; /** Auto-detected entity edges from Dream insights. */ suggested_edges: number; profiles_generated?: number; } /** * Run the full Dream Engine pipeline. */ export async function runDreamEngine( store: LoomStore, ctx: ExtensionContext | null, config: DreamConfig = DEFAULT_DREAM_CONFIG, ): Promise { const expired = store.expireOverdue(); if (ctx?.hasUI && expired > 0) { ctx.ui.notify(`๐Ÿงน Expired ${expired} memories`, "info"); } const seen = new Set(); const allSampled: MemRow[] = []; // โ”€โ”€ Step 1: Low-cost multi-round sampling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ for (let round = 0; round < config.samplerRounds; round++) { const sampled = config.entityId ? store.sampleByEntity(config.entityId, config.samplerRoundCount, config.visibility) : store.sampleWeighted(config.samplerRoundCount, config.visibility); for (const m of sampled) { if (!seen.has(m.id)) { seen.add(m.id); allSampled.push(m); } } // Also pull entity-neighbor memories via graph traversal if (config.entityId && round === 0) { const neighbors = store.traverseGraph([config.entityId], 1); for (const nid of neighbors) { if (nid === config.entityId) continue; const neighborMems = store.recallByEntity(nid, 3, config.visibility); for (const m of neighborMems) { if (!seen.has(m.id)) { seen.add(m.id); allSampled.push(m); } } } } } // โ”€โ”€ Step 2: Conflict-driven sampling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const conflicts = store.findConflicts(100, config.visibility); const conflictMems: MemRow[] = []; for (const pair of conflicts.slice(0, config.conflictPairs)) { for (const m of pair) { if (!seen.has(m.id)) { seen.add(m.id); conflictMems.push(m); } } } // โ”€โ”€ Step 3: Shuffle & non-causal combination โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const combined = [...allSampled, ...conflictMems]; const shuffled = [...combined].sort(() => Math.random() - 0.5); if (shuffled.length === 0) { return { steps: 0, sampled_count: 0, insight_count: 0, insights: [], suggested_edges: 0 }; } // โ”€โ”€ Step 4: LLM-driven insight generation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // โ”€โ”€ Zero-dependency fallback: skip LLM insight generation โ”€โ”€ if (!hasLLM(ctx)) { // Still do entity edge detection (pure stats, no LLM) let suggestedEdges = 0; const entityCounts = new Map(); for (const m of shuffled) { if (m.entity_id) entityCounts.set(m.entity_id, (entityCounts.get(m.entity_id) ?? 0) + 1); } const coMentioned = [...entityCounts.entries()].filter(([, c]) => c >= 2).map(([e]) => e); for (let i = 0; i < coMentioned.length; i++) { for (let j = i + 1; j < coMentioned.length; j++) { try { store.linkEntities({ source_entity: coMentioned[i], target_entity: coMentioned[j], relation_type: "RELATES_TO", confidence: 0.35 + 0.05 * entityCounts.get(coMentioned[i])!, }); suggestedEdges++; } catch { /* skip */ } } } console.error( `[pi-loom] Dream: sampled ${shuffled.length} memories, ${suggestedEdges} edges. Skipped insight generation (no LLM). Set DEEPSEEK_API_KEY or OPENAI_API_KEY to enable.`, ); if (ctx?.hasUI) ctx.ui.notify(`Dream: sampled ${shuffled.length} (no LLM โ€” insights skipped)`, "warning"); return { steps: 3, sampled_count: shuffled.length, insight_count: 0, insights: [], suggested_edges: suggestedEdges, }; } const model = resolveModelForLLM(ctx, config, "PI_DREAM_MODEL", "deepseek/deepseek-v3.1"); if (!model) { if (ctx?.hasUI) ctx.ui.notify("Dream: no model available", "warning"); return { steps: 0, sampled_count: shuffled.length, insight_count: 0, insights: [], suggested_edges: 0 }; } const auth = ctx ? await ctx.modelRegistry.getApiKeyAndHeaders(model) : resolveMcpAuth(model); if (!auth.ok || !auth.apiKey) { if (ctx?.hasUI) ctx.ui.notify("Dream: no API key", "warning"); return { steps: 0, sampled_count: shuffled.length, insight_count: 0, insights: [], suggested_edges: 0 }; } const memoryLines = shuffled.map((m) => { const preview = m.content.length > 120 ? `${m.content.slice(0, 117)}...` : m.content; return `[${m.id.slice(0, 6)}] e=${m.entity_id ?? "-"} imp=${m.importance.toFixed(1)}\n ${preview}`; }); const conflictLines = conflicts.map((pair) => pair.map((m) => ` - [${m.id.slice(0, 6)}] ${m.content.slice(0, 80)}`).join("\n vs\n"), ); const prompt = [ "You are a Dream Engine for an AI coding agent's memory system.", "Analyze the following memories and generate insights.", "", "## Sampled Memories", memoryLines.join("\n\n"), "", "## Potential Conflicts", conflictLines.length > 0 ? conflictLines.join("\n\n---\n\n") : "(no conflicts detected)", "", "## Task", "Generate 1-3 concise insights or patterns. Each insight should:", "1. Identify a pattern across multiple memories", "2. Note any contradictions or resolved tensions", "3. Suggest a generalization or lesson learned", "", "Output format (JSON array):", `[{"content": "insight text", "confidence": 0.0-1.0, "supporting_ids": ["mem_id1","mem_id2"]}]`, ].join("\n"); type InsightJSON = { content: string; confidence: number; supporting_ids: string[] }; const parsed = await callLLMForJSON(model, prompt, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: 2048, }); if (!parsed) { if (ctx?.hasUI) ctx.ui.notify("Dream: couldn't parse insights", "warning"); return { steps: 4, sampled_count: shuffled.length, insight_count: 0, insights: [], suggested_edges: 0 }; } const saved: MemRow[] = []; for (const item of parsed) { saved.push( store.storeInsight({ content: item.content, supporting_ids: item.supporting_ids ?? [], confidence: item.confidence, step: 4, visibility: config.visibility, }), ); } // โ”€โ”€ Step 5: Auto-suggest entity edges from co-mentioned entities โ”€โ”€ let suggestedEdges = 0; const entityCounts = new Map(); for (const m of shuffled) { if (m.entity_id) { entityCounts.set(m.entity_id, (entityCounts.get(m.entity_id) ?? 0) + 1); } } const coMentioned = [...entityCounts.entries()].filter(([, c]) => c >= 2).map(([e]) => e); // Pairwise edges between all co-mentioned entities in this Dream round for (let i = 0; i < coMentioned.length; i++) { for (let j = i + 1; j < coMentioned.length; j++) { try { store.linkEntities({ source_entity: coMentioned[i], target_entity: coMentioned[j], relation_type: "RELATES_TO", confidence: 0.35 + 0.05 * entityCounts.get(coMentioned[i])!, }); suggestedEdges++; } catch { /* best-effort */ } } } // Also link entities to insight IDs (entity โ†’ insight edge for drill-down) for (const insight of saved) { const derivation: Array<{ id: string }> = insight.derivation ? JSON.parse(insight.derivation) : []; const insightEntities = new Set(); for (const d of derivation) { if (d.id) { const mem = store.get(d.id); if (mem && !matchesVisibility(mem, config.visibility)) continue; if (mem?.entity_id) insightEntities.add(mem.entity_id); } } for (const eid of insightEntities) { try { store.linkEntities({ source_entity: eid, target_entity: `insight:${insight.id}`, relation_type: "INFORMS", confidence: insight.importance, }); suggestedEdges++; } catch { /* best-effort */ } } } if (ctx?.hasUI) { ctx.ui.notify(`๐Ÿ’ก Dream: ${saved.length} insights, ${suggestedEdges} entity edges`, "info"); } // โ”€โ”€ Step 6: TriMem profile generation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // For entities with โ‰ฅ5 memories in the sampled set, generate/update // a holistic profile portrait aggregating dispersed facts. let _profileCount = 0; try { const byEntity = new Map(); for (const m of shuffled) { if (!m.entity_id) continue; if (!byEntity.has(m.entity_id)) byEntity.set(m.entity_id, []); byEntity.get(m.entity_id)!.push(m.id); } for (const [eid, memIds] of byEntity) { if (memIds.length < 5) continue; const facts = memIds .map((id) => { const mem = store.get(id); if (mem && !matchesVisibility(mem, config.visibility)) return ""; return mem?.fact_summary ?? mem?.content ?? ""; }) .filter(Boolean); if (facts.length < 5) continue; const prompt = [ "You are an entity profiler. Synthesize a 1-2 sentence portrait of this entity", "from the atomic facts below. Capture what this entity IS, what it DOES,", "and its KEY RELATIONSHIPS/STATE. Be specific. Avoid restating individual facts.", "", `## Entity: ${eid}`, `## Facts (${facts.length}):`, facts.map((f, i) => `${i + 1}. ${f.slice(0, 200)}`).join("\n"), "", 'Output JSON: {"profile": "1-2 sentence entity portrait"}', ].join("\n"); type ProfileJSON = { profile: string }; const parsed = await callLLMForJSON(model, prompt, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: 256, }); if (parsed?.[0]?.profile) { store.storeProfile({ entity_id: eid, content: parsed[0].profile, supporting_ids: memIds, visibility: config.visibility, }); _profileCount++; } } } catch (err) { console.error("[pi-loom] Dream profile generation error:", err instanceof Error ? err.message : err); } return { steps: 6, sampled_count: shuffled.length, insight_count: saved.length, insights: saved, suggested_edges: suggestedEdges, }; } function matchesVisibility(mem: MemRow, visibility?: VisibilityFilter): boolean { if (visibility === "private") return mem.visibility === "private"; if (visibility === "shared") return mem.visibility === "shared"; return mem.visibility !== "private"; }