The layers
durable — Survive Restarts
Sessions, the append-only transcript tree, and the store contract that let an agent outlive its process
- Persisting conversations past a process restart
- Implementing edit, retry, fork, or compaction on a conversation
- Deciding what a session ID should mean in your application
You can make an agent survive restarts with pkg/durable and pkg/session.
A session ID opens or resumes a conversation. Input persists before the inner loop starts, and each produced message persists as it ends. A new process opens the same ID and rebuilds the same model history.
store, err := fs.New("/var/lib/app/sessions") // or session.NewMemoryStore()
a, err := durable.New(ctx, durable.Model(model), durable.WithStore(store), durable.WithSessionID("ticket-8472"), // create it, or resume it agent.WithTools(lookupOrder), // agent options ride along // ...)
msgs, err := a.Run(ctx, durable.Text("Where is order order_1234?")).Wait()The store keeps the transcript, and nothing else. Titles, modes, model choices,
and UI state stay in your own tables, keyed by the same session ID. A Store
that holds product metadata leaks every application schema into the SDK.
The parent pointer is the tree
Every session.Entry carries an EntryHeader with ID, ParentID, and
CreatedAt. The store stays a flat append-only log. The pointers make it a tree,
and the agent tracks one leaf.
e1 ─── e2 ─── e3 ─── e4 the first answer, now abandoned └──── e5 ─── e6 ← leaf Branch(ctx, e2), then Run againa.LeafID() // where the next entry attachesa.Branch(ctx, "e2") // edit, retry, and rewind are all this
forked, err := a.Fork(ctx, "ticket-8472-what-if") // copy the active patha.Compact(ctx, durable.KeepTurns(4)) // summarize, remove nothingCompact appends a session.CompactionEntry and changes projection only. You can
still branch to an entry from before the compaction, and that branch returns the
uncompacted path.
One turn, four audiences
A durable run takes entries, not messages. That boundary lets one turn carry values for readers who must not see the same things.
| Entry | Model reads | Store keeps | Transcript shows |
|---|---|---|---|
durable.Text and friends | yes | yes | yes |
durable.Meta(…) | yes | yes | no |
durable.Ephemeral(…) | yes | no | no |
session.CustomEntry | no | yes | yes |
// A live reminder: the model reads it once, the store never sees it.a.Run(ctx, durable.Text("Rebase this branch."), durable.Ephemeral(durable.Text("The build is red since 14:02 UTC.")),)
// An application record the model must never read.a.Append(ctx, ArtifactEntry{ CustomEntry: session.CustomEntry{Kind: "artifact"}, // ...})
artifacts := session.Filter[ArtifactEntry](entries)Lifecycle events arrive outside the run
a, err := durable.New(ctx, durable.Model(model), durable.WithPublisher(pub), // session_init, branched, forked, compacted)The run stream carries the inner agent events under EventAgent. Session
lifecycle changes happen between runs, so a publisher receives them instead. The
publisher runs after the mutation commits.