Storage
Durable thread state provider.
Storage is required for production.
import { createProductionThreadStore } from "./thread-storage";
export const storage = createProductionThreadStore({
url: process.env.DATABASE_URL!,
});Use it with createAgentRuntime():
export const runtime = createAgentRuntime({
agent,
storage,
scopeKey: (identity) => identity.workspaceId,
});What Storage Owns
Storage persists:
- thread records, status,
ThreadVersion, and metadata; - discriminated run/admin leases on thread records;
- run lifecycle records for accepted work;
- transcript messages;
- queued items;
- tool message parts on the open tail assistant message;
- immutable incarnation ids and monotonic per-incarnation revisions.
Scope Constraint
Production storage must load and mutate threads by scope plus thread id.
readThread({ scopeKey, threadId });Never rely on globally loading by plain threadId.
See Persisted Data Model for the logical record shapes and storage invariants expected from ThreadStore.
Runtime Facades
Application and lifecycle code should not receive ThreadStore directly. Raw storage is a provider-level API: it can read and mutate any scoped record the provider exposes, so it is too broad for ordinary agent behavior.
The runtime exposes narrower server-side facades instead:
- lifecycle hooks receive a current-thread
threadfacade; - backend tools may receive scoped runtime helpers;
runtime.admin()exposes trusted server/admin operations;- browser clients receive the smaller remote
ThreadAPI.
The facade still writes through the storage provider, but it preserves runtime invariants:
- current thread scope;
- active run ownership or explicit runtime lock;
- complete-version compare-and-swap updates;
- realtime/watch invalidation;
- message visibility metadata;
- provider portability.
For example, context compaction should use the lifecycle utility, which keeps the original transcript and message visibility authoritative:
const compaction = createContextCompaction({
threshold: { maxEstimatedTokens: 24_000 },
retain: { maxMessages: 8, maxEstimatedTokens: 12_000 },
summarize: async ({ messages }) => summarize(messages),
});Explicit product-driven visibility changes may use the scoped lifecycle or trusted runtime facade. Lifecycle code should not call raw storage directly.