UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

562 lines (561 loc) 23.3 kB
/** * MemoryCoordinator — 记忆系统统一协调器 * * 设计原则 (CoALA / MemGPT / Generative Agents / Mem0): * - Single Coordinator: 所有记忆操作通过此模块路由 * - Budget-Aware Injection: 记忆注入受统一 token 预算管控 * - Extract-Update Write Path: 写入经去重/合并/冲突解决 * - Graceful Degradation: 任意子系统故障不影响核心执行 * * 生命周期: * - Bootstrap 模式: 会话级 (orchestrator 创建, 贯穿所有维度) * - User Chat 模式: 实例级 (AgentRuntime 创建) * * @module MemoryCoordinator */ import Logger from '#infra/logging/Logger.js'; import { ActiveContext } from './ActiveContext.js'; // ── 预算分配策略 (§4.1) ── const BUDGET_PROFILES = Object.freeze({ user: { activeContext: 0.2, sessionStore: 0.0, persistentMemory: 0.6, conversationLog: 0.2, }, analyst: { activeContext: 0.45, sessionStore: 0.35, persistentMemory: 0.15, conversationLog: 0.05, }, producer: { activeContext: 0.25, sessionStore: 0.55, persistentMemory: 0.15, conversationLog: 0.05, }, }); /** 默认记忆 token 总预算 */ const DEFAULT_MEMORY_BUDGET = 4000; /** 副作用工具 — 不缓存结果 (B3 fix) */ const NON_CACHEABLE_TOOLS = new Set([ 'submit_knowledge', 'submit_with_check', 'note_finding', 'get_previous_analysis', 'get_previous_evidence', ]); // ── 写入路由: 规则匹配模式 ── const PREFERENCE_PATTERNS = [ /我们(项目|团队)?(不用|不使用|禁止|避免|偏好|习惯|规范是)/, /以后(都|请|要)/, /记住/, /we\s+(don'?t|never|always|prefer|avoid)\s+use/i, /remember\s+(to|that)/i, /our\s+(convention|standard|rule)\s+is/i, ]; const DECISION_PATTERNS = [ /决定(了|用|采用|使用)/, /(确认|同意|通过)(了|这个方案|审核)/, /就(这样|这么)(做|定|办)/, /let'?s\s+(go\s+with|use|adopt)/i, /approved|confirmed|decided/i, ]; const MEMORY_TAG_REGEX = /\[MEMORY:(\w+)\]\s*([\s\S]*?)\s*\[\/MEMORY\]/g; export class MemoryCoordinator { _lastSurplus = 0; // ── Config ── #mode; #totalBudget; #budgetAllocation; // ── Tier 3: Persistent (跨会话) ── #persistentMemory; #conversationLog; // ── Tier 2: Session (会话级) ── #sessionStore; // ── Tier 1: Dimension (维度级) ── #activeContexts; #currentScopeId; #logger; #completedScopes; /** * @param [config.persistentMemory] PersistentMemory 实例 * @param [config.sessionStore] SessionStore 实例 (bootstrap 模式) * @param [config.conversationLog] ConversationStore 实例 * @param [config.totalMemoryBudget=4000] 记忆 section 的 token 总预算 */ constructor(config = {}) { this.#persistentMemory = config.persistentMemory || null; this.#sessionStore = config.sessionStore || null; this.#conversationLog = config.conversationLog || null; this.#mode = config.mode || 'bootstrap'; this.#totalBudget = config.totalMemoryBudget || DEFAULT_MEMORY_BUDGET; this.#activeContexts = new Map(); this.#currentScopeId = null; this.#completedScopes = new Set(); this.#budgetAllocation = { activeContext: 0, sessionStore: 0, persistentMemory: 0, conversationLog: 0, }; this.#logger = Logger.getInstance(); // 应用默认预算 this.allocateBudget(this.#mode === 'user' ? 'user' : 'analyst'); } // ═══════════════════════════════════════════════════════════ // 预算管理 // ═══════════════════════════════════════════════════════════ /** * 配置总预算 (由 AgentRuntime.execute 入口调用) * @param options.totalContextBudget 模型总上下文 token 数 */ configure({ totalContextBudget, model } = {}) { if (totalContextBudget) { // 记忆 section 约占总上下文的 12.5% this.#totalBudget = Math.round(totalContextBudget * 0.125); } } /** * 按模式分配预算 * @param [totalTokens] 覆盖总预算 */ allocateBudget(mode, totalTokens) { if (totalTokens) { this.#totalBudget = totalTokens; } const profile = BUDGET_PROFILES[mode] || BUDGET_PROFILES.analyst; this.#budgetAllocation = { activeContext: Math.round(this.#totalBudget * profile.activeContext), sessionStore: Math.round(this.#totalBudget * profile.sessionStore), persistentMemory: Math.round(this.#totalBudget * profile.persistentMemory), conversationLog: Math.round(this.#totalBudget * profile.conversationLog), }; } getTotalBudget() { return this.#totalBudget; } getBudgetAllocation() { return { ...this.#budgetAllocation }; } /** * 获取消息缓冲区可用预算 (F2) * @param totalContextBudget 模型总上下文 token */ getMessageBudget(totalContextBudget, systemPromptEstimate = 2000, toolSchemaEstimate = 3000, safetyMargin = 3000) { return (totalContextBudget - this.#totalBudget - systemPromptEstimate - toolSchemaEstimate - safetyMargin); } // ═══════════════════════════════════════════════════════════ // 读取 (Prompt 构建) // ═══════════════════════════════════════════════════════════ /** * 构建静态记忆 prompt (execute 入口调用一次) * 包含: PersistentMemory + ConversationLog + SessionStore 上下文 * * @param [options.mode] 'user' | 'analyst' | 'producer' * @param [options.taskContext] 当前任务描述 (用于 relevance 打分) * @param [options.currentDimId] 当前维度 (用于 SessionStore 过滤) * @param [options.focusKeywords] 聚焦关键词 */ async buildStaticMemoryPrompt(options = {}) { const parts = []; let surplus = 0; try { // ── 1. PersistentMemory / Memory ── const pmBudget = this.#budgetAllocation.persistentMemory || 0; if (pmBudget > 0) { const pmSection = await this.#buildPersistentMemorySection(options); if (pmSection) { const used = this.#estimateTokens(pmSection); surplus += Math.max(0, pmBudget - used); parts.push(pmSection); } else { surplus += pmBudget; } } // ── 2. SessionStore (legacy: EpisodicMemory) ── const ssBudget = this.#budgetAllocation.sessionStore || 0; if (ssBudget > 0) { const ssSection = this.#buildSessionStoreSection(options); if (ssSection) { const used = this.#estimateTokens(ssSection); surplus += Math.max(0, ssBudget - used); parts.push(ssSection); } else { surplus += ssBudget; } } // ── 3. ConversationLog ── const clBudget = this.#budgetAllocation.conversationLog || 0; if (clBudget > 0 && this.#conversationLog) { // ConversationLog 通常通过 history 传入,此处预留 surplus += clBudget; } } catch (err) { this.#logger.warn(`[MemoryCoordinator] buildStaticMemoryPrompt error: ${err.message}`); } // 静态 prompt 不做二次重分配 (动态 prompt 使用 surplus) this._lastSurplus = surplus; return parts.filter(Boolean).join('\n'); } /** * 构建动态记忆 prompt (每轮调用) * 包含: ActiveContext / WorkingMemory 上下文 */ buildDynamicMemoryPrompt(options = {}) { try { const acBudget = (this.#budgetAllocation.activeContext || 0) + (this._lastSurplus || 0); if (acBudget <= 0) { return ''; } const ac = options.scopeId ? this.getActiveContext(options.scopeId) : this.#getCurrentActiveContext(); if (!ac) { return ''; } return ac.buildContext(acBudget) || ''; } catch (err) { this.#logger.warn(`[MemoryCoordinator] buildDynamicMemoryPrompt error: ${err.message}`); return ''; } } /** 合并构建完整记忆 prompt (便捷方法) */ async buildMemoryPrompt(options = {}) { const staticPart = await this.buildStaticMemoryPrompt(options); const dynamicPart = this.buildDynamicMemoryPrompt(options); return [staticPart, dynamicPart].filter(Boolean).join('\n'); } // ═══════════════════════════════════════════════════════════ // 写入 // ═══════════════════════════════════════════════════════════ /** * 记录工具调用观察 (合并 WM.observe + TRC.set) * @param round 当前迭代轮次 * @param [cacheHit=false] 本次是否缓存命中 */ recordObservation(toolName, args, result, round, cacheHit = false) { try { // ActiveContext 的数据记录由 trace.recordToolCall() 处理, // 此处只处理缓存写入。 // 委托给 SessionStore 缓存 if (!cacheHit && this.#sessionStore) { if (!NON_CACHEABLE_TOOLS.has(toolName)) { this.#sessionStore.cacheToolResult(toolName, args, result); } } } catch (err) { this.#logger.warn(`[MemoryCoordinator] recordObservation error: ${err.message}`); } } /** * 记录关键发现 (从 note_finding handler 调用) * @param importance 1-10 * @param [scopeId] 显式指定 scope (并行安全) * @returns 响应消息 */ noteFinding(finding, evidence, importance, round, scopeId) { try { const ac = scopeId ? this.getActiveContext(scopeId) : this.#getCurrentActiveContext(); if (ac) { ac.noteKeyFinding(finding, evidence, importance, round); return `📌 已记录发现 [${importance}/10]: "${finding.substring(0, 80)}" — 当前共 ${ac.scratchpadSize} 条关键发现`; } return '⚠ 工作记忆未初始化 (仅在 bootstrap 分析期间可用)'; } catch (err) { this.#logger.warn(`[MemoryCoordinator] noteFinding error: ${err.message}`); return `⚠ 记录发现失败: ${err.message}`; } } /** * 从对话中提取记忆 * * 写入路由 (WriteRouter): * - 规则 1: 只在 user 源触发规则匹配 (B4 fix) * - 规则 2: [MEMORY] 标签提取 (所有源) * * @param prompt 用户输入 * @param reply AI 回复 */ extractFromConversation(prompt, reply, source) { // §7.6 step 4: 只写 PersistentMemory (不再双写 Memory.js) if (!this.#persistentMemory) { return; } try { // ── 层 1: 规则快速匹配 (仅 user 源) ── if (source === 'user') { if (PREFERENCE_PATTERNS.some((p) => p.test(prompt))) { this.#persistentMemory.append({ type: 'preference', content: prompt.substring(0, 200), source, importance: 5, }); } if (DECISION_PATTERNS.some((p) => p.test(prompt))) { this.#persistentMemory.append({ type: 'fact', content: prompt.substring(0, 200), source, importance: 7, }); } } // ── 层 2: [MEMORY] 标签提取 (所有源) ── if (reply) { const regex = new RegExp(MEMORY_TAG_REGEX.source, MEMORY_TAG_REGEX.flags); let match; while ((match = regex.exec(reply)) !== null) { const type = match[1]; const content = match[2].trim(); if (content && ['preference', 'decision', 'context'].includes(type)) { this.#persistentMemory.append({ type: type === 'decision' ? 'fact' : type === 'context' ? 'fact' : type, content: content.substring(0, 200), source, importance: type === 'decision' ? 7 : 5, }); } } } } catch { /* memory write failure is non-critical */ } } // ═══════════════════════════════════════════════════════════ // 缓存代理 (委托到 SessionStore / legacy ToolResultCache) // ═══════════════════════════════════════════════════════════ /** 获取缓存的工具结果 */ getCachedResult(toolName, args) { try { if (NON_CACHEABLE_TOOLS.has(toolName)) { return null; } return this.#sessionStore?.getCachedResult(toolName, args) ?? null; } catch { return null; } } /** 缓存工具结果 */ cacheToolResult(toolName, args, result) { try { if (NON_CACHEABLE_TOOLS.has(toolName)) { return; } this.#sessionStore?.cacheToolResult(toolName, args, result); } catch { /* non-critical */ } } // ═══════════════════════════════════════════════════════════ // 维度生命周期 // ═══════════════════════════════════════════════════════════ /** * 创建维度作用域 (D2/D3: 多 scope 支持) * * @param scopeId 如 'api-patterns:analyst', 'api-patterns:producer' * @param [config.lightweight=false] 轻量模式 (User Chat) * @returns WorkingMemory (Phase 2) / ActiveContext (Phase 3) */ createDimensionScope(scopeId, config = {}) { this.#currentScopeId = scopeId; // Phase 3: 创建 ActiveContext 实例 const ac = new ActiveContext({ lightweight: config.lightweight || false, maxRecentRounds: config.maxRecentRounds || 3, }); this.#activeContexts.set(scopeId, ac); this.#logger.debug(`[MemoryCoordinator] scope created: ${scopeId} (ActiveContext)`); return ac; } /** * 完成维度: 蒸馏 + 存储到 SessionStore * @param [report] 附加报告数据 */ completeDimension(scopeId, report) { try { const ac = this.#activeContexts.get(scopeId); const distilled = ac ? ac.distill() : null; if (distilled && this.#sessionStore && report) { this.#sessionStore.storeDimensionReport(scopeId.replace(/:.*$/, ''), { ...report, workingMemoryDistilled: distilled, }); } // 清理 ActiveContext if (ac) { ac.clear(); this.#activeContexts.delete(scopeId); } this.#completedScopes.add(scopeId); // 切换当前 scope 到下一个或清空 if (this.#currentScopeId === scopeId) { this.#currentScopeId = null; } this.#logger.debug(`[MemoryCoordinator] scope completed: ${scopeId}`); } catch (err) { this.#logger.warn(`[MemoryCoordinator] completeDimension error: ${err.message}`); } } /** * 完成会话: 触发 Consolidator * @returns |null>} */ async completeSession() { try { this.#currentScopeId = null; this.#logger.info('[MemoryCoordinator] session completed'); return { consolidated: 0 }; } catch (err) { this.#logger.warn(`[MemoryCoordinator] completeSession error: ${err.message}`); return null; } } // ═══════════════════════════════════════════════════════════ // 状态查询 // ═══════════════════════════════════════════════════════════ /** 获取当前或指定 scope 的 ActiveContext / WorkingMemory */ getActiveContext(scopeId) { const id = scopeId || this.#currentScopeId; if (!id) { return null; } return this.#activeContexts.get(id) || null; } /** 获取 SessionStore */ getSessionStore() { return this.#sessionStore || null; } /** 获取 PersistentMemory */ getPersistentMemory() { return this.#persistentMemory || null; } /** 获取 ConversationLog / ConversationStore */ getConversationLog() { return this.#conversationLog || null; } // ═══════════════════════════════════════════════════════════ // 自动摘要 (F23) // ═══════════════════════════════════════════════════════════ /** 对话更新后触发自动摘要 */ async onConversationUpdated(conversationId, aiProvider) { if (!this.#conversationLog || !aiProvider) { return; } try { const messages = this.#conversationLog.load(conversationId, { tokenBudget: Infinity }); if (messages.length >= 12) { await this.#conversationLog.summarize(conversationId, { aiProvider }); } } catch { // 摘要失败不影响主流程 } } // ═══════════════════════════════════════════════════════════ // 断点续传 // ═══════════════════════════════════════════════════════════ /** 保存 checkpoint */ async checkpoint(projectRoot) { try { if (this.#sessionStore?.saveCheckpoint) { await this.#sessionStore.saveCheckpoint(projectRoot); } } catch (err) { this.#logger.warn(`[MemoryCoordinator] checkpoint error: ${err.message}`); } } /** 恢复 checkpoint */ async restore(projectRoot) { try { if (this.#sessionStore?.loadCheckpoint) { return await this.#sessionStore.loadCheckpoint(projectRoot); } return false; } catch (err) { this.#logger.warn(`[MemoryCoordinator] restore error: ${err.message}`); return false; } } // ═══════════════════════════════════════════════════════════ // 清理 // ═══════════════════════════════════════════════════════════ dispose() { for (const ac of this.#activeContexts.values()) { try { ac.clear(); } catch { /* non-critical */ } } this.#activeContexts.clear(); this.#sessionStore = null; this.#currentScopeId = null; this.#completedScopes.clear(); } // ═══════════════════════════════════════════════════════════ // 私有方法 // ═══════════════════════════════════════════════════════════ /** 获取当前 scope 的 ActiveContext */ #getCurrentActiveContext() { if (!this.#currentScopeId) { return null; } return this.#activeContexts.get(this.#currentScopeId) || null; } /** 构建 PersistentMemory section */ async #buildPersistentMemorySection(options = {}) { if (this.#persistentMemory?.toPromptSection) { return (await this.#persistentMemory.toPromptSection({ source: 'user' })) || ''; } return ''; } /** 构建 SessionStore section (legacy: EpisodicMemory) */ #buildSessionStoreSection(options = {}) { const ss = this.#sessionStore; if (!ss?.buildContextForDimension) { return ''; } const dimId = options.currentDimId; if (!dimId) { return ''; } try { return ss.buildContextForDimension(dimId, options.focusKeywords || []) || ''; } catch { return ''; } } /** 粗略估算 token 数 (CJK 感知) */ #estimateTokens(text) { if (!text) { return 0; } // 粗略: 英文 ~4 chars/token, 中文 ~2 chars/token const cjkCount = (text.match(/[\u4e00-\u9fff\u3000-\u303f]/g) || []).length; const restCount = text.length - cjkCount; return Math.ceil(cjkCount / 2 + restCount / 4); } } export default MemoryCoordinator;