Session Trees

A Session stores conversation history as a tree, not a flat list. Every entry has an id and parent_id; the head points at the current branch tip. Appending after a seek creates a new branch — history is never overwritten. This is the primitive behind:

  • Fork — try a different direction from any point
  • Checkpoints — label a state, come back to it later
  • Edit & re-run — change an earlier turn on a new branch; the original branch stays intact
#![allow(unused)]
fn main() {
use yoagent::{Agent, Session, provider::ModelConfig};

let mut session = Session::new();

// Run a turn, record it.
let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"));
let mut rx = agent.prompt("draft a plan").await;
while rx.recv().await.is_some() {}
agent.finish().await;
session.append_new(agent.messages())?;
session.checkpoint("first-draft")?;

// ... more turns ...

// Rewind to the checkpoint and branch.
session.seek_checkpoint("first-draft")?;
let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"))
    .with_messages(session.path_messages());   // only this branch's history
let mut rx = agent.prompt("actually, make it a library instead").await;
while rx.recv().await.is_some() {}
agent.finish().await;
session.append_new(agent.messages())?;          // new branch recorded

// Both branches exist:
assert_eq!(session.branch_tips().len(), 2);
}

Persistence: JSONL

#![allow(unused)]
fn main() {
std::fs::write("session.jsonl", session.to_jsonl())?;
let restored = Session::from_jsonl(&std::fs::read_to_string("session.jsonl")?)?;
}

One entry per line, append-friendly, diff-friendly. On load the head is the last line's entry, and the file is validated (duplicate ids and dangling/forward parent references are rejected — which also makes cycles impossible).

Compaction caveat: append_new verifies the agent's history still extends the session path and returns SessionError::HistoryDiverged when it doesn't. The usual cause is context compaction (on by default) rewriting the agent's messages — disable context management on session-tracked agents, or rebuild the branch with Session::from_messages after a divergence. The flat save_messages() / restore_messages() API remains for single-branch persistence.

API sketch

MethodPurpose
append(msg) -> idAdd as child of head, advance head
append_new(&agent_messages)Record everything beyond the current path (verifies the prefix; errors on divergence)
seek(id) / seek_checkpoint(label)Move the head (fork point)
checkpoint(label)Label the head
path_messages()Root→head messages — feed to Agent::with_messages
branch_tips() / children(id) / entries()Inspect the tree
to_jsonl() / from_jsonl()Persist / restore

GASP

This tree is a natural format for the GASP transcripts/ tier — the raw-conversation cold tier (the spec leaves its format open). The semantic event log lives in the gasp feature: a recorder over the AgentEvent stream that emits a conformance-checked agent repo.