update
This commit is contained in:
@@ -0,0 +1,705 @@
|
||||
# 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<TMetadata>`
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
class MemoryStorage<TMetadata> implements SessionStorage<TMetadata> {
|
||||
// In-memory storage using Map
|
||||
// Good for: Testing, short-lived sessions
|
||||
// Not good for: Persistence across runs
|
||||
}
|
||||
```
|
||||
|
||||
#### JSONLStorage
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```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<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:
|
||||
|
||||
```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<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:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
class MemoryRepo<TMetadata> implements SessionRepo<TMetadata> {
|
||||
// In-memory storage using Map<string, Session<TMetadata>>
|
||||
// Good for: Testing, ephemeral sessions
|
||||
}
|
||||
```
|
||||
|
||||
#### JSONLRepo
|
||||
|
||||
```typescript
|
||||
class JSONLRepo<TMetadata> implements SessionRepo<TMetadata> {
|
||||
// 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
|
||||
Reference in New Issue
Block a user