New shared package: packages/palace/ (@bytelyst/palace) Modules: - types.ts — BasePalaceWingDoc, RoomDoc, MemoryDoc, TunnelDoc, KGTripleDoc, DiaryDoc - halls.ts — HallType union, HALL_PRESETS (notelett/mindlyst/coding), hallFromLabel() - cosine.ts — cosineSimilarity(), topKByCosine(), normalizeVector() - dedup.ts — isContentDuplicate(), isExactDuplicate(), findClosestMatch() - decay.ts — computeDecayedRelevance(), boostRelevance() - extraction.ts — buildExtractionPrompt(), parseExtractionResponse(), regexFallbackExtraction() - kg.ts — findContradictions(), mergeTriples(), isTripleCurrent() - wakeup.ts — buildWakeUpLayers(), truncateToTokenBudget(), WAKEUP_PRESETS - config.ts — palaceConfigSchema (Zod) 7 test files, 91 tests passing. Consumed by NoteLett, MindLyst, and future palace-enabled products.
127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
/**
|
|
* 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<string, WakeUpConfig> = {
|
|
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 },
|
|
};
|