Persistence
By default everything is kept in memory and lost on restart. In production, pass your own storage to createAgent. All three options are independent and optional.
| Option | Holds | Default |
|---|---|---|
checkpointer | Chat messages | In-memory |
threadStore | History list per user (id, title, updatedAt) | In-memory |
vectorStore | Knowledge embeddings | In-memory |
createAgent({ …, checkpointer, threadStore, vectorStore });
Startup logs a warning when checkpointer is missing, or when it is set without threadStore.
Messages: checkpointer
Any LangGraph BaseCheckpointSaver. Postgres example:
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
await checkpointer.setup(); // creates the tables; run once at startup
When a user deletes a thread, sideagent calls checkpointer.deleteThread(threadId) if the saver has it.
History list: threadStore
Implement ThreadStore. Key every row by (userId, threadId).
import type { ThreadStore } from "sideagent/server";
const threadStore: ThreadStore = {
// newest first
list: async (userId) => /* SELECT id, title, updated_at AS "updatedAt" … ORDER BY updated_at DESC */,
has: async (userId, threadId) => /* SELECT 1 … WHERE user_id = $1 AND id = $2 */,
// create with `title` if new; otherwise only bump updatedAt. Never overwrite the title.
touch: async (userId, threadId, title) => /* INSERT … ON CONFLICT (user_id, id) DO UPDATE SET updated_at = now() */,
delete: async (userId, threadId) => /* DELETE … WHERE user_id = $1 AND id = $2 */,
};
- On a LangGraph
BaseStore(Postgres, Redis, …) you can skip this and usenew LangGraphThreadStore(store)fromsideagent/server. It keeps the same 200-thread cap asMemoryThreadStore. hasis the ownership check. A thread that is not in the user's own index returns 404, even if the id is guessed.updatedAtis a number of milliseconds since epoch.- The client shows at most 50 threads.
MemoryThreadStorekeeps at most 200 per user; your store has no such cap unless you add one.
Knowledge: vectorStore
Any LangChain vector store: VectorStoreInterface from @langchain/core/vectorstores (pgvector, Vectorize, Qdrant, …). Sideagent embeds the chunks itself, so the store's own embeddings are not used. It calls:
addVectors(vectors, documents, { ids })with stable ids<sourceId>#<n>. A refresh must overwrite rows with the same id (upsert).delete({ ids })for chunks a refreshed source no longer has.similaritySearchVectorWithScore(vector, k). Each document carriesmetadata: { sourceId, title, url? }.
import { PGVectorStore } from "@langchain/community/vectorstores/pgvector";
const vectorStore = await PGVectorStore.initialize(embeddings, { postgresConnectionOptions, tableName: "sideagent_kb" });
createAgent({ …, vectorStore });
vectorsize is set by your embedding model. The column dimension must match it. Changingembeddings.modelneeds re-embedding, so clear the table first.- With a persistent store, the agent still re-embeds sources on every start. The store only avoids keeping them in process memory.