UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

251 lines (250 loc) 7.71 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../monitoring/logger.js"; import { FrameError, ErrorCode } from "../errors/index.js"; class FrameDigestGenerator { constructor(frameDb) { this.frameDb = frameDb; } /** * Generate digest for a frame */ generateDigest(frameId) { try { const frame = this.frameDb.getFrame(frameId); if (!frame) { throw new FrameError( `Frame not found: ${frameId}`, ErrorCode.FRAME_NOT_FOUND, { frameId } ); } const events = this.frameDb.getFrameEvents(frameId); const anchors = this.frameDb.getFrameAnchors(frameId); const text = this.generateTextDigest(frame, events, anchors); const structured = this.generateStructuredDigest(frame, events, anchors); return { text, structured }; } catch (error) { logger.error("Failed to generate frame digest", { frameId, error }); return { text: `Error generating digest for frame ${frameId}`, structured: { error: error.message } }; } } /** * Generate text summary of frame */ generateTextDigest(frame, events, anchors) { const lines = []; lines.push(`Frame: ${frame.name} (${frame.type})`); lines.push( `Duration: ${this.formatDuration(frame.created_at, frame.closed_at)}` ); lines.push(""); if (frame.inputs.goals) { lines.push(`Goals: ${frame.inputs.goals}`); } if (frame.inputs.constraints && frame.inputs.constraints.length > 0) { lines.push(`Constraints: ${frame.inputs.constraints.join(", ")}`); } const importantAnchors = anchors.filter((a) => a.priority >= 7).sort((a, b) => b.priority - a.priority); if (importantAnchors.length > 0) { lines.push(""); lines.push("Key Decisions & Facts:"); importantAnchors.forEach((anchor) => { lines.push(`- ${anchor.type}: ${anchor.text}`); }); } const eventSummary = this.summarizeEvents(events); if (eventSummary.length > 0) { lines.push(""); lines.push("Activity Summary:"); eventSummary.forEach((summary) => { lines.push(`- ${summary}`); }); } if (frame.outputs && Object.keys(frame.outputs).length > 0) { lines.push(""); lines.push("Outputs:"); Object.entries(frame.outputs).forEach(([key, value]) => { lines.push(`- ${key}: ${this.formatValue(value)}`); }); } return lines.join("\n"); } /** * Generate structured digest data */ generateStructuredDigest(frame, events, anchors) { const eventsByType = this.groupEventsByType(events); const anchorsByType = this.groupAnchorsByType(anchors); return { frameId: frame.frame_id, frameName: frame.name, frameType: frame.type, duration: { startTime: frame.created_at, endTime: frame.closed_at, durationMs: frame.closed_at ? (frame.closed_at - frame.created_at) * 1e3 : null }, activity: { totalEvents: events.length, eventsByType, eventTimeline: events.slice(-10).map((e) => ({ type: e.event_type, timestamp: e.ts, summary: this.summarizeEvent(e) })) }, knowledge: { totalAnchors: anchors.length, anchorsByType, keyDecisions: anchors.filter((a) => a.type === "DECISION" && a.priority >= 7).map((a) => a.text), constraints: anchors.filter((a) => a.type === "CONSTRAINT").map((a) => a.text), risks: anchors.filter((a) => a.type === "RISK").map((a) => a.text) }, outcomes: { outputs: frame.outputs, success: frame.state === "closed" && !this.hasErrorEvents(events), artifacts: this.extractArtifacts(events) }, metadata: { projectId: frame.project_id, runId: frame.run_id, parentFrameId: frame.parent_frame_id, depth: frame.depth } }; } /** * Summarize events into readable format */ summarizeEvents(events) { const summaries = []; const eventsByType = this.groupEventsByType(events); if (eventsByType.tool_call && eventsByType.tool_call.length > 0) { const toolCounts = this.countTools(eventsByType.tool_call); const toolSummary = Object.entries(toolCounts).map(([tool, count]) => `${tool} (${count})`).join(", "); summaries.push(`Tool calls: ${toolSummary}`); } if (eventsByType.decision && eventsByType.decision.length > 0) { summaries.push(`Made ${eventsByType.decision.length} decisions`); } if (eventsByType.observation && eventsByType.observation.length > 0) { summaries.push( `Recorded ${eventsByType.observation.length} observations` ); } const errorEvents = events.filter( (e) => e.payload.error || e.payload.status === "error" ); if (errorEvents.length > 0) { summaries.push(`Encountered ${errorEvents.length} errors`); } return summaries; } /** * Group events by type */ groupEventsByType(events) { const groups = {}; for (const event of events) { if (!groups[event.event_type]) { groups[event.event_type] = []; } groups[event.event_type].push(event); } return groups; } /** * Group anchors by type */ groupAnchorsByType(anchors) { const groups = {}; for (const anchor of anchors) { groups[anchor.type] = (groups[anchor.type] || 0) + 1; } return groups; } /** * Count tool usage */ countTools(toolEvents) { const counts = {}; for (const event of toolEvents) { const toolName = event.payload.tool_name || "unknown"; counts[toolName] = (counts[toolName] || 0) + 1; } return counts; } /** * Check if events contain errors */ hasErrorEvents(events) { return events.some((e) => e.payload.error || e.payload.status === "error"); } /** * Extract artifacts from events */ extractArtifacts(events) { const artifacts = []; for (const event of events) { if (event.event_type === "artifact" && event.payload.path) { artifacts.push(event.payload.path); } } return [...new Set(artifacts)]; } /** * Summarize a single event */ summarizeEvent(event) { switch (event.event_type) { case "tool_call": return `${event.payload.tool_name || "tool"}`; case "decision": return `${event.payload.type}: ${event.payload.content?.substring(0, 50)}...`; case "observation": return `${event.payload.content?.substring(0, 50)}...`; case "artifact": return `Created ${event.payload.path}`; default: return event.event_type; } } /** * Format duration */ formatDuration(startTime, endTime) { if (!endTime) { return "ongoing"; } const durationMs = (endTime - startTime) * 1e3; if (durationMs < 1e3) { return `${durationMs.toFixed(0)}ms`; } else if (durationMs < 6e4) { return `${(durationMs / 1e3).toFixed(1)}s`; } else { return `${(durationMs / 6e4).toFixed(1)}m`; } } /** * Format value for display */ formatValue(value) { if (typeof value === "string") { return value.length > 100 ? `${value.substring(0, 100)}...` : value; } else if (typeof value === "object") { return JSON.stringify(value).substring(0, 100) + "..."; } else { return String(value); } } } export { FrameDigestGenerator }; //# sourceMappingURL=frame-digest.js.map