# Session Architecture ## Overview The session system provides **persistent, branchable conversation history**. It's the storage layer that enables: - Conversation persistence across restarts - Branching to earlier points in conversation - Context window compaction - Session tree navigation --- ## Core Concepts ### 1. SessionTreeEntry The fundamental unit of session history: ```typescript type SessionTreeEntry = | MessageEntry | ModelChangeEntry | ThinkingLevelChangeEntry | ActiveToolsChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | LeafEntry | SessionInfoEntry; ``` **Key properties**: - `id`: Unique identifier (UUID v7) - `parentId`: Points to parent entry (forms tree structure) - `timestamp`: ISO 8601 string ### 2. Tree Structure ``` Entry tree (simplified): root (parentId: null) ├─► message (user #1) [id: 1] │ └─► message (assistant #1) [id: 2] │ └─► tool_result [id: 3] │ └─► message (user #2) [id: 4] │ └─► compaction [id: 5] ← New root for future │ ├─► retained messages here │ └─► message (assistant #2) [id: 6] │ └─► message (user #3) [id: 7] │ └─► leaf [id: 8] ← Current head │ └─► branch_summary [id: 9] ← Point where branch was created └─► message (user #4) [id: 10] └─► message (assistant #4) [id: 11] └─► leaf [id: 12] ``` ### 3. Context Building **Context** = Current state needed for LLM call: ```typescript interface SessionContext { systemPrompt: string; messages: AgentMessage[]; thinkingLevel: ThinkingLevel; model: { provider: string; modelId: string } | null; activeToolNames: string[] | null; } ``` **Building context** involves: 1. Tracing from leaf to root (path entries) 2. Applying transforms (compaction, etc.) 3. Projecting entries to messages 4. Deriving state (model, thinking level, active tools) --- ## Session Storage Interface ### `SessionStorage` ```typescript interface SessionStorage { // Metadata readonly id: string; readonly metadata: TMetadata; // Entry operations getLeafId(): Promise; setLeafId(id: string): Promise; getEntry(id: string): Promise; getEntries(options?: SessionEntryCursorOptions): Promise; getBranch(): Promise; // Write operations appendEntry(entry: SessionTreeEntry): Promise; // Branch operations fork(targetId: string): Promise; delete(): Promise; // Cleanup cleanup(): Promise; } ``` ### Built-in Implementations #### MemoryStorage ```typescript class MemoryStorage implements SessionStorage { // In-memory storage using Map // Good for: Testing, short-lived sessions // Not good for: Persistence across runs } ``` #### JSONLStorage ```typescript class JSONLStorage implements SessionStorage { // File-based storage using JSONL format // One file per entry: entries/{id}.json // Metadata file: metadata.json // Good for: Development, local sessions // Not good for: High-concurrency, production // File structure: // session/ // metadata.json // entries/ // {id1}.json // {id2}.json // ... } ``` --- ## Session Class ### `Session` High-level session API built on storage: ```typescript class Session { // Metadata readonly id: string; readonly storage: SessionStorage; // Read operations getMetadata(): Promise; getLeafId(): Promise; getEntry(id: string): Promise; getBranch(): Promise; buildContext(options?: SessionContextBuildOptions): Promise; // Write operations appendMessage(message: AgentMessage): Promise; appendModelChange(provider: string, modelId: string): Promise; appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise; appendActiveToolsChange(activeToolNames: string[]): Promise; appendCompaction(...): Promise; appendBranchSummary(...): Promise; appendCustomEntry(customType: string, data: unknown): Promise; appendCustomMessageEntry(...): Promise; appendLabel(targetId: string, label: string): Promise; appendSessionName(name: string): Promise; // Branch operations fork(targetId: string): Promise; delete(): Promise; } ``` --- ## Context Building Details ### Path Tracing **Goal**: Get all entries from leaf to root. ```typescript async function getPathEntries(session: Session): Promise { const path: SessionTreeEntry[] = []; let currentId = await session.getLeafId(); while (currentId !== null) { const entry = await session.getEntry(currentId); if (!entry) break; path.unshift(entry); currentId = entry.parentId; } return path; } ``` ### Default Transform **Purpose**: Apply compaction logic to context. ```typescript function defaultContextEntryTransform( pathEntries: readonly SessionTreeEntry[] ): SessionTreeEntry[] { let compaction: CompactionEntry | null = null; for (const entry of pathEntries) { if (entry.type === "compaction") { compaction = entry; } } if (!compaction) { return [...pathEntries]; // No compaction } // Compaction retains either: // 1. All entries after compaction (retainedTail) // 2. Entries from firstKeptEntryId to compaction (inclusive) const entries: SessionTreeEntry[] = [compaction]; const compactionIdx = pathEntries.findIndex(e => e.id === compaction.id); if (compaction.retainedTail) { // Include everything after compaction for (let i = compactionIdx + 1; i < pathEntries.length; i++) { entries.push(pathEntries[i]!); } return entries; } if (compaction.firstKeptEntryId) { // Include entries from firstKeptEntryId to compaction let foundFirstKept = false; for (let i = compactionIdx - 1; i >= 0; i--) { const entry = pathEntries[i]!; if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true; if (foundFirstKept) entries.unshift(entry); } } // Always include entries after compaction for (let i = compactionIdx + 1; i < pathEntries.length; i++) { entries.push(pathEntries[i]!); } return entries; } ``` ### Entry to Message Projection ```typescript function sessionEntryToContextMessages( entry: SessionTreeEntry, index: number, entries: readonly SessionTreeEntry[], options: SessionContextBuildOptions = {} ): AgentMessage[] { if (entry.type === "message") { return [entry.message as AgentMessage]; } if (entry.type === "custom_message") { return [createCustomMessage(...)]; } if (entry.type === "compaction") { return [ createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp), ...(entry.retainedTail ?? []) ]; } if (entry.type === "branch_summary" && entry.summary) { return [createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)]; } if (entry.type === "custom") { // Custom entry projectors can convert to messages return [...(options.entryProjectors?.[entry.customType]?.(entry, index, entries) ?? [])]; } return []; // Skip other entry types } ``` --- ## Branching ### What is Branching? Branching creates a **new session tree** from an existing one, starting at a specific point. **Example use case**: ``` Original tree: root → A → B → C → D (leaf) Branch at B: root → A → B → B' (leaf) ← New branch \ → C → D (leaf) ← Original branch ``` ### Fork Operation ```typescript async function fork(session: Session, targetId: string): Promise { // 1. Clone storage (copy entries up to targetId) const newStorage = await session.storage.fork(targetId); // 2. Create new session from storage const newSession = new Session({ storage: newStorage }); // 3. Set leaf to targetId await newSession.getStorage().setLeafId(targetId); return newSession; } ``` ### Branch Summary When branching, a **branch_summary** entry is created: ```typescript interface BranchSummaryEntry extends SessionTreeEntryBase { type: "branch_summary"; summary: string; // Human-readable summary details?: unknown; // Implementation details usage?: Usage; // LLM usage for generating summary fromId: string; // Entry ID where branch was created } ``` **Purpose**: Help model understand what happened in the branch. --- ## Compaction ### What is Compaction? Compaction replaces old conversation history with a **summary**, reducing context size. **Before compaction**: ``` message (user #1) message (assistant #1) tool_result message (user #2) message (assistant #2) tool_result ... (many more messages) ``` **After compaction**: ``` compaction (summary: "User asked X, assistant did Y, then Z...") message (assistant #3) ← Recent messages retained message (user #3) ``` ### Compaction Entry ```typescript interface CompactionEntry extends SessionTreeEntryBase { type: "compaction"; summary: string; // Summarized history firstKeptEntryId?: string; // First entry kept after compaction tokensBefore: number; // Context size before compaction details?: CompactionDetails; // File operations, etc. usage?: Usage; // LLM usage for generating summary retainedTail?: AgentMessage[]; // Recent messages stored inline } ``` ### Compaction Process ```typescript async function compact(session: Session): Promise { // 1. Get branch entries const entries = await session.getBranch(); // 2. Prepare compaction const preparation = prepareCompaction(entries, settings); // Identifies which messages to summarize, retained tail, etc. // 3. Generate summary using LLM const summary = await generateSummary( preparation.messagesToSummarize, preparation.retainedTail ); // 4. Create compaction entry const compactionEntry: CompactionEntry = { type: "compaction", id: uuidv7(), parentId: preparation.firstKeptEntry.parentId, timestamp: new Date().toISOString(), summary: summary.text, firstKeptEntryId: preparation.firstKeptEntry.id, tokensBefore: preparation.tokensBefore, details: { readFiles: preparation.fileOps.readFiles, modifiedFiles: preparation.fileOps.modifiedFiles }, usage: summary.usage }; // 5. Persist entry const compactionId = await session.storage.appendEntry(compactionEntry); return { summary: summary.text, firstKeptEntryId: preparation.firstKeptEntry.id, tokensBefore: preparation.tokensBefore, usage: summary.usage, retainedTail: preparation.retainedTail, details: compactionEntry.details }; } ``` --- ## Session Repositories ### `SessionRepo` Repository pattern for session management: ```typescript interface SessionRepo { // CRUD create(options: CreateSessionOptions): Promise>; open(id: string): Promise>; list(): Promise; delete(id: string): Promise; // Forking fork(id: string, targetId: string): Promise>; // Cleanup cleanup(): Promise; } ``` ### Built-in Implementations #### MemoryRepo ```typescript class MemoryRepo implements SessionRepo { // In-memory storage using Map> // Good for: Testing, ephemeral sessions } ``` #### JSONLRepo ```typescript class JSONLRepo implements SessionRepo { // File-based storage // Sessions stored in: sessions/{id}/ // Good for: Local development } ``` --- ## Entry Types Reference ### MessageEntry ```typescript interface MessageEntry extends SessionTreeEntryBase { type: "message"; message: AgentMessage; } ``` **Stored**: Every user/assistant/toolResult message ### ModelChangeEntry ```typescript interface ModelChangeEntry extends SessionTreeEntryBase { type: "model_change"; provider: string; modelId: string; } ``` **Stored**: When model is changed via `setModel()` ### ThinkingLevelChangeEntry ```typescript interface ThinkingLevelChangeEntry extends SessionTreeEntryBase { type: "thinking_level_change"; thinkingLevel: ThinkingLevel; } ``` **Stored**: When thinking level is changed via `setThinkingLevel()` ### ActiveToolsChangeEntry ```typescript interface ActiveToolsChangeEntry extends SessionTreeEntryBase { type: "active_tools_change"; activeToolNames: string[]; } ``` **Stored**: When active tools are changed via `setActiveTools()` ### CompactionEntry ```typescript interface CompactionEntry extends SessionTreeEntryBase { type: "compaction"; summary: string; firstKeptEntryId?: string; tokensBefore: number; details?: CompactionDetails; usage?: Usage; retainedTail?: AgentMessage[]; } ``` **Stored**: After compaction ### BranchSummaryEntry ```typescript interface BranchSummaryEntry extends SessionTreeEntryBase { type: "branch_summary"; summary: string; details?: unknown; usage?: Usage; fromId: string; } ``` **Stored**: When creating a branch ### CustomEntry ```typescript interface CustomEntry extends SessionTreeEntryBase { type: "custom"; customType: string; data: unknown; } ``` **Stored**: Custom application data (not visible to model) ### CustomMessageEntry ```typescript interface CustomMessageEntry extends SessionTreeEntryBase { type: "custom_message"; customType: string; content: string | (TextContent | ImageContent)[]; display: string; details: unknown; } ``` **Stored**: Custom messages that appear in conversation ### LabelEntry ```typescript interface LabelEntry extends SessionTreeEntryBase { type: "label"; targetId: string; // Entry ID being labeled label: string; } ``` **Stored**: User-assigned labels for entries ### LeafEntry ```typescript interface LeafEntry extends SessionTreeEntryBase { type: "leaf"; targetId: string; // Current leaf entry ID } ``` **Stored**: Updates to current session head ### SessionInfoEntry ```typescript interface SessionInfoEntry extends SessionTreeEntryBase { type: "session_info"; name: string; } ``` **Stored**: Session name/description --- ## Best Practices ### 1. Use Branching for Experiments ```typescript // Original branch await harness.prompt("Build a web app"); // Experiment branch const experimentalSession = await session.fork(leafId); const experimentalHarness = new AgentHarness({ ...options, session: experimentalSession }); await experimentalHarness.prompt("Try using React instead"); ``` ### 2. Compact Regularly ```typescript // After each turn, check if compaction needed if (estimateTokens(context) > MAX_TOKENS * 0.8) { await harness.compact(); } ``` ### 3. Use Custom Entries for Metadata ```typescript // Store application state without exposing to model await harness.appendMessage({ role: "custom", type: "task_progress", taskId: "abc123", steps: [...] }); // Custom entry won't appear in model context ``` ### 4. Label Important Points ```typescript // Mark important conversation points await harness.appendLabel(messageId, "IMPORTANT_DECISION"); await harness.appendLabel(messageId, "BLOCKER"); ``` ### 5. Handle Branching Gracefully ```typescript try { await harness.navigateTree(targetId, { summarize: true }); } catch (error) { if (error instanceof AgentHarnessError && error.code === "branch_summary") { // Branch summary failed, navigate without summary await harness.navigateTree(targetId, { summarize: false }); } } ``` --- ## Summary **Session architecture provides**: - Persistent conversation history (JSONL storage) - Branchable conversation trees - Context window compaction - Custom metadata and messages **Key operations**: - `buildContext()` → Get LLM context from tree - `appendMessage()` → Add message to tree - `fork()` → Create branch at point - `compact()` → Summarize history **Storage layers**: - `MemoryStorage` → Testing, ephemeral - `JSONLStorage` → Development, local