@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
JavaScript
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