Guides

Sessions & Memory

Persisting conversation history and managing multi-turn memory.

In a conversational agent, separating static configuration from dynamic runtime state is crucial. Chai AI SDK implements a clean separation between these layers:

  • Agent Configuration: name, instructions, model, and tools belong to the Agent instance. They do not change across turns.
  • Current Run State: The local messages list ([system, ...history, user, ...tool_calls]) built during a single execution.
  • Persistent Session State: Owned by Session / SessionStore, consisting of the turns that need to be remembered across calls (excluding the system instructions).

Defining a Session

To remember prior conversation turns, you pass a session object to .generate() or .stream().

By default, creating a session without arguments uses an in-memory store, which resets when the process restarts:

import { createSession } from "chai-ai";

// In-Memory store (default)
const session = createSession({ id: "user-123" });

Storage Abstraction: SessionStore

To persist sessions across process restarts, you can use any database, Cache/KV store, or file system by implementing the SessionStore interface:

export interface SessionStore {
  load(sessionId: string): Promise<Message[]>;
  save(sessionId: string, messages: Message[]): Promise<void>;
}

The File System Adapter: FileSessionStore

Chai AI SDK includes a built-in FileSessionStore for easy file-based persistence:

import { Agent, createSession, FileSessionStore } from "chai-ai";
import { google } from "@chai-ai/google";

// Save sessions inside a directory named './sessions'
const fileStore = new FileSessionStore("./sessions");

const session = createSession({
  id: "user-123",
  store: fileStore,
});

const agent = new Agent({
  name: "support-agent",
  instructions: "You are a helpful assistant.",
  model: google("gemini-3.1-pro-preview"),
});

// The agent will load prior files if they exist and save new turns on completion
const result = await agent.generate({
  prompt: "Hello, my name is Alice.",
  session,
});

How It Works Internally

When you pass a session to agent.generate({ prompt, session }):

  1. Load History: The agent runs await session.getMessages(). If no session is attached, it defaults to [].
  2. Build Message Queue: The agent compiles [systemMessage, ...history, newUserMessage].
  3. Track Run Additions: The agent collects only the new turns created in this run (the user's new message, model responses, and tool executions) in a temporary array.
  4. Append & Save: On a successful final response, the agent appends these new turns to the session store in one call: await session.appendMessages(newMessages).
  5. Fail-Safe Discard: If the run fails (e.g. timeout, max steps exceeded, guardrail blocks), nothing is written to the store, preventing session corruption with half-finished thoughts.

Sequence Trace

Here is how two turns unfold using a single session:

// Turn 1
agent.generate({ prompt: "My name is Alice.", session })
  → session.getMessages() → []
  → Call Model with: [System, User("My name is Alice.")]
  ← Model responds: "Hi Alice!"
  → session.appendMessages([User("My name is Alice."), Assistant("Hi Alice!")])
  ← returns "Hi Alice!"

// Turn 2
agent.generate({ prompt: "What's my name?", session })
  → session.getMessages() → [User("My name is Alice."), Assistant("Hi Alice!")]
  → Call Model with: [System, User("My name is Alice."), Assistant("Hi Alice!"), User("What's my name?")]
  ← Model responds: "Your name is Alice."
  → session.appendMessages([User("What's my name?"), Assistant("Your name is Alice.")])
  ← returns "Your name is Alice."

On this page