memory-abstract.ts
1 /** 2 * Generates concise abstracts (~100 tokens) for memory entries. 3 * Inspired by OpenViking's L0/L1/L2 tiered context representations. 4 * 5 * Used in proactive recall to inject summaries instead of truncated raw content, 6 * reducing token waste and preserving semantic meaning. 7 */ 8 import { HumanMessage } from '@langchain/core/messages' 9 10 const ABSTRACT_TIMEOUT_MS = 15_000 11 12 /** 13 * Generate a short abstract (~100 tokens) summarizing memory content. 14 * Falls back to a truncated prefix if LLM generation fails or is unavailable. 15 */ 16 export async function generateAbstract(content: string, title?: string): Promise<string | null> { 17 if (!content || content.length <= 200) return null 18 19 try { 20 const { buildLLM } = await import('@/lib/server/build-llm') 21 const { llm } = await buildLLM() 22 23 const prompt = [ 24 'Summarize the following memory entry in 1-2 concise sentences (max ~100 tokens).', 25 'Preserve the key facts, decisions, or conclusions. Do not add commentary.', 26 title ? `Title: ${title}` : '', 27 `Content: ${content.slice(0, 2000)}`, 28 ].filter(Boolean).join('\n') 29 30 const response = await Promise.race([ 31 llm.invoke([new HumanMessage(prompt)]), 32 new Promise<never>((_, reject) => 33 setTimeout(() => reject(new Error('abstract-timeout')), ABSTRACT_TIMEOUT_MS), 34 ), 35 ]) 36 37 const text = extractText(response.content) 38 return text || fallbackAbstract(content) 39 } catch { 40 return fallbackAbstract(content) 41 } 42 } 43 44 function fallbackAbstract(content: string): string { 45 return content.slice(0, 150) + (content.length > 150 ? '...' : '') 46 } 47 48 function extractText(content: unknown): string { 49 if (typeof content === 'string') return content.trim() 50 if (Array.isArray(content)) { 51 for (const part of content) { 52 if (typeof part === 'string') return part.trim() 53 if (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') { 54 return part.text.trim() 55 } 56 } 57 } 58 return '' 59 }