/ src / lib / server / memory / memory-abstract.ts
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  }