/** * Wake-up context builder for palace-augmented sessions. * * Builds a layered context string (L0/L1/L2) within a token budget. * Products provide the raw data; this module assembles and truncates. */ export interface WakeUpLayer { label: string; content: string; priority: number; } export interface WakeUpConfig { totalBudget: number; l0Budget: number; l1Budget: number; l2Budget: number; } export interface WakeUpContext { text: string; layers: { label: string; charCount: number }[]; totalChars: number; truncated: boolean; } /** * Approximate token count for a string. * Uses the rough heuristic of ~4 characters per token. */ export function estimateTokens(text: string): number { return Math.ceil(text.length / 4); } /** * Truncate text to fit within a token budget. * * @param text - Input text * @param maxTokens - Maximum tokens allowed * @returns Truncated text (with "..." appended if truncated) */ export function truncateToTokenBudget(text: string, maxTokens: number): string { if (!text) return ''; const maxChars = maxTokens * 4; if (text.length <= maxChars) return text; // Truncate at word boundary const truncated = text.slice(0, maxChars); const lastSpace = truncated.lastIndexOf(' '); const cutPoint = lastSpace > maxChars * 0.8 ? lastSpace : maxChars; return truncated.slice(0, cutPoint) + '...'; } /** * Build a wake-up context from L0/L1/L2 layers within a total token budget. * * Layer priority: * - L0 (identity/project context) — always included, smallest budget * - L1 (critical facts from recent memories) — high priority * - L2 (semantically relevant memories) — fills remaining budget * * @param l0 - Identity/project context string * @param l1 - Critical facts string * @param l2 - Semantically relevant memories string * @param config - Token budget configuration * @returns Assembled wake-up context with metadata */ export function buildWakeUpLayers( l0: string, l1: string, l2: string, config: WakeUpConfig ): WakeUpContext { const layers: { label: string; charCount: number }[] = []; const parts: string[] = []; let truncated = false; // L0: identity (always included) const l0Truncated = truncateToTokenBudget(l0, config.l0Budget); if (l0Truncated) { parts.push(`[Identity]\n${l0Truncated}`); layers.push({ label: 'L0:identity', charCount: l0Truncated.length }); if (l0Truncated.endsWith('...')) truncated = true; } // L1: critical facts const l1Truncated = truncateToTokenBudget(l1, config.l1Budget); if (l1Truncated) { parts.push(`[Critical Facts]\n${l1Truncated}`); layers.push({ label: 'L1:facts', charCount: l1Truncated.length }); if (l1Truncated.endsWith('...')) truncated = true; } // L2: semantic context (gets remaining budget) const usedTokens = estimateTokens(parts.join('\n\n')); const remainingBudget = Math.max(0, config.totalBudget - usedTokens); const l2Budget = Math.min(config.l2Budget, remainingBudget); const l2Truncated = truncateToTokenBudget(l2, l2Budget); if (l2Truncated) { parts.push(`[Relevant Memories]\n${l2Truncated}`); layers.push({ label: 'L2:semantic', charCount: l2Truncated.length }); if (l2Truncated.endsWith('...')) truncated = true; } const text = parts.join('\n\n'); return { text, layers, totalChars: text.length, truncated, }; } /** * Default wake-up configs for each product. */ export const WAKEUP_PRESETS: Record = { notelett: { totalBudget: 600, l0Budget: 50, l1Budget: 150, l2Budget: 400 }, mindlyst: { totalBudget: 800, l0Budget: 80, l1Budget: 200, l2Budget: 500 }, coding: { totalBudget: 800, l0Budget: 80, l1Budget: 200, l2Budget: 500 }, };