Skip to content

Creating persistent chat sessions

Many of your users will have interacted with large language models for the first time through chatbots. Although LLMs are capable of much more than simulating conversations, it remains a familiar and useful style of interaction. Even when your users will not be interacting directly with the model in this way, the conversational style of prompting is a powerful way to influence the output generated by an AI model.

Genkit Go provides a sessions API that helps you manage stateful interactions with LLMs, persisting state across multiple requests.

Before reading this page, you should be familiar with the content covered on the Generating content with AI models page.

If you want to run the code examples on this page, first complete the steps in the Getting started guide. All of the examples assume that you have already installed Genkit as a dependency in your project.

The sessions API is in the experimental x package:

import "github.com/firebase/genkit/go/core/x/session"

A session encapsulates a stateful execution environment with strongly-typed state that can be persisted across requests. Sessions are useful for maintaining user preferences, conversation context, or any application state that needs to survive between interactions.

Here’s a simple example that creates a session and uses it within a flow:

// Define your session state type
type ChatState struct {
UserName string `json:"userName"`
History []string `json:"history"`
}
// Create a store to persist sessions across requests
store := session.NewInMemoryStore[ChatState]()
genkit.DefineFlow(g, "chat", func(ctx context.Context, input string) (string, error) {
sessionID := "user-123" // In practice, get this from the request
// Load existing session or create new one
sess, err := session.Load(ctx, store, sessionID)
if err != nil {
sess, err = session.New(ctx,
session.WithID[ChatState](sessionID),
session.WithStore(store),
session.WithInitialState(ChatState{UserName: "Guest"}),
)
if err != nil {
return "", err
}
}
// Attach session to context for use in tools and prompts
ctx = session.NewContext(ctx, sess)
// Generate with the session-aware context
return genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-2.5-flash"),
ai.WithPrompt(input),
)
})

Sessions become powerful when combined with tools that can read and modify session state. This allows the model to maintain context and update state based on the conversation.

Here’s a shopping cart example that demonstrates stateful sessions:

// CartState holds the shopping cart items
type CartState struct {
Items []string `json:"items"`
}
store := session.NewInMemoryStore[CartState]()
const sessionID = "shopping-session"
// Define a tool that adds items to the cart
addToCartTool := genkit.DefineTool(g, "addToCart",
"Adds items to the shopping cart",
func(ctx *ai.ToolContext, input struct{ Items []string }) ([]string, error) {
sess := session.FromContext[CartState](ctx.Context)
if sess == nil {
return nil, fmt.Errorf("no session in context")
}
state := sess.State()
state.Items = append(state.Items, input.Items...)
if err := sess.UpdateState(ctx.Context, state); err != nil {
return nil, err
}
return state.Items, nil
},
)
// Define a tool that retrieves cart contents
getCartTool := genkit.DefineTool(g, "getCart",
"Returns all items currently in the shopping cart",
func(ctx *ai.ToolContext, input struct{}) ([]string, error) {
sess := session.FromContext[CartState](ctx.Context)
if sess == nil {
return nil, fmt.Errorf("no session in context")
}
return sess.State().Items, nil
},
)
genkit.DefineFlow(g, "manageCart", func(ctx context.Context, input string) (string, error) {
sess, err := session.Load(ctx, store, sessionID)
if err != nil {
sess, err = session.New(ctx,
session.WithID[CartState](sessionID),
session.WithStore(store),
session.WithInitialState(CartState{Items: []string{}}),
)
if err != nil {
return "", err
}
}
ctx = session.NewContext(ctx, sess)
return genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-2.5-flash"),
ai.WithSystem("You are a helpful shopping assistant. Use the provided tools to manage the user's cart."),
ai.WithTools(addToCartTool, getCartTool),
ai.WithPrompt(input),
)
})

When a session is attached to the context, the session state is automatically available in dotprompt templates using the {{@state.fieldName}} syntax. This allows you to personalize prompts based on the current session state without explicitly passing the state to the prompt.

