Sessions
Persist conversation history across runs in a provider-neutral format.
A session stores the conversation as provider-neutral messages. zidane saves each turn when it completes. After a crash, you keep the history up to the last completed turn and can resume. To continue the interrupted run itself, from its last model or tool call, add a durable runtime (crash recovery).
Create and load a session#
import { createAgent, createSession } from 'zidane'
import { basic } from 'zidane/presets'
import { anthropic } from 'zidane/providers'
import { createSqliteStore } from 'zidane/session/sqlite'
const provider = anthropic()
const store = createSqliteStore({ path: './sessions.db' })
const session = await createSession({ store })
const agent = createAgent({ ...basic, provider, session })
await agent.run({ prompt: 'hello' })
await session.save()Restore with loadSession(store, id). On a session with turns, agent.run() without a prompt resumes it, for hosts that save the user message before the run starts.
session:start and session:end (with status and turnRange) fire at run boundaries (session hooks). The apps keep every session in one SQLite file, sessions.db (where), and add forks, titles and spaces (app sessions).
Storage backends#
| Store | Import | Durability | Concurrency |
|---|---|---|---|
createMemoryStore() | zidane | Lost on exit | Single process |
createSqliteStore({ path }) | zidane/session/sqlite (Bun only) | Per-turn flush, WAL | Safe in one process. Never attach two Agents to one session |
createRemoteStore({ url }) | zidane | Server-side (HTTP) | Delegated |
createFileMapStore(adapter) | zidane | turns.jsonl + meta.json | Wraps any { get, save, delete } backend |
createSqliteStore migrates v3 databases in place on first open (idempotent), after copying the database to <db>.zidane-pre-v4.bak. To roll back, stop every process using it and copy the .bak over it.
Compaction#
Compaction summarizes older turns when the conversation nears the model's context window, so a long session keeps going. A model writes the summary. zidane saves it as a compact-summary block in place of the older turns, adds back recently read files and active skills, and continues.
| Setting | Library (behavior.autoCompact) | Terminal and desktop apps |
|---|---|---|
| Default | Off | On (Auto-compact) |
| Fires at | 85% of the usable window (threshold: 0.85) | 80% (autoCompactThreshold: 0.6, 0.7, 0.8 or 0.9) |
| Summary model | The run's model, or autoCompact.provider and model | The session's model, or Compaction model (compactionModel) |
| By hand | agent.compact() | Session details: Compact now in the desktop app, k in the terminal app. It runs agent.compact() on the session's model. |
| Show the summary | Compaction summaries (off by default) |
createAgent({
provider,
behavior: { autoCompact: { threshold: 0.8, keepTurns: 4, model: 'claude-haiku-4-5' } },
})autoCompact option | Default | Does |
|---|---|---|
threshold | 0.85 | Fraction of the usable window (window minus the output reserve) that triggers it |
contextWindow | the provider's | The model's window in tokens |
keepTurns | compactKeepTurns, else 4 | Recent turns kept word for word |
restore | on | Add recently read files and active skills back after the summary |
minGrowthFraction | 0.1 | Share of the window new content must fill before another compaction |
maxFailures | 2 | Failures in a row before the run stops trying |
provider, model | the run's | Where the summary runs |
When the history still starts with the last request a run sent, the summary call resends that request with the summary instruction added as a system note, so the provider reads the conversation from its prompt cache. If that fails, or the reply has no summary, zidane sends a separate summary request.
Compact by hand#
const result = await agent.compact({ keepTurns: 4 })
if (result.compacted)
console.log(`${result.replacedCount} turns replaced`)| Option | Default | Does |
|---|---|---|
keepTurns | autoCompact.keepTurns, then compactKeepTurns, then 4 | Recent turns kept word for word |
model | The model the last run ended on | The model that writes the summary. Another model cannot reuse the cached request. |
signal | Cancels the compaction. agent.abort() cancels it too. |
When it compacts, the result has compacted: true, the summaryTurnId, replacedCount, droppedCount, restoredFiles, restoredSkills, the model, the usage of the summary call and effectiveTokens (the estimated context size on the next turn). Otherwise it has compacted: false and a reason: 'nothing-to-compact', 'no-session' or 'aborted'. The first two make no provider call.
agent.compact() needs an idle agent. It rejects while a run or another compaction is in progress, and run() rejects while it works. It also rejects when the summary call fails. It fires the same hooks as auto-compaction, with reason: 'manual'. The lower-level compactConversation() summarizes turns outside an agent.
Compaction fires compact:start, compact:progress and compact:end. A context overflow mid-run stubs older tool output and media instead (errors). compactConversation() throws CompactInvalidInputError when there is nothing to summarize (no turns, or keepTurns covers them all), and CompactPromptTooLongError when the summary request is still too long after its retries.
In the apps, the experimental setting Compact before the cache expires (compactBeforeCacheExpiry, off) compacts an idle long session about a minute before its prompt cache is estimated to expire. A session counts as long when its last request had at least 100k tokens, or half the token count at which auto-compaction fires, if that is more. Any activity in the session cancels it.
behavior.compactStrategy: 'tail' costs less. It hides older tool results on the wire and writes no summary (output budgets).
Message format#
Providers convert to and from SessionContentBlock:
Block type | Fields |
|---|---|
text | text, optional origin |
image, audio, video, document | mediaType, data (base64) or ref, optional name. Documents add encoding |
tool_call | id, name, input |
tool_result | callId, output: string | ToolResultContent[], isError?, metadata? |
thinking, redacted_thinking | text and signature?, or opaque data |
provider_reasoning | Opaque OpenRouter or vLLM reasoning, replayed to the same provider |
server_tool_use, server_tool_result | Anthropic server tools such as web_search |
compact-summary | Summary that replaces compacted turns |
A text block's origin (TextOrigin) says who sent it when that is not the session's own user: kind ('user', 'signal' or 'event'), source (such as 'slack'), and optional sender and messageId.
An assistant turn with aborted: true holds model output that a process death cut off, rebuilt when its durable run resumed. The transcript shows it, the model context leaves it out, and the request is sent again.
ToolResultContent is a text, image, audio, video or document part. Providers with imageInToolResult: true get image results natively. Other providers get them in a companion user message. toolResultToText(output) flattens any output to text.
Build provider requests yourself#
Use session.toMessages({ provider }), not session.turns. Raw turns can hold orphaned tool_use / tool_result blocks (from an interrupted run, a partial write or a compaction boundary), which providers reject with a 400. toMessages() applies the loop's pairing repair and, given a provider, the user-tail rule some models need. It never changes session.turns. Do not write its result back with setTurns or appendTurns.
| Helper | Input |
|---|---|
toWireMessages(turns, opts?) | Raw SessionTurn[] from store.getTurns() |
ensureToolResultPairing(messages, opts?) | Messages |
ensureEndsWithUserMessage(messages, provider) | Messages |
Converters for other frameworks, from zidane: fromAnthropic, toAnthropic, fromOpenAI, toOpenAI, autoDetectAndConvert.