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, andtoolsbelong to theAgentinstance. 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 }):
- Load History: The agent runs
await session.getMessages(). If no session is attached, it defaults to[]. - Build Message Queue: The agent compiles
[systemMessage, ...history, newUserMessage]. - 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.
- Append & Save: On a successful final response, the agent appends these new turns to the session store in one call:
await session.appendMessages(newMessages). - 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."