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_newverifies the agent's history still extends the session path and returnsSessionError::HistoryDivergedwhen 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 withSession::from_messagesafter a divergence. The flatsave_messages()/restore_messages()API remains for single-branch persistence.
API sketch
| Method | Purpose |
|---|---|
append(msg) -> id | Add 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.