import type { ISdk } from "iii-sdk"; import type { StateKV } from "../state/kv.js"; import { KV, generateId } from "../state/schema.js"; import type { Action, ActionEdge, RoutineRun, MemoryProvider } from "../types.js"; import { recordAudit } from "./audit.js"; const FLOW_COMPRESS_SYSTEM = `You are a workflow summarizer. Given a completed action chain, produce a concise summary capturing: 1. The overall goal and outcome 2. Key steps taken and their results 3. Any notable decisions or discoveries 4. Lessons learned Output as XML: What was the workflow trying to achieve What happened Numbered list of key steps Any new insights or discoveries What to remember for next time `; export function registerFlowCompressFunction( sdk: ISdk, kv: StateKV, provider: MemoryProvider, ): void { sdk.registerFunction("mem::flow-compress", async (data: { runId?: string; actionIds?: string[]; project?: string }) => { let actionsToCompress: Action[] = []; if (data.runId) { const run = await kv.get(KV.routineRuns, data.runId); if (!run) { return { success: false, error: "run not found" }; } for (const id of run.actionIds) { const action = await kv.get(KV.actions, id); if (action) actionsToCompress.push(action); } } else if (data.actionIds && data.actionIds.length > 0) { for (const id of data.actionIds) { const action = await kv.get(KV.actions, id); if (action) actionsToCompress.push(action); } } else if (data.project) { const allActions = await kv.list(KV.actions); actionsToCompress = allActions.filter( (a) => a.project === data.project && a.status === "done", ); } else { return { success: false, error: "runId, actionIds, or project is required", }; } const doneActions = actionsToCompress.filter( (a) => a.status === "done", ); if (doneActions.length === 0) { return { success: true, message: "No completed actions to compress", compressed: 0, }; } const allEdges = await kv.list(KV.actionEdges); const relevantIds = new Set(doneActions.map((a) => a.id)); const relevantEdges = allEdges.filter( (e) => relevantIds.has(e.sourceActionId) || relevantIds.has(e.targetActionId), ); const prompt = buildFlowPrompt(doneActions, relevantEdges); try { const response = await provider.summarize( FLOW_COMPRESS_SYSTEM, prompt, ); const summary = parseFlowSummary(response); const ts = new Date().toISOString(); const memory = { id: generateId("mem"), createdAt: ts, updatedAt: ts, type: "workflow" as const, title: summary.goal || `Workflow: ${doneActions.length} actions`, content: formatSummary(summary), concepts: extractConcepts(doneActions), files: extractFiles(doneActions), sessionIds: [], strength: 1.0, version: 1, isLatest: true, metadata: { flowCompressed: true, actionCount: doneActions.length, actionIds: doneActions.map((a) => a.id), }, }; await kv.set(KV.memories, memory.id, memory); await recordAudit(kv, "compress", "mem::flow-compress", [memory.id], { action: "compress_flow", flowCompressed: true, actionCount: doneActions.length, project: data.project, }); return { success: true, compressed: doneActions.length, memoryId: memory.id, summary, }; } catch (err) { return { success: false, error: `compression failed: ${String(err)}`, compressed: 0, }; } }, ); } function buildFlowPrompt( actions: Action[], edges: ActionEdge[], ): string { const lines: string[] = ["## Completed Action Chain\n"]; const sorted = [...actions].sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), ); for (const action of sorted) { lines.push(`### ${action.title}`); if (action.description) lines.push(action.description); if (action.result) lines.push(`Result: ${action.result}`); lines.push(`Priority: ${action.priority}, Tags: ${(action.tags ?? []).join(", ")}`); lines.push(""); } if (edges.length > 0) { lines.push("## Dependencies"); for (const edge of edges) { lines.push(`- ${edge.sourceActionId} --${edge.type}--> ${edge.targetActionId}`); } } return lines.join("\n"); } function parseFlowSummary(response: string): { goal: string; outcome: string; steps: string; discoveries: string; lesson: string; } { const extract = (tag: string): string => { const match = response.match( new RegExp(`<${tag}>([\\s\\S]*?)`), ); return match ? match[1].trim() : ""; }; return { goal: extract("goal"), outcome: extract("outcome"), steps: extract("steps"), discoveries: extract("discoveries"), lesson: extract("lesson"), }; } function formatSummary(s: { goal: string; outcome: string; steps: string; discoveries: string; lesson: string; }): string { const parts: string[] = []; if (s.goal) parts.push(`Goal: ${s.goal}`); if (s.outcome) parts.push(`Outcome: ${s.outcome}`); if (s.steps) parts.push(`Steps: ${s.steps}`); if (s.discoveries) parts.push(`Discoveries: ${s.discoveries}`); if (s.lesson) parts.push(`Lesson: ${s.lesson}`); return parts.join("\n\n"); } function extractConcepts(actions: Action[]): string[] { const concepts = new Set(); for (const a of actions) { for (const tag of a.tags ?? []) { if (!tag.startsWith("routine:")) concepts.add(tag); } } return Array.from(concepts); } function extractFiles(actions: Action[]): string[] { const files = new Set(); for (const a of actions) { if (a.metadata && typeof a.metadata === "object") { const meta = a.metadata as Record; if (Array.isArray(meta.files)) { for (const f of meta.files) { if (typeof f === "string") files.add(f); } } } } return Array.from(files); }