176 lines
5.0 KiB
TypeScript
176 lines
5.0 KiB
TypeScript
/**
|
|
* Wake-up context builder for NoteLett palace.
|
|
*
|
|
* Builds L0/L1/L2 layered context within a configurable token budget.
|
|
* Uses @bytelyst/palace shared wakeup builder for assembly + truncation.
|
|
*/
|
|
|
|
import { buildWakeUpLayers, WAKEUP_PRESETS, estimateTokens, truncateToTokenBudget } from '@bytelyst/palace';
|
|
import type { WakeUpContext } from '@bytelyst/palace';
|
|
import { config } from '../../lib/config.js';
|
|
import { embedText, stripHtmlForEmbedding } from '../../lib/embeddings.js';
|
|
import { getWing, listRooms, listMemories, searchSemantic, getWingSummary } from './repository.js';
|
|
import type { PalaceMemoryDoc, PalaceWingDoc } from './types.js';
|
|
|
|
export type { WakeUpContext };
|
|
|
|
export interface NoteLettWakeUpContext extends WakeUpContext {
|
|
wingId: string;
|
|
wingName: string;
|
|
}
|
|
|
|
const NOTELETT_PRESET = WAKEUP_PRESETS.notelett ?? {
|
|
totalBudget: 600, l0Budget: 50, l1Budget: 150, l2Budget: 400,
|
|
};
|
|
|
|
/**
|
|
* Build wake-up context for a workspace/wing.
|
|
*
|
|
* L0 — Identity: workspace name + room topics (~50 tokens)
|
|
* L1 — Critical Facts: recent decisions/preferences/insights (~150 tokens)
|
|
* L2 — Task Context: semantically relevant memories (~400 tokens)
|
|
*/
|
|
export async function buildNoteLettWakeUp(
|
|
userId: string,
|
|
productId: string,
|
|
wingId: string,
|
|
taskDescription?: string,
|
|
): Promise<NoteLettWakeUpContext> {
|
|
const wing = await getWing(userId, productId, wingId);
|
|
if (!wing) {
|
|
return {
|
|
text: '',
|
|
layers: [],
|
|
totalChars: 0,
|
|
truncated: false,
|
|
wingId,
|
|
wingName: 'Unknown',
|
|
};
|
|
}
|
|
|
|
const budget = {
|
|
...NOTELETT_PRESET,
|
|
totalBudget: config.PALACE_WAKE_UP_BUDGET,
|
|
};
|
|
|
|
const l0 = await buildL0Identity(userId, productId, wing);
|
|
const l1 = await buildL1CriticalFacts(userId, productId, wingId, wing);
|
|
const l2 = await buildL2TaskContext(userId, productId, wingId, taskDescription);
|
|
|
|
const ctx = buildWakeUpLayers(l0, l1, l2, budget);
|
|
|
|
return {
|
|
...ctx,
|
|
wingId,
|
|
wingName: wing.name,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* L0 — Identity: workspace name, description, room topics.
|
|
*/
|
|
async function buildL0Identity(
|
|
userId: string,
|
|
productId: string,
|
|
wing: PalaceWingDoc,
|
|
): Promise<string> {
|
|
const rooms = await listRooms(userId, productId, wing.id);
|
|
const roomNames = rooms.map(r => r.name).slice(0, 10).join(', ');
|
|
|
|
const parts = [`Workspace: ${wing.name}`];
|
|
if (wing.description) parts.push(wing.description);
|
|
if (roomNames) parts.push(`Topics: ${roomNames}`);
|
|
|
|
return parts.join('\n');
|
|
}
|
|
|
|
/**
|
|
* L1 — Critical Facts: recent decisions, preferences, insights.
|
|
* Uses cached L1 if available, otherwise regenerates.
|
|
*/
|
|
async function buildL1CriticalFacts(
|
|
userId: string,
|
|
productId: string,
|
|
wingId: string,
|
|
wing: PalaceWingDoc,
|
|
): Promise<string> {
|
|
// Try cached L1
|
|
if (wing.l1Cache && wing.l1CacheUpdatedAt) {
|
|
const cacheAge = Date.now() - new Date(wing.l1CacheUpdatedAt).getTime();
|
|
if (cacheAge < 24 * 3600_000) return wing.l1Cache;
|
|
}
|
|
|
|
// Rebuild from recent decisions + preferences + insights
|
|
const factHalls = ['decisions', 'preferences', 'insights'] as const;
|
|
const recentFacts: PalaceMemoryDoc[] = [];
|
|
|
|
for (const hall of factHalls) {
|
|
const mems = await listMemories(userId, productId, {
|
|
wingId,
|
|
hall,
|
|
limit: 5,
|
|
});
|
|
recentFacts.push(...mems);
|
|
}
|
|
|
|
if (recentFacts.length === 0) return '';
|
|
|
|
// Sort by recency
|
|
recentFacts.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
|
|
const lines = recentFacts.slice(0, 8).map(m => `- [${m.hall}] ${m.content}`);
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* L2 — Task Context: semantically relevant memories (or recent if no task).
|
|
*/
|
|
async function buildL2TaskContext(
|
|
userId: string,
|
|
productId: string,
|
|
wingId: string,
|
|
taskDescription?: string,
|
|
): Promise<string> {
|
|
let memories: PalaceMemoryDoc[];
|
|
|
|
if (taskDescription && taskDescription.trim().length > 0) {
|
|
const embedding = await embedText(taskDescription);
|
|
if (embedding) {
|
|
memories = await searchSemantic(userId, productId, taskDescription, embedding, wingId, 8);
|
|
} else {
|
|
memories = await listMemories(userId, productId, { wingId, limit: 8 });
|
|
}
|
|
} else {
|
|
memories = await listMemories(userId, productId, { wingId, limit: 8 });
|
|
}
|
|
|
|
if (memories.length === 0) return '';
|
|
|
|
const lines = memories.map(m => `- [${m.hall}] ${m.content}`);
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Regenerate L1 cache for a wing (called after memory extraction).
|
|
*/
|
|
export async function regenerateCriticalFacts(
|
|
userId: string,
|
|
productId: string,
|
|
wingId: string,
|
|
): Promise<string> {
|
|
const wing = await getWing(userId, productId, wingId);
|
|
if (!wing) return '';
|
|
|
|
const l1 = await buildL1CriticalFacts(userId, productId, wingId, wing);
|
|
|
|
// Update wing doc with cached L1
|
|
const { getCollection } = await import('../../lib/datastore.js');
|
|
const col = getCollection<PalaceWingDoc>('palace_wings', '/userId');
|
|
await col.update(wingId, userId, {
|
|
l1Cache: l1,
|
|
l1CacheUpdatedAt: new Date().toISOString(),
|
|
} as Partial<PalaceWingDoc>);
|
|
|
|
return l1;
|
|
}
|