Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Persistence Controls
Pass only the stores you use. Messages can live in your database while approvals live somewhere else.
Persistence has no feature flags. The stores that the backend provides decide what is persisted. Use composePersistence to mix backends per store, and give only the stores your app needs.
Swap one store#
composePersistence takes the base backend first and an overrides object second. This example starts from the in-memory backend and replaces the interrupts and runs stores:
import { composePersistence, memoryPersistence } from '@tanstack/ai-persistence'
// Your own implementations of the InterruptStore and RunStore contracts.
import { interruptStore, runStore } from './stores'
const persistence = composePersistence(memoryPersistence(), {
overrides: {
interrupts: interruptStore,
runs: runStore,
},
})Remove one store#
Set the store to false:
import { composePersistence, memoryPersistence } from '@tanstack/ai-persistence'
// Drop metadata. The resulting type has no `metadata` key.
const withoutMetadata = composePersistence(memoryPersistence(), {
overrides: { metadata: false },
})Each override is independent:
| Override value | Result |
|---|---|
| Key omitted | Keep the base store. |
| undefined | Keep the base store. |
| A store object | Replace that store only. |
| false | Remove that store. |
Unknown store names fail the type check. They also fail at runtime when the values come from untyped JavaScript.
To define a partial backend directly, without a base, use defineAIPersistence({ stores: { ... } }) and pass only the stores you have. The store reference has the store contracts.
What each store gives you#
| You want | Store |
|---|---|
| A server transcript that is the true copy | messages (required by withPersistence and reconstructChat) |
| Run status and usage | runs (required on ChatPersistence, and required when interrupts is set) |
| Durable approvals or human input | interrupts (needs runs) |
| App or integration checkpoints | metadata (always optional) |
withPersistence(persistence) looks at the stores that are present. A present store turns its feature on.
Valid store combinations#
- withPersistence needs messages.
- interrupts needs runs, because an interrupt record belongs to a run.
- withGenerationPersistence needs generationRuns.
- Portable sandbox snapshots need messages, artifacts, and blobs on the same persistence object. See Keep Files After Reload.
Advanced#
Named shapes#
Use these types for a persistence object. They are better than a sparse map:
| Type | Required stores | Use |
|---|---|---|
| ChatTranscriptStores / ChatTranscriptPersistence | messages (runs, interrupts, and metadata optional) | The minimum for withPersistence and reconstructChat |
| ChatPersistenceStores / ChatPersistence | messages, runs, interrupts, and metadata | Packaged backends (memoryPersistence, Drizzle, Prisma, D1) |
| ChatWithInterruptsStores / ChatWithInterruptsPersistence | messages, runs, and interrupts | Human-in-the-loop without metadata |
There is no public sparse AIPersistenceStores export. For a custom map, use a named shape or AIPersistence<{ messages: MessageStore, ... }>. defineAIPersistence and composePersistence still accept sparse maps by inference.
Entrypoint requirements#
| Entrypoint | Shape | Notes |
|---|---|---|
| withPersistence | At least ChatTranscriptStores | interrupts needs runs |
| reconstructChat | ChatTranscriptStores | runs and interrupts add data to the response when present |
| Packaged *Persistence() | ChatPersistence | messages and runs, plus interrupts and metadata |
| defineAIPersistence / composePersistence | Sparse, by inference | Use a named shape for the result |
Locks#
Locks coordinate work across server instances, like a distributed mutex. They live in @tanstack/ai/locks, and you add them as their own middleware with withLocks, next to withPersistence. Full guide: Locks.
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
import { withPersistence, memoryPersistence } from '@tanstack/ai-persistence'
const middleware = [
withPersistence(memoryPersistence()),
withLocks(new InMemoryLockStore()), // For many instances, use a distributed LockStore.
]