feat(ai): add persistent conversation store backends - #1540
Conversation
There was a problem hiding this comment.
Pull request overview
Adds pluggable, persistent AI conversation storage shared across agents, tools, sessions, and server components.
Changes:
- Introduces memory and GORM store backends with migrations and limits.
- Injects one shared store throughout AI runtime components.
- Adds configuration schemas and comprehensive backend tests.
Reviewed changes
Copilot reviewed 39 out of 40 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
ai/store/types.go |
Defines persisted session and turn types. |
ai/store/store.go |
Defines conversation store interfaces. |
ai/store/errors.go |
Adds store sentinel errors. |
ai/store/memory/store.go |
Implements in-memory storage. |
ai/store/gorm/store.go |
Implements relational storage. |
ai/store/gorm/models.go |
Defines GORM database models. |
ai/store/gorm/dialector.go |
Opens supported databases. |
ai/store/gorm/codec.go |
Serializes Genkit messages. |
ai/store/gorm/codec_test.go |
Tests message serialization. |
ai/store/gorm/store_test.go |
Tests persistence and transactions. |
ai/store/test/contract.go |
Defines shared backend contract tests. |
ai/store/test/memory_store_test.go |
Tests memory-store behavior. |
ai/component/memory/config.go |
Adds backend/database configuration. |
ai/component/memory/factory.go |
Applies configuration defaults. |
ai/component/memory/component.go |
Owns and initializes the shared store. |
ai/component/memory/config_test.go |
Tests configuration and lifecycle. |
ai/component/agent/agent.go |
Adds request context to agent interactions. |
ai/component/agent/react/react.go |
Persists interaction history and turns. |
ai/component/agent/react/steps.go |
Reads and writes through MessageStore. |
ai/component/agent/react/orchestrator.go |
Carries persistence context. |
ai/component/agent/react/component.go |
Injects the shared store. |
ai/component/agent/react/component_store_test.go |
Verifies store injection. |
ai/component/agent/react/persistence_context_test.go |
Tests detached persistence contexts. |
ai/component/agent/react/page_context_test.go |
Updates page-context tests. |
ai/component/agent/react/step_test.go |
Updates step tests for stores. |
ai/component/tools/engine/tools.go |
Injects MessageStore into tools. |
ai/component/tools/engine/memory_tools.go |
Reads persisted conversation history. |
ai/component/tools/test/engine_tools_test.go |
Tests shared-store tool access. |
ai/component/server/component.go |
Injects stores and manages router cleanup. |
ai/component/server/engine/router.go |
Constructs store-backed session routing. |
ai/component/server/engine/handlers.go |
Uses persistent session operations. |
ai/component/server/engine/handlers_test.go |
Updates handler tests. |
ai/component/server/engine/session/session.go |
Replaces session maps with SessionStore. |
ai/component/server/engine/session/session_test.go |
Tests shared session storage. |
ai/component/server/engine/sse/sse_test.go |
Adds SSE behavior tests. |
ai/config/test/loader_test.go |
Tests GORM configuration loading. |
ai/schema/json/memory.schema.json |
Defines backend configuration schema. |
ai/schema/json/REQUIRED_FIELDS.md |
Documents new configuration fields. |
ai/go.mod |
Adds GORM database dependencies. |
ai/go.sum |
Records dependency checksums. |
Suppressed comments (2)
ai/component/server/engine/handlers.go:212
SessionStore.Getcan now return arbitrary GORM/database failures, but this maps every error to 404 and labels it "Session not found". Return 404 only forErrSessionNotFound/ErrSessionExpired; operational failures should produce a sanitized 5xx response.
sessionObj, err := h.sessionMgr.GetSession(c.Request.Context(), sessionID)
if err != nil {
c.JSON(http.StatusNotFound, NewErrorResponse("Session not found: "+err.Error()))
return
ai/component/server/engine/handlers.go:245
- A failed database transaction is also mapped to 404 here, even when the session exists. Check specifically for
ErrSessionNotFound; return a sanitized 5xx response for other persistence errors so clients do not treat an outage as a missing resource.
if err := h.sessionMgr.DeleteSession(c.Request.Context(), sessionID); err != nil {
c.JSON(http.StatusNotFound, NewErrorResponse("Session not found: "+err.Error()))
return
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // context, so there is no need to wrap the input in a | ||
| // JSON envelope the model would otherwise have to read through. | ||
| history.AddHistory(sessionID, ai.NewUserMessage(ai.NewTextPart(input.Content))) | ||
| turnID, err := ra.messageStore.BeginTurn(parent, sessionID) |
There was a problem hiding this comment.
This creates the persisted Turn before the interaction loop starts. Any later cancellation path must either finalize or explicitly abort this Turn; otherwise it remains active.
| if err != nil { | ||
| return nil, nil, fmt.Errorf("failed to begin turn: %w", err) | ||
| } | ||
| if err := ra.messageStore.AddHistoryToTurn(parent, sessionID, turnID, ai.NewUserMessage(ai.NewTextPart(input.Content))); err != nil { |
There was a problem hiding this comment.
The user message is persisted immediately after BeginTurn. Therefore, a client disconnect after this point leaves durable conversation state even if the remaining interaction is canceled.
| } | ||
| defer s.cancelPersistence() | ||
|
|
||
| if err := runLoop(ctx, s, ra.maxIterations, ra.buildSteps(chans)...); err != nil { |
There was a problem hiding this comment.
The context passed to runLoop is derived from the HTTP request context in newInteraction, so runLoop remains cancellation-sensitive even though persistCtx is detached.
| defer s.cancelPersistence() | ||
|
|
||
| if err := runLoop(ctx, s, ra.maxIterations, ra.buildSteps(chans)...); err != nil { | ||
| chans.ErrorChan <- err |
There was a problem hiding this comment.
When a stage returns context.Canceled after a client disconnect, this immediate return has no cleanup or abort operation for the Turn created above.
| return | ||
| } | ||
|
|
||
| if err := ra.messageStore.NextTurnForTurn(s.persistenceContext(ctx), sessionID, s.TurnID); err != nil { |
There was a problem hiding this comment.
NextTurnForTurn is only reached after runLoop succeeds. It is skipped by the cancellation or error return above, leaving the persisted Turn active.
|
|
LGTM |



Please provide a description of this PR:
This PR adds pluggable persistence for AI conversation data.
Related to #1502.
Changes
SessionStore,MessageStore, and the combinedStoreinterfaces.ai.Messagehandling toMemoryStore.GormStorefor persistent conversation history.max_turnsas the single Turn-limit configuration for both backends.ErrTurnLimitReachedwhen a Session attempts to create a Turn beyond the configured limit.(turn_id, sequence)constraint.HistoryMemoryonly as a lazily initialized compatibility layer; production Agent, Memory Tool, and Session Manager paths use Store interfaces exclusively.Testing
The following checks pass in the
ai/Go module:go test ./...go vet ./...go test -race ./store/... ./component/agent/react ./component/tools/test ./component/server/engine ./component/server/engine/sseSQLite is used for local persistence and behavioral tests. MySQL and PostgreSQL drivers are included and compile successfully. Service-backed MySQL/PostgreSQL integration tests are outside the current PR because CI database services are not configured.
To help us figure out who should review this PR, please put an X in all the areas that this PR affects.
Please check any characteristics that apply to this pull request.