import { randomUUID } from "node:crypto"; import type { ImageContent, TextContent } from "@mariozechner/pi-ai"; import type { AgentMessage } from "../../types.js"; import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.js"; import type { BranchSummaryEntry, CompactionEntry, CustomEntry, CustomMessageEntry, LabelEntry, MessageEntry, ModelChangeEntry, SessionContext, SessionInfo, SessionInfoEntry, SessionTree, SessionTreeEntry, SessionTreeStorage, ThinkingLevelChangeEntry, } from "../types.js"; function generateId(byId: { has(id: string): boolean }): string { for (let i = 0; i < 100; i++) { const id = randomUUID().slice(0, 8); if (!byId.has(id)) return id; } return randomUUID(); } export function buildSessionContext(entries: SessionTreeEntry[]): SessionContext { let thinkingLevel = "off"; let model: { provider: string; modelId: string } | null = null; let compaction: CompactionEntry | null = null; for (const entry of entries) { if (entry.type === "thinking_level_change") { thinkingLevel = entry.thinkingLevel; } else if (entry.type === "model_change") { model = { provider: entry.provider, modelId: entry.modelId }; } else if (entry.type === "message" && entry.message.role === "assistant") { model = { provider: entry.message.provider, modelId: entry.message.model }; } else if (entry.type === "compaction") { compaction = entry; } } const messages: AgentMessage[] = []; const appendMessage = (entry: SessionTreeEntry) => { if (entry.type === "message") { messages.push(entry.message); } else if (entry.type === "custom_message") { messages.push( createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp), ); } else if (entry.type === "branch_summary" && entry.summary) { messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)); } }; if (compaction) { messages.push(createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp)); const compactionIdx = entries.findIndex((e) => e.type === "compaction" && e.id === compaction.id); let foundFirstKept = false; for (let i = 0; i < compactionIdx; i++) { const entry = entries[i]!; if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true; if (foundFirstKept) appendMessage(entry); } for (let i = compactionIdx + 1; i < entries.length; i++) { appendMessage(entries[i]!); } } else { for (const entry of entries) { appendMessage(entry); } } return { messages, thinkingLevel, model }; } export class DefaultSessionTree implements SessionTree { private storage: SessionTreeStorage; constructor(storage: SessionTreeStorage) { this.storage = storage; } getLeafId(): Promise { return this.storage.getLeafId(); } getEntry(id: string): Promise { return this.storage.getEntry(id); } getEntries(): Promise { return this.storage.getEntries(); } async getBranch(fromId?: string): Promise { const leafId = fromId ?? (await this.storage.getLeafId()); return this.storage.getPathToRoot(leafId); } async buildContext(): Promise { return buildSessionContext(await this.getBranch()); } getSessionInfo(): Promise { return this.storage.getSessionInfo(); } async getLabel(id: string): Promise { const entries = await this.storage.getEntries(); for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]!; if (entry.type === "label" && entry.targetId === id) { return entry.label?.trim() || undefined; } } return undefined; } async getSessionName(): Promise { const entries = await this.storage.getEntries(); for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]!; if (entry.type === "session_info") { return entry.name?.trim() || undefined; } } return undefined; } private async makeEntryId(): Promise { const entries = await this.storage.getEntries(); return generateId(new Set(entries.map((entry) => entry.id))); } private async appendTypedEntry(entry: TEntry): Promise { await this.storage.appendEntry(entry); return entry.id; } async appendMessage(message: AgentMessage): Promise { return this.appendTypedEntry({ type: "message", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), message, } satisfies MessageEntry); } async appendThinkingLevelChange(thinkingLevel: string): Promise { return this.appendTypedEntry({ type: "thinking_level_change", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), thinkingLevel, } satisfies ThinkingLevelChangeEntry); } async appendModelChange(provider: string, modelId: string): Promise { return this.appendTypedEntry({ type: "model_change", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), provider, modelId, } satisfies ModelChangeEntry); } async appendCompaction( summary: string, firstKeptEntryId: string, tokensBefore: number, details?: T, fromHook?: boolean, ): Promise { return this.appendTypedEntry({ type: "compaction", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), summary, firstKeptEntryId, tokensBefore, details, fromHook, } satisfies CompactionEntry); } async appendBranchSummary( fromId: string, summary: string, details?: T, fromHook?: boolean, ): Promise { return this.appendTypedEntry({ type: "branch_summary", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), fromId, summary, details, fromHook, } satisfies BranchSummaryEntry); } async appendCustomEntry(customType: string, data?: unknown): Promise { return this.appendTypedEntry({ type: "custom", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), customType, data, } satisfies CustomEntry); } async appendCustomMessageEntry( customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details?: T, ): Promise { return this.appendTypedEntry({ type: "custom_message", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), customType, content, display, details, } satisfies CustomMessageEntry); } async appendLabelChange(targetId: string, label: string | undefined): Promise { return this.appendTypedEntry({ type: "label", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), targetId, label, } satisfies LabelEntry); } async appendSessionInfo(name: string): Promise { return this.appendTypedEntry({ type: "session_info", id: await this.makeEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), name: name.trim(), } satisfies SessionInfoEntry); } async moveTo(entryId: string | null): Promise { if (entryId !== null && !(await this.storage.getEntry(entryId))) { throw new Error(`Entry ${entryId} not found`); } await this.storage.setLeafId(entryId); } }