For example, if your session state has a userName field:

type UserState struct {
UserName string `json:"userName"`
Preferences []string `json:"preferences"`
}

You can reference these fields directly in your dotprompt templates:

---
model: googleai/gemini-2.5-flash
---
Hello {{@state.userName}}! Based on your preferences ({{@state.preferences}}),
here are some recommendations for you.
{{prompt}}

When the prompt is rendered, {{@state.userName}} will be replaced with the actual value from the session state. This works automatically when you attach a session to the context using session.NewContext().

By default, sessions created without a store exist only in memory for the current request. To persist sessions across requests (even within the same process), you must provide a store. For production deployments, you’ll want to persist sessions to a database or other durable storage.

To create a custom session store, implement the session.Store[S] interface:

type Store[S any] interface {
// Get retrieves session data by ID. Returns nil if not found.
Get(ctx context.Context, sessionID string) (*session.Data[S], error)
// Save persists session data, creating or updating as needed.
Save(ctx context.Context, sessionID string, data *session.Data[S]) error
}

Here’s an example JSON file-based store:

type JsonFileStore[S any] struct {
dir string
}
func NewJsonFileStore[S any](dir string) *JsonFileStore[S] {
return &JsonFileStore[S]{dir: dir}
}
func (s *JsonFileStore[S]) Get(ctx context.Context, sessionID string) (*session.Data[S], error) {
path := filepath.Join(s.dir, sessionID+".json")
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
var sessionData session.Data[S]
if err := json.Unmarshal(data, &sessionData); err != nil {
return nil, err
}
return &sessionData, nil
}
func (s *JsonFileStore[S]) Save(ctx context.Context, sessionID string, data *session.Data[S]) error {
path := filepath.Join(s.dir, sessionID+".json")
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
return os.WriteFile(path, jsonData, 0644)
}

For production deployments, you can use the Firebase Firestore session store provided in the Firebase plugin:

import firebasex "github.com/firebase/genkit/go/plugins/firebase/x"
// Create a Firestore-backed session store
store, err := firebasex.NewFirestoreSessionStore[CartState](ctx, g,
firebasex.WithCollection("sessions"),
firebasex.WithTTL(24*time.Hour), // Sessions expire after 24 hours
)
if err != nil {
log.Fatal(err)
}
// Use the store when creating sessions
sess, err := session.New(ctx,
session.WithStore(store),
session.WithInitialState(CartState{Items: []string{}}),
)

The Firestore store automatically handles:

  • Persisting sessions across server restarts
  • Session expiration via TTL
  • Concurrent access from multiple server instances
// Create an ephemeral session (not persisted, lives only for current request)
sess, err := session.New[MyState](ctx)
// Create with custom ID
sess, err := session.New(ctx, session.WithID[MyState]("my-session-id"))
// Create with initial state
sess, err := session.New(ctx, session.WithInitialState(MyState{Field: "value"}))
// Create with a store for persistence (required to load sessions later)
sess, err := session.New(ctx, session.WithStore(myStore))
// Combine options for a persistent session
sess, err := session.New(ctx,
session.WithID[MyState]("my-session-id"),
session.WithStore(myStore),
session.WithInitialState(MyState{Field: "value"}),
)
// Load an existing session from a store
sess, err := session.Load(ctx, store, sessionID)
if err != nil {
// Handle error (session not found or store error)
}
// Get the session ID
id := sess.ID()
// Get a copy of the current state
state := sess.State()
// Update the session state (persists to store)
state.Field = "new value"
err := sess.UpdateState(ctx, state)
// Attach session to context
ctx = session.NewContext(ctx, sess)
// Retrieve session from context (in tools or other functions)
sess := session.FromContext[MyState](ctx)
// Get state without knowing the type (for template rendering)
state := session.StateFromContext(ctx)
  • Learn about tool calling to add interactive capabilities to your sessions
  • Explore prompts to understand how to use session state in templates
  • See flows for structuring your session-based applications