Memory & sessions
How an agent remembers: a Session carries the conversation across run() calls, a store makes
it durable across processes, summarization keeps it bounded, and context_budget fits it to the
model’s window. All local-first — files and SQLite, no server.
Quickstart
from cendor.sdk import Agent, run, Session
agent = Agent(name="assistant", model="gpt-4o", instructions="Be helpful.")
session = Session()
run(agent, "My name is Alice.", session=session)
result = run(agent, "What's my name?", session=session) # -> knows "Alice"
import { Agent, run, Session } from '@cendor/sdk';
const agent = new Agent({ name: 'assistant', model: 'gpt-4o', instructions: 'Be helpful.' });
const session = new Session();
await run(agent, 'My name is Alice.', { session });
const result = await run(agent, "What's my name?", { session }); // -> knows "Alice"
Core concepts
Conversation memory — Session
Pass the same Session to successive run() calls: the canonical conversation is carried in,
and the run writes it back. Persistence is one call away:
session.save("chat.json")
session = Session.load("chat.json") # resume later, same process or not
session.save('chat.json');
const resumed = Session.load('chat.json'); // resume later, same process or not
Durable, multi-conversation memory — SQLiteSessionStore
Many named conversations in one local SQLite file:
from cendor.sdk import SQLiteSessionStore
store = SQLiteSessionStore("sessions.db")
session = store.load("user-42") # empty Session if unknown
run(agent, "hi, I'm Alice", session=session)
store.save("user-42", session) # survives restarts
# next process:
session = store.load("user-42") # remembers Alice
import { SqliteSessionStore } from '@cendor/sdk'; // note the casing: Sqlite, not SQLite
const store = new SqliteSessionStore('sessions.db');
const session = store.load('user-42'); // empty Session if unknown
await run(agent, "hi, I'm Alice", { session });
store.save('user-42', session); // survives restarts
// next process:
const resumed = store.load('user-42'); // remembers Alice
Rolling summarization — SummarizingSession
A long conversation eventually outgrows any window. SummarizingSession folds old turns into a
durable summary note (keeping recent turns verbatim), so memory stays bounded without losing the
gist:
from cendor.sdk import SummarizingSession, run
mem = SummarizingSession(model="gpt-4o-mini", max_messages=20, keep_recent=8)
for msg in conversation:
run(agent, msg, session=mem) # old turns fold into a memory note; recent stay verbatim
import { SummarizingSession, run } from '@cendor/sdk';
const mem = new SummarizingSession({ model: 'gpt-4o-mini', maxMessages: 20, keepRecent: 8 });
for (const msg of conversation) {
await run(agent, msg, { session: mem }); // old turns fold into a note; recent stay verbatim
}
The summarizer is pluggable: model= builds a governed one-shot summarizer (llm_summarizer) —
its model call rides the same budget/audit seams as everything else — or pass your own
summarizer= callable for offline/extractive summaries.
Fitting memory to the window — context_budget
Where SummarizingSession folds, context_budget trims: set
Agent(context_budget=8000) and each turn assembles the history to that token budget via
contextkit (with squeeze compression when installed),
emitting an audited AssemblyReport — a receipt of what was kept, shrunk, or dropped.
Long-term / semantic memory
For facts that should survive across sessions, memory becomes retrieval: back a VectorIndex
(or your own vector store) and attach it as the agent’s retriever — past facts come back by
relevance, injected exactly like RAG documents. See Retrieval (RAG).
So, in one line each: conversation memory = Session / SummarizingSession · durable memory =
SQLiteSessionStore · window fitting = context_budget · semantic memory = a retriever ·
crash recovery = checkpoint=.
How it works
%%{init: {"flowchart": {"htmlLabels": false}} }%%
graph TD
S["Session<br/>(conversation, in memory)"]
ST["SQLiteSessionStore<br/>(durable, many conversations)"]
SUM["SummarizingSession<br/>(folds old turns into a note)"]
RUN["run(agent, input, session=...)"]
CK["context_budget -><br/>contextkit assembly (a receipt per turn)"]
MODEL["the model call"]
ST -->|load / save| S
SUM -->|is a| S
S -->|history in| RUN --> CK --> MODEL
MODEL -->|turns written back| S
classDef seam fill:#2563EB,color:#ffffff,stroke:#1E40AF;
class MODEL seam;
Plugs into the stack
Assembly decisions are audited: when context_budget is set, each turn’s AssemblyReport
lands on the bus, so an AuditLog records what the model
actually saw — not just what it answered. Session files and SQLite stores are plain local
artifacts; put them on shared storage if processes must hand off.
Honest limits
Sessionis not thread-safe shared state — one conversation, one writer at a time. For concurrent users, load/save per conversation viaSQLiteSessionStore.- Summarization is lossy by design.
SummarizingSessionkeeps the gist, not the transcript; when the verbatim record matters, that’s the audit chain’s job. context_budgettrims silently past the budget — deliberately. Read theAssemblyReport(or the audit entry) when you need to know what was dropped.- No distributed memory. Stores are local files by design; sharing them across machines is your storage layer’s job, not the SDK’s.