# Agent Sessions & Persistence (Experimental) > **Experimental / preview API.** Session stores come from > `ai/exp/localstore`. Read [agents.md](agents.md) for the basics first. When an agent has a `SessionStore` (via `aix.WithSessionStore`), the **server** owns the session history. Each turn produces an immutable **snapshot**; the snapshot chain carries conversation state forward. A store also enables [branching](agents-branching.md) and [background execution](agents-background.md). (Interrupts work with or without a store — see [human-in-the-loop](agents-human-in-the-loop.md).) ## Pick a store `ai/exp/localstore` ships two single-process stores. Both are generic over the custom-state type `State` (use `any` when you have no typed custom state). ```go import "github.com/firebase/genkit/go/ai/exp/localstore" // In-memory: great for tests/dev; lost on restart. memStore := localstore.NewInMemorySessionStore[any]() // File-backed: snapshots persisted as //.json // (the prefix defaults to "global"). Returns an error if dir can't be created. fileStore, err := localstore.NewFileSessionStore[any]("./.snapshots") if err != nil { log.Fatal(err) } ``` `localstore` is for local development, tests, and single-instance apps. For multi-instance production, use the Firestore or PostgreSQL store, or implement `aix.SessionStore` against your own database (see below). ### PostgreSQL store (experimental) > Requires Genkit Go v1.14.0+. `plugins/postgresql/exp` adds a session store with cross-process abort of detached runs (`LISTEN`/`NOTIFY`), metadata-only reads, and per-tenant prefixes. It shares the pool of the registered PostgreSQL plugin: ```go import ( "github.com/firebase/genkit/go/plugins/postgresql" pgexp "github.com/firebase/genkit/go/plugins/postgresql/exp" ) engine, err := postgresql.NewPostgresEngine(ctx, postgresql.WithCloudSQLInstance("my-project", "us-central1", "my-instance"), postgresql.WithDatabase("my-database")) if err != nil { log.Fatal(err) } g := genkit.Init(ctx, genkit.WithExperimental(), genkit.WithPlugins(&postgresql.Postgres{Engine: engine})) store, err := pgexp.NewPostgresSessionStore[ChatState](ctx, g, pgexp.WithSnapshotPathPrefix(tenantFromContext)) ``` Note: since v1.14.0, `PostgresEngine.Close` no longer closes a pool passed with `postgresql.WithPool`; close it yourself. `NewFileSessionStore` options: - `localstore.WithMaxPersistedChainLength(n)`: keep only the newest `n` snapshots in a chain (each save prunes older ancestors). `n >= 1`. - `localstore.WithSnapshotPathPrefix(fn)`: derive a per-call subdirectory from context to isolate snapshots per tenant (e.g. `"org-42/user-7"`). - `localstore.WithPollInterval(d)`: how often the store re-reads subscribed snapshot files to observe cross-process status changes (default 2s). This is what lets one process abort a detached turn another process is running. ```go pruning, err := localstore.NewFileSessionStore[any]("./.snapshots", localstore.WithMaxPersistedChainLength(3), ) ``` Attach a store to the agent with `aix.WithSessionStore`: ```go logbookAgent := genkitx.DefineAgent(g, "logbookAgent", aix.InlinePrompt{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("You are a personal logbook assistant."), }, aix.WithSessionStore(fileStore), ) ``` ## Turns persist automatically Each turn writes a snapshot chained off the previous one. The turn's snapshot ID is on `out.SnapshotID`; the conversation's stable ID is on `out.SessionID`. ```go out1, _ := logbookAgent.RunText(ctx, "Log this: I started studying Genkit today.") out2, _ := logbookAgent.RunText(ctx, "What did I study today?", aix.WithSessionID(out1.SessionID)) // remembers turn 1 fmt.Println(out1.SnapshotID, out2.SnapshotID) ``` ## Resume a conversation Two ways to resume a server-managed conversation: - `aix.WithSessionID(id)` — resume the session's **latest** snapshot. Use it when you track the conversation, not individual snapshots. - `aix.WithSnapshotID(id)` — resume a **specific** snapshot (see [branching](agents-branching.md)). ```go // Continue from the session's latest snapshot: out, _ := logbookAgent.RunText(ctx, "Add another note.", aix.WithSessionID(sessionID)) // Or continue from an exact checkpoint: out, _ = logbookAgent.RunText(ctx, "Add another note.", aix.WithSnapshotID(snapshotID)) ``` Resume is rejected if the latest snapshot is a failed, aborted, or pending dead end (a pending tip means a detached invocation is still running — wait for it or abort it). `aix.WithState` is for client-managed agents only and is mutually exclusive with `WithSessionID` / `WithSnapshotID`. ## Read a snapshot without running a turn `Agent.GetSnapshot` / `Agent.GetLatestSnapshot` fetch a stored snapshot (applying any configured `aix.WithStateTransform`). Handy for restoring a UI after a reload. They return `FAILED_PRECONDITION` on a client-managed agent (no store). ```go snap, err := logbookAgent.GetSnapshot(ctx, snapshotID) if err != nil { log.Fatal(err) } fmt.Println(snap.Status) // aix.SnapshotStatusCompleted, ... for _, m := range snap.State.Messages { fmt.Println(m.Role, m.Text()) } latest, _ := logbookAgent.GetLatestSnapshot(ctx, sessionID) ``` ## Typed session state Parameterize the store (and agent) with a struct to attach typed **custom state**. State is validated (via JSON) when a snapshot is loaded. See [working with state](agents-state.md) for reading and mutating it inside tools. ```go type Profile struct { Name string `json:"name"` Tier string `json:"tier"` // "free" | "pro" } profileAgent := genkitx.DefineAgent(g, "profileAgent", aix.InlinePrompt{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("Greet the user by name and tailor answers to their tier."), }, aix.WithSessionStore(localstore.NewInMemorySessionStore[Profile]()), ) // Seed custom state on the first turn (client-managed here for illustration; // with a store you would seed via WithState on a fresh session). out, _ := profileAgent.Run(ctx, &aix.AgentInput{Message: ai.NewUserTextMessage("Hello")}, aix.WithState(&aix.SessionState[Profile]{ Custom: Profile{Name: "Ada", Tier: "pro"}, }), ) ``` ## Interrupts (human-in-the-loop) Interrupts pause a turn so a human can approve or provide input, then resume from the exact pause point. They work with a store or with client-managed state (persistence is orthogonal). See [Human-in-the-loop / interrupts](agents-human-in-the-loop.md). ## Implementing a custom `SessionStore` For production, implement `aix.SessionStore` (which is `aix.SnapshotReader` + `aix.SnapshotWriter`) against your database. Add `aix.SnapshotSubscriber` to support [background/detach](agents-background.md) aborts. ```go type SnapshotReader[State any] interface { GetSnapshot(ctx context.Context, snapshotID string) (*SessionSnapshot[State], error) GetLatestSnapshot(ctx context.Context, sessionID string) (*SessionSnapshot[State], error) } type SnapshotWriter[State any] interface { // Atomically read the row at id (nil if absent), apply fn, and persist the // result. fn must be pure (it may be retried). Returning (nil, nil) skips // the write. The store owns identity (SnapshotID); the caller owns // timestamps and Status. An empty id means "mint a fresh one". SaveSnapshot(ctx context.Context, snapshotID string, fn func(existing *SessionSnapshot[State]) (*SessionSnapshot[State], error), ) (*SessionSnapshot[State], error) } // Optional: enables detach/abort by observing status changes without polling. type SnapshotSubscriber interface { OnSnapshotStatusChange(ctx context.Context, snapshotID string) <-chan SnapshotStatus } ``` Contract notes: preserve `SessionID` across rewrites (a row's session never changes); default an empty `Status` to `SnapshotStatusCompleted`; keep `CreatedAt`/`UpdatedAt` caller-managed (persist them verbatim, never stamp them); `GetLatestSnapshot` returns the greatest-`CreatedAt` row, ties broken by `SnapshotID`. See the `localstore` implementations for a reference. **Prove it with the conformance suite** (v1.14.0+). `ai/exp/sessionstoretest` checks a store against the contract the agent runtime relies on; the bundled stores all run it: ```go import "github.com/firebase/genkit/go/ai/exp/sessionstoretest" func TestMyStore(t *testing.T) { sessionstoretest.Run(t, func(t *testing.T) aix.SessionStore[MyState] { return newEmptyStore(t) }, nil) // or &sessionstoretest.Options[MyState]{Reopen: ..., Scoped: ...} } ```