/** * Consolidation Policy — 记忆固化触发决策(v0.7.3 P1-5) * * 背景:MemoryConsolidator 在每次 run 完成后无条件发起一次非流式 LLM 请求 * (30s 超时)判断本次对话是否有值得持久化的记忆。短寒暄/单轮问答同样触发, * 纯成本浪费且对 Provider 构成无意义请求压力。 * * 本模块把触发决策收敛为纯函数(可表测),决策输入: * - 总开关 memory.consolidationEnabled(fail-secure:仅显式 false 才关闭) * - 内容门控:本次回答 ≥ minChars 字符 **或** 本次 run 存在成功的工具调用 * (工具调用意味着产生了可沉淀的事实性上下文) * - 频率门控:距该会话上次固化 ≥ intervalMs(首次不设限,但仍受内容门控约束) * * 决策与执行解耦:本模块不做 IO,调用方(ipc/agent.ts)持有会话级 * lastConsolidationAt 状态并执行 consolidate。 */ export interface ConsolidationDecisionInput { /** 总开关(memory.consolidationEnabled;undefined/null 视为开启) */ enabled: boolean | null | undefined; /** 本次 Agent 最终回答的字符数 */ answerChars: number; /** 内容门控阈值(memory.consolidationMinChars,默认 200) */ minChars: number; /** 本次 run 是否存在成功的工具调用 */ hadSuccessfulToolCall: boolean; /** 该会话上次固化的时间戳(0 = 从未固化) */ lastConsolidationAt: number; /** 当前时间戳 */ now: number; /** 频率门控窗口(memory.consolidationIntervalMs,默认 600000) */ intervalMs: number; } export type ConsolidationDecision = | { consolidate: true; reason: 'content-and-frequency-pass' } | { consolidate: false; reason: 'disabled' | 'below-threshold' | 'throttled' }; /** 频率窗口合法下限(防误配 0/负值导致门控失效——0 等价于每条消息都固化) */ export const MIN_CONSOLIDATION_INTERVAL_MS = 60_000; /** 内容门控合法下限(防误配 0 导致纯寒暄也固化) */ export const MIN_CONSOLIDATION_MIN_CHARS = 20; /** * 判定本次 run 是否应触发记忆固化。 */ export function shouldConsolidate(input: ConsolidationDecisionInput): ConsolidationDecision { // 1. 总开关 —— fail-secure 语义由调用方负责(!== false 才视为开启后传入布尔) if (input.enabled === false) { return { consolidate: false, reason: 'disabled' }; } // 2. 内容门控:回答够长 或 有成功的工具调用(事实性上下文) const minChars = Math.max( MIN_CONSOLIDATION_MIN_CHARS, Number.isFinite(input.minChars) ? input.minChars : 200, ); const contentWorthy = input.answerChars >= minChars || input.hadSuccessfulToolCall; if (!contentWorthy) { return { consolidate: false, reason: 'below-threshold' }; } // 3. 频率门控:上次固化距今不足窗口 → 跳过(首次 lastConsolidationAt=0 不受限) const intervalMs = Math.max( MIN_CONSOLIDATION_INTERVAL_MS, Number.isFinite(input.intervalMs) ? input.intervalMs : 600_000, ); if (input.lastConsolidationAt > 0 && input.now - input.lastConsolidationAt < intervalMs) { return { consolidate: false, reason: 'throttled' }; } return { consolidate: true, reason: 'content-and-frequency-pass' }; }