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.

274 lines (273 loc) 8.85 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { createPrivacyFilter } from "./privacy-filter.js"; import { logger } from "../monitoring/logger.js"; const DEFAULT_UNIFIED_CONTEXT_CONFIG = { totalTokenBudget: 8e3, userKnowledgeBudget: 0.2, // 20% taskContextBudget: 0.7, // 70% systemContextBudget: 0.1, // 10% privacyMode: "standard" }; function estimateTokens(content) { if (!content) return 0; return Math.ceil(content.length / 4); } function truncateToTokenBudget(content, tokenBudget) { if (!content) return ""; const estimatedTokens = estimateTokens(content); if (estimatedTokens <= tokenBudget) { return content; } const charLimit = tokenBudget * 4; const truncated = content.substring(0, charLimit); const lastSpace = truncated.lastIndexOf(" "); if (lastSpace > charLimit * 0.8) { return truncated.substring(0, lastSpace) + "..."; } return truncated + "..."; } class UnifiedContextAssembler { stackMemoryRetrieval; diffMemHooks; config; privacyFilter; constructor(stackMemoryRetrieval, diffMemHooks, config = {}) { this.stackMemoryRetrieval = stackMemoryRetrieval; this.diffMemHooks = diffMemHooks; this.config = { ...DEFAULT_UNIFIED_CONTEXT_CONFIG, ...config }; this.privacyFilter = createPrivacyFilter(this.config.privacyMode); const totalAllocation = this.config.userKnowledgeBudget + this.config.taskContextBudget + this.config.systemContextBudget; if (Math.abs(totalAllocation - 1) > 1e-3) { logger.warn("Budget allocations do not sum to 1.0", { userKnowledge: this.config.userKnowledgeBudget, taskContext: this.config.taskContextBudget, systemContext: this.config.systemContextBudget, total: totalAllocation }); } } /** * Assemble unified context from all sources */ async assemble(query) { const startTime = Date.now(); let totalPrivacyFiltered = 0; const userKnowledgeBudget = Math.floor( this.config.totalTokenBudget * this.config.userKnowledgeBudget ); const taskContextBudget = Math.floor( this.config.totalTokenBudget * this.config.taskContextBudget ); const systemContextBudget = Math.floor( this.config.totalTokenBudget * this.config.systemContextBudget ); const { content: userKnowledge, memories: diffMemMemories, available: diffMemAvailable } = await this.gatherUserKnowledge(query, userKnowledgeBudget); const userKnowledgeFiltered = this.privacyFilter.filter(userKnowledge); totalPrivacyFiltered += userKnowledgeFiltered.redactedCount; const filteredUserKnowledge = truncateToTokenBudget( userKnowledgeFiltered.filtered, userKnowledgeBudget ); const { content: taskContext, frameCount } = await this.gatherTaskContext( query, taskContextBudget ); const taskContextFiltered = this.privacyFilter.filter(taskContext); totalPrivacyFiltered += taskContextFiltered.redactedCount; const filteredTaskContext = truncateToTokenBudget( taskContextFiltered.filtered, taskContextBudget ); const systemContext = this.gatherSystemContext(systemContextBudget); const systemContextFiltered = this.privacyFilter.filter(systemContext); totalPrivacyFiltered += systemContextFiltered.redactedCount; const filteredSystemContext = truncateToTokenBudget( systemContextFiltered.filtered, systemContextBudget ); const combined = this.combineContextSections( filteredUserKnowledge, filteredTaskContext, filteredSystemContext ); const tokenUsage = { userKnowledge: estimateTokens(filteredUserKnowledge), taskContext: estimateTokens(filteredTaskContext), systemContext: estimateTokens(filteredSystemContext), total: estimateTokens(combined), budget: this.config.totalTokenBudget }; const metadata = { diffMemAvailable, diffMemMemories, stackMemoryFrames: frameCount, privacyFiltered: totalPrivacyFiltered }; logger.info("Unified context assembled", { query: query.substring(0, 50), tokenUsage, metadata, assemblyTimeMs: Date.now() - startTime }); return { userKnowledge: filteredUserKnowledge, taskContext: filteredTaskContext, systemContext: filteredSystemContext, combined, tokenUsage, metadata }; } /** * Gather user knowledge from DiffMem */ async gatherUserKnowledge(query, tokenBudget) { if (!this.diffMemHooks) { return { content: "", memories: 0, available: false }; } try { const status = await this.diffMemHooks.getStatus(); if (!status.connected) { logger.debug("DiffMem not connected"); return { content: "", memories: 0, available: false }; } const memories = await this.diffMemHooks.getRelevantMemories(query, 10); if (memories.length === 0) { return { content: "", memories: 0, available: true }; } const sections = ["## User Knowledge"]; const byCategory = /* @__PURE__ */ new Map(); for (const memory of memories) { const existing = byCategory.get(memory.category) || []; existing.push(memory); byCategory.set(memory.category, existing); } for (const [category, categoryMemories] of byCategory) { sections.push(` ### ${this.formatCategory(category)}`); for (const memory of categoryMemories) { const confidence = memory.confidence >= 0.8 ? "(high confidence)" : memory.confidence >= 0.5 ? "" : "(tentative)"; sections.push(`- ${memory.content} ${confidence}`); } } const content = sections.join("\n"); return { content: truncateToTokenBudget(content, tokenBudget), memories: memories.length, available: true }; } catch (error) { logger.warn("Failed to gather user knowledge from DiffMem", { error }); return { content: "", memories: 0, available: false }; } } /** * Format category name for display */ formatCategory(category) { return category.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); } /** * Gather task context from StackMemory */ async gatherTaskContext(query, tokenBudget) { try { const retrievedContext = await this.stackMemoryRetrieval.retrieveContext( query, { tokenBudget } ); return { content: retrievedContext.context, frameCount: retrievedContext.frames.length }; } catch (error) { logger.warn("Failed to gather task context from StackMemory", { error }); return { content: "", frameCount: 0 }; } } /** * Gather system context (environment, timestamps, etc.) */ gatherSystemContext(tokenBudget) { const sections = ["## System Context"]; sections.push(` **Current Time**: ${(/* @__PURE__ */ new Date()).toISOString()}`); const nodeEnv = process.env.NODE_ENV || "development"; sections.push(`**Environment**: ${nodeEnv}`); const projectId = process.env.STACKMEMORY_PROJECT_ID; if (projectId) { sections.push(`**Project**: ${projectId}`); } const sessionId = process.env.STACKMEMORY_SESSION_ID; if (sessionId) { sections.push(`**Session**: ${sessionId.substring(0, 8)}...`); } const content = sections.join("\n"); return truncateToTokenBudget(content, tokenBudget); } /** * Combine all context sections into a single string */ combineContextSections(userKnowledge, taskContext, systemContext) { const sections = []; if (taskContext) { sections.push(taskContext); } if (userKnowledge) { sections.push(userKnowledge); } if (systemContext) { sections.push(systemContext); } return sections.join("\n\n---\n\n"); } /** * Update privacy mode */ setPrivacyMode(mode) { this.config.privacyMode = mode; this.privacyFilter.setMode(mode); } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Update configuration */ updateConfig(config) { this.config = { ...this.config, ...config }; if (config.privacyMode) { this.privacyFilter.setMode(config.privacyMode); } } } function createUnifiedContextAssembler(stackMemoryRetrieval, diffMemHooks = null, config = {}) { return new UnifiedContextAssembler( stackMemoryRetrieval, diffMemHooks, config ); } export { DEFAULT_UNIFIED_CONTEXT_CONFIG, UnifiedContextAssembler, createUnifiedContextAssembler }; //# sourceMappingURL=unified-context-assembler.js.map