16 KiB
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:
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:
interface SessionContext {
systemPrompt: string;
messages: AgentMessage[];
thinkingLevel: ThinkingLevel;
model: { provider: string; modelId: string } | null;
activeToolNames: string[] | null;
}
Building context involves:
- Tracing from leaf to root (path entries)
- Applying transforms (compaction, etc.)
- Projecting entries to messages
- Deriving state (model, thinking level, active tools)
Session Storage Interface
SessionStorage<TMetadata>
interface SessionStorage<TMetadata extends SessionMetadata = SessionMetadata> {
// Metadata
readonly id: string;
readonly metadata: TMetadata;
// Entry operations
getLeafId(): Promise<string | null>;
setLeafId(id: string): Promise<void>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]>;
getBranch(): Promise<SessionTreeEntry[]>;
// Write operations
appendEntry(entry: SessionTreeEntry): Promise<string>;
// Branch operations
fork(targetId: string): Promise<SessionStorage>;
delete(): Promise<void>;
// Cleanup
cleanup(): Promise<void>;
}
Built-in Implementations
MemoryStorage
class MemoryStorage<TMetadata> implements SessionStorage<TMetadata> {
// In-memory storage using Map
// Good for: Testing, short-lived sessions
// Not good for: Persistence across runs
}
JSONLStorage
class JSONLStorage<TMetadata> implements SessionStorage<TMetadata> {
// 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<TMetadata>
High-level session API built on storage:
class Session<TMetadata extends SessionMetadata = SessionMetadata> {
// Metadata
readonly id: string;
readonly storage: SessionStorage<TMetadata>;
// Read operations
getMetadata(): Promise<TMetadata>;
getLeafId(): Promise<string>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getBranch(): Promise<SessionTreeEntry[]>;
buildContext(options?: SessionContextBuildOptions): Promise<SessionContext>;
// Write operations
appendMessage(message: AgentMessage): Promise<string>;
appendModelChange(provider: string, modelId: string): Promise<string>;
appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise<string>;
appendActiveToolsChange(activeToolNames: string[]): Promise<string>;
appendCompaction(...): Promise<string>;
appendBranchSummary(...): Promise<string>;
appendCustomEntry(customType: string, data: unknown): Promise<string>;
appendCustomMessageEntry(...): Promise<string>;
appendLabel(targetId: string, label: string): Promise<void>;
appendSessionName(name: string): Promise<string>;
// Branch operations
fork(targetId: string): Promise<Session>;
delete(): Promise<void>;
}
Context Building Details
Path Tracing
Goal: Get all entries from leaf to root.
async function getPathEntries(session: Session): Promise<SessionTreeEntry[]> {
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.
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
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
async function fork(session: Session, targetId: string): Promise<Session> {
// 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:
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
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
async function compact(session: Session): Promise<CompactionResult> {
// 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<TMetadata>
Repository pattern for session management:
interface SessionRepo<TMetadata extends SessionMetadata = SessionMetadata> {
// CRUD
create(options: CreateSessionOptions<TMetadata>): Promise<Session<TMetadata>>;
open(id: string): Promise<Session<TMetadata>>;
list(): Promise<SessionInfo[]>;
delete(id: string): Promise<void>;
// Forking
fork(id: string, targetId: string): Promise<Session<TMetadata>>;
// Cleanup
cleanup(): Promise<void>;
}
Built-in Implementations
MemoryRepo
class MemoryRepo<TMetadata> implements SessionRepo<TMetadata> {
// In-memory storage using Map<string, Session<TMetadata>>
// Good for: Testing, ephemeral sessions
}
JSONLRepo
class JSONLRepo<TMetadata> implements SessionRepo<TMetadata> {
// File-based storage
// Sessions stored in: sessions/{id}/
// Good for: Local development
}
Entry Types Reference
MessageEntry
interface MessageEntry extends SessionTreeEntryBase {
type: "message";
message: AgentMessage;
}
Stored: Every user/assistant/toolResult message
ModelChangeEntry
interface ModelChangeEntry extends SessionTreeEntryBase {
type: "model_change";
provider: string;
modelId: string;
}
Stored: When model is changed via setModel()
ThinkingLevelChangeEntry
interface ThinkingLevelChangeEntry extends SessionTreeEntryBase {
type: "thinking_level_change";
thinkingLevel: ThinkingLevel;
}
Stored: When thinking level is changed via setThinkingLevel()
ActiveToolsChangeEntry
interface ActiveToolsChangeEntry extends SessionTreeEntryBase {
type: "active_tools_change";
activeToolNames: string[];
}
Stored: When active tools are changed via setActiveTools()
CompactionEntry
interface CompactionEntry extends SessionTreeEntryBase {
type: "compaction";
summary: string;
firstKeptEntryId?: string;
tokensBefore: number;
details?: CompactionDetails;
usage?: Usage;
retainedTail?: AgentMessage[];
}
Stored: After compaction
BranchSummaryEntry
interface BranchSummaryEntry extends SessionTreeEntryBase {
type: "branch_summary";
summary: string;
details?: unknown;
usage?: Usage;
fromId: string;
}
Stored: When creating a branch
CustomEntry
interface CustomEntry extends SessionTreeEntryBase {
type: "custom";
customType: string;
data: unknown;
}
Stored: Custom application data (not visible to model)
CustomMessageEntry
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
interface LabelEntry extends SessionTreeEntryBase {
type: "label";
targetId: string; // Entry ID being labeled
label: string;
}
Stored: User-assigned labels for entries
LeafEntry
interface LeafEntry extends SessionTreeEntryBase {
type: "leaf";
targetId: string; // Current leaf entry ID
}
Stored: Updates to current session head
SessionInfoEntry
interface SessionInfoEntry extends SessionTreeEntryBase {
type: "session_info";
name: string;
}
Stored: Session name/description
Best Practices
1. Use Branching for Experiments
// 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
// After each turn, check if compaction needed
if (estimateTokens(context) > MAX_TOKENS * 0.8) {
await harness.compact();
}
3. Use Custom Entries for Metadata
// 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
// Mark important conversation points
await harness.appendLabel(messageId, "IMPORTANT_DECISION");
await harness.appendLabel(messageId, "BLOCKER");
5. Handle Branching Gracefully
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 treeappendMessage()→ Add message to treefork()→ Create branch at pointcompact()→ Summarize history
Storage layers:
MemoryStorage→ Testing, ephemeralJSONLStorage→ Development, local