Skip to main content
Memory lets an agent remember previous turns within a session. Without memory, each Run call starts with a clean context.

Built-in stores

StorePackageInfrastructureBest for
In-memorypkg/memory/inmemoryNoneTesting, single-process
SQLitepkg/memory/sqliteNone (pure Go)Dev, embedded, zero-dep
PostgreSQLpkg/memory/postgresPostgres serverProduction SQL
Redispkg/memory/redisRedis serverFast sessions, TTL
All stores implement core.MemoryStore and plug in identically via chainforge.WithMemory(store).

In-memory store

import "github.com/lioarce01/chainforge/pkg/memory/inmemory"

agent, err := chainforge.NewAgent(
    // ...
    chainforge.WithMemory(inmemory.New()),
)

// First call
agent.Run(ctx, "session-1", "My name is Alice.")

// Second call — agent remembers "Alice"
agent.Run(ctx, "session-1", "What is my name?")
Sessions are namespaced by sessionID. Different IDs are fully isolated.

Options

The in-memory store accepts functional options to control session lifecycle:
import (
    "time"
    "github.com/lioarce01/chainforge/pkg/memory/inmemory"
)

store := inmemory.New(
    inmemory.WithTTL(30 * time.Minute),   // expire idle sessions after 30 min
    inmemory.WithMaxMessages(100),         // keep only the last 100 messages per session
)
OptionDescriptionDefault
WithTTL(d)Sessions that have not received a new message within d are cleared on the next Get. TTL resets on every Append (sliding window).No expiry
WithMaxMessages(n)After each Append, if the session exceeds n messages the oldest are dropped.Unlimited
Both options can be combined. Use WithTTL to prevent unbounded memory growth in long-running processes, and WithMaxMessages to cap per-session context size.

SQLite store

Zero infrastructure required — pure Go, no cgo.
import "github.com/lioarce01/chainforge/pkg/memory/sqlite"

// In-process database (great for tests / dev)
store, err := sqlite.NewInMemory()

// File-based (persists across restarts)
store, err := sqlite.New("./data/chat.db")
store, err := sqlite.New("./data/chat.db",
    sqlite.WithTableName("my_messages"),
    sqlite.WithBusyTimeout(10*time.Second),
)
defer store.Close()

agent, err := chainforge.NewAgent(
    chainforge.WithMemory(store),
    // ...
)

PostgreSQL store

import "github.com/lioarce01/chainforge/pkg/memory/postgres"

store, err := postgres.New(os.Getenv("DATABASE_URL"))
store, err := postgres.New(os.Getenv("DATABASE_URL"),
    postgres.WithSchemaName("myapp"),
    postgres.WithTableName("chat_history"),
    postgres.WithMaxConns(20),
)
defer store.Close()

agent, err := chainforge.NewAgent(
    chainforge.WithMemory(store),
    // ...
)
The schema is auto-migrated on first use. Identifiers are safely quoted via pgx.Identifier.

Redis store

import "github.com/lioarce01/chainforge/pkg/memory/redis"

// By address
store, err := redis.New("localhost:6379")

// By URL (supports redis:// and rediss://)
store, err := redis.NewFromURL(os.Getenv("REDIS_URL"),
    redis.WithTTL(24*time.Hour), // sliding-window expiry
    redis.WithKeyPrefix("myapp"),
)
defer store.Close()

agent, err := chainforge.NewAgent(
    chainforge.WithMemory(store),
    // ...
)
Redis stores messages in a List per session ({prefix}:{sessionID}). TTL is reset on every Append (sliding window).

Custom store

Implement core.MemoryStore to use any other backend:
type MemoryStore interface {
    Get(ctx context.Context, sessionID string) ([]Message, error)
    Append(ctx context.Context, sessionID string, msgs ...Message) error
    Clear(ctx context.Context, sessionID string) error
}

History summarization

When WithMaxHistory is set, old messages are normally dropped. Use WithHistorySummarizer to compress them into a single compact message instead:
// A cheap, fast model works well as a summarizer.
summarizer, _ := chainforge.NewAgent(
    chainforge.WithAnthropic(apiKey, "claude-haiku-4-5-20251001"),
    chainforge.WithSystemPrompt("Summarize the conversation concisely, preserving key facts."),
)

agent, _ := chainforge.NewAgent(
    chainforge.WithAnthropic(apiKey, "claude-sonnet-4-6"),
    chainforge.WithMemory(inmemory.New()),
    chainforge.WithMaxHistory(10),          // allow 10 messages per run
    chainforge.WithHistorySummarizer(summarizer), // compress instead of drop
)
How it works: When history exceeds maxHistory:
  1. The oldest len(history) - (maxHistory - 1) messages are sent to the summarizer agent.
  2. The summary is stored as a single [Summary: ...] message.
  3. The maxHistory - 1 most recent messages are kept verbatim.
  4. The compressed history (summary + recent) replaces the full history in the memory store.
The result is always maxHistory messages: one summary + maxHistory-1 recent. On subsequent runs the same compression can happen again, layering summaries. Session isolation: the summarizer runs under "<sessionID>:summarizer" so its internal conversation does not interfere with the parent session. Error handling: if the summarizer fails, Run returns the error immediately. There is no silent fallback to dropping messages.

Multi-agent memory isolation

The orchestrator automatically namespaces session IDs per step ("sessionID:step-N"), so each agent in a sequential pipeline has isolated history.

Semantic retrieval (RAG)

For semantic search over a knowledge base — not just conversation history — see the RAG guide. The RAG layer uses Qdrant as a vector store and exposes rag.NewQdrantRetriever and rag.NewQdrantIngestor built on the same qdrant.Store used for memory.