How the Earendil Pi Agent Runtime Manages State: Session Trees and Append-Only Storage
The Earendil Pi agent runtime stores all interaction data in an immutable append-only session tree that records every user message, model change, thinking-level adjustment, compaction, and branch summary, enabling deterministic branching and persistent conversation history.
The state management system in the earendil-works/pi repository treats conversation history as a versioned tree rather than a flat message array. This architecture supports advanced features like non-linear branching, model switching mid-conversation, and crash-resistant persistence through multiple storage backends.
Core State Management Components
The Session Class
The Session class in packages/agent/src/harness/session/session.ts serves as the primary interface for state manipulation. It exposes high-level methods including appendMessage, appendModelChange, appendThinkingLevelChange, and appendCompaction, while internally coordinating with the storage layer. When the agent needs to send a request to the LLM, the class calls buildContext(), which invokes buildSessionContext (lines 21-75) to traverse the tree and compile the active conversation path into a SessionContext object.
Storage Abstractions
All persistence logic is abstracted behind the SessionStorage interface defined in packages/agent/src/types.ts. This decoupling allows the runtime to swap between volatile and durable storage without changing business logic. The system provides two concrete implementations:
InMemorySessionStorage(packages/agent/src/harness/session/memory-storage.ts): Stores the entire tree in RAM, ideal for testing or short-lived sessions.JsonlSessionStorage(packages/agent/src/harness/session/jsonl-storage.ts): Persists entries as line-delimited JSON, enabling session resumption after process termination.
How Session State Evolves
Creating and Initializing Sessions
Session initialization depends on the chosen storage backend. Ephemeral sessions use new InMemorySessionStorage(), while persistent sessions call await JsonlSessionStorage.open(fs, path) to load and replay existing conversation history. The toSession helper from packages/agent/src/harness/session/repo-utils.ts wraps the storage instance with the public API.
Appending Entries to the Session Tree
Every state modification creates a new node rather than mutating existing data. When methods like appendMessage execute, the runtime performs the following steps:
- Generates a unique entry ID using
uuidv7().slice(0,8)(fromrepo-utils.ts) - Records the current leaf ID as
parentIdto maintain tree structure - Stamps the entry with
new Date().toISOString() - Calls
storage.appendEntry(entry)to persist the update
The storage layer maintains internal maps (byId, labelsById) and updates the leaf pointer (leafIdAfterEntry) to reflect the new active state.
Branch Navigation and Undo/Redo
The session.moveTo(entryId, summary?) method enables non-linear conversation flows by repositioning the active leaf to any previous entry (or null for the root). When a summary is provided, the system inserts a branch_summary entry at the diversion point, creating a new timeline while preserving the original conversation path for potential future revisiting.
Building Context for LLM Calls
Before each inference request, the runtime constructs the prompt context by calling buildSessionContext. This function:
- Retrieves the path from the current leaf to the root via
storage.getPathToRoot - Extracts the active thinking level, model configuration, and any compaction boundaries
- Flattens the entries into an ordered
AgentMessage[]array that respects compaction limits
The resulting context contains { messages, thinkingLevel, model }, where messages represents the compiled conversation history ready for the AI provider.
Persistence with JSON-Lines
The JsonlSessionStorage implementation writes each entry as a separate line using fs.appendFile, ensuring that crashes never corrupt existing data. The file structure consists of:
- Line 1: A session header containing metadata (
id,timestamp,cwd) - Lines 2-N: Individual JSON objects representing entries (messages, model changes, compactions)
When reopening a session, the implementation replays the entire file to reconstruct the in-memory tree structure, leaf pointers, and label mappings.
Code Examples
Starting a New In-Memory Session
import { InMemorySessionStorage } from "./harness/session/memory-storage.ts";
import { toSession } from "./harness/session/repo-utils.ts";
const storage = new InMemorySessionStorage();
const session = toSession(storage);
Recording a User Message
await session.appendMessage({
role: "user",
content: "Explain how state works",
provider: "openai",
model: "gpt-4",
});
Switching Models and Thinking Levels
await session.appendModelChange("anthropic", "claude-2");
await session.appendThinkingLevelChange("on");
Adding Conversation Compaction
await session.appendCompaction(
"Summarized previous 20 messages",
firstKeptEntryId, // ID of the first entry retained after compaction
1500, // Token count before compaction occurred
);
Building Context for the Next LLM Call
const ctx = await session.buildContext();
// ctx.messages => ordered AgentMessage[]
// ctx.thinkingLevel => "on"
// ctx.model => { provider: "anthropic", modelId: "claude-2" }
Persisting to a JSON-Lines File
import { JsonlSessionStorage } from "./harness/session/jsonl-storage.ts";
const fs = /* a FileSystem implementation */;
const filePath = "/tmp/pi-session.jsonl";
const jsonl = await JsonlSessionStorage.create(fs, filePath, {
cwd: process.cwd(),
sessionId: "my-session",
});
const persisted = toSession(jsonl);
await persisted.appendMessage({ role: "user", content: "Hello" });
Summary
- The Earendil Pi agent uses an immutable append-only session tree where every interaction creates a new entry rather than modifying history.
- Branch navigation via
moveToenables undo/redo and parallel conversation timelines without data loss. - The
SessionStorageinterface abstracts persistence, supporting both in-memory (InMemorySessionStorage) and durable JSON-Lines (JsonlSessionStorage) backends. - Context building walks the tree from leaf to root to compile the active message list, model configuration, and thinking level for LLM requests.
- All state transitions are pure data operations that generate unique IDs via
uuidv7and timestamp entries with ISO 8601 strings.
Frequently Asked Questions
What is the difference between InMemorySessionStorage and JsonlSessionStorage?
InMemorySessionStorage keeps the entire session tree in RAM and loses data when the process exits, making it suitable for testing or transient interactions. JsonlSessionStorage writes each entry as a newline-delimited JSON record to disk, allowing sessions to survive crashes and be resumed later by replaying the file.
How does the Pi agent handle conversation branching or undo operations?
The runtime implements branching through the session.moveTo(entryId, summary?) method, which changes the active leaf pointer to any previous entry in the tree. This creates a new timeline from that point forward while preserving the original branch, effectively providing unlimited undo and parallel conversation paths.
What happens to old messages during compaction?
When appendCompaction is called, the system inserts a special compaction entry that marks a boundary in the tree. The buildSessionContext function recognizes this boundary and omits messages prior to the compaction point from the LLM context, while the full history remains in storage for audit purposes.
Is the session data format human-readable?
Yes, the JSON-Lines format used by JsonlSessionStorage produces plain text files where each line is a valid JSON object. The first line contains session metadata, and subsequent lines contain typed entries (messages, model changes, compactions) that can be inspected with standard text editors or UNIX tools like jq.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →