Overview
Memory gives agents the ability to remember. Without it, every agent.run() is a blank slate. With it, agents maintain conversation history, learn user preferences, track entities, remember decisions, and build knowledge graphs.
All memory features share a single storage backend and are configured through the memory field on AgentConfig.
Quick Start
import { Agent, openai, InMemoryStorage } from "@agentium/core";
const agent = new Agent({
name: "assistant",
model: openai("gpt-4o"),
memory: {
storage: new InMemoryStorage(),
},
});
// First conversation
await agent.run("My name is Alex", { sessionId: "s1", userId: "u1" });
// Later — the agent remembers
const result = await agent.run("What's my name?", { sessionId: "s1", userId: "u1" });
// "Your name is Alex."
UnifiedMemoryConfig
The complete memory configuration. Only storage is required — everything else is opt-in.
Reading the boolean | XConfig types. Several features (like summaries, userFacts, entities) accept a value of type boolean | SomeConfig. That’s a union — it means you have three ways to set each one:entities: false // OFF (or just omit it entirely)
entities: true // ON with default settings ← the shortcut
entities: { namespace: "acme" } // ON with custom settings
So when the table says boolean | EntityConfig, both entities: true and entities: { namespace: "acme" } are valid — true is simply a shorthand for “turn it on, use the defaults.” Pass the config object only when you want to change a default. This is why you’ll see both forms throughout the examples.(Features that always need a backend — learnings needs a vectorStore, graph needs a GraphStore — take only a config object, not a boolean, because there’s no sensible default without that backend.)
| Property | Type | Default | Description |
|---|
storage | StorageDriver | Required | Storage backend shared by all memory subsystems. See Storage |
maxMessages | number | 50 | Maximum messages kept in session history. Oldest are trimmed first when exceeded |
maxTokens | number | undefined | Token-based history trimming. When set, history is trimmed to fit within this token count (instead of message count) |
summaries | boolean | SummaryConfig | true (ON) | Long-term conversation summaries. Auto-summarizes overflow messages. Pass false to disable |
userFacts | boolean | UserFactsConfig | false (OFF) | Extract and store facts about the user (preferences, location, interests). Pass true for defaults |
userProfile | boolean | UserProfileConfig | false (OFF) | Structured user profile (name, role, timezone, language). Pass true for defaults |
entities | boolean | EntityConfig | false (OFF) | Entity memory — companies, people, projects extracted from conversations |
decisions | boolean | DecisionConfig | false (OFF) | Decision audit trail — what the agent decided and why |
learnings | LearningsConfig | undefined (OFF) | Vector-backed learned insights from interactions. Requires a vectorStore |
corrections | CorrectionsConfig | undefined (OFF) | Structured human corrections of agent output, retrieved at inference time. Requires a vectorStore. See Correction Capture |
graph | GraphMemoryConfig | undefined (OFF) | Knowledge graph — entity-relationship graph with temporal awareness. Requires a GraphStore |
procedures | boolean | ProceduresConfig | false (OFF) | Procedural memory — records successful tool-call workflows for reuse |
contextBudget | ContextBudgetConfig | undefined | Token budget allocation for how memory context is distributed |
model | ModelProvider | Agent’s primary model | Separate (cheaper) model used for background extraction (summaries, facts, entities, etc.) |
timezone | string | undefined (UTC) | IANA timezone (e.g. "Asia/Kolkata") used to anchor date-relative extraction ("today", "yesterday"). Always set in production — otherwise users near midnight get the wrong date extracted |
tenantId | string | undefined | Tenant identifier. Required for scope: "tenant" learnings/procedures to be saved and read back. See Scope Hierarchy |
eventBus | EventBus | Agent’s event bus | Auto-injected from the Agent. Memory mutations and extraction failures emit events here for observability. See Observability |
Memory Architecture
Memory is not a single store — it’s a layered subsystem with one orchestrator coordinating up to nine specialized stores over a shared storage backend.
┌──────────────────────────────────────────────────┐
│ Agent │
│ beforeRun → buildContext() → LLM ⇄ tools │
│ afterRun → appendMessages + background extract │
└────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ MemoryManager │
│ buildContext() · appendMessages() · afterRun() │
│ recall() · remember() · forget() · curator │
└───┬──────┬──────┬──────┬──────┬──────┬──────┬─────┘
▼ ▼ ▼ ▼ ▼ ▼ ▼
Sessions Summary Facts Profile Entity Decision …
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────┐
│ StorageDriver interface │
│ MongoDB · Postgres · Redis · SQLite · … │
└──────────────────────────────────────────────────┘
Learnings ──► VectorStore (Qdrant · Pinecone · …)
Graph ──► GraphStore (Neo4j · InMemory)
The memory lifecycle
Every agent.run() wraps the LLM call with two memory phases:
-
Before the run — Context Assembly.
MemoryManager.buildContext() reads every enabled store, ranks the results, fits them within the token budget, and injects the result into the system prompt. Each block is wrapped in a <memory section="…" scope="current_user"> sentinel so the LLM never conflates one store’s data with another’s.
-
After the run — Background Extraction.
MemoryManager.afterRun() fires (non-blocking) and runs up to six parallel extractions against the last 6 turns of the conversation: user facts, profile, entities, learnings, graph, procedures. Failures emit memory.error events rather than failing silently.
// Conceptually, every run looks like this:
const ctx = await memory.buildContext(sessionId, userId, input, agentName); // phase 1
const output = await llmLoop.run([systemPrompt + ctx, ...history, input]);
memory.afterRun(sessionId, userId, recentTurns, extractModel, agentName); // phase 2 (async)
The stores
| Store | Holds | Default | Scope |
|---|
| Sessions | Chat history | ON | sessionId |
| Summaries | Compressed old history | ON | sessionId |
| User Facts | ”User lives in Mumbai” | OFF | userId |
| User Profile | Structured identity | OFF | userId |
| Entities | Companies/people/projects | OFF | userId |
| Decisions | Audit trail of choices | OFF | agentName + sessionId |
| Learnings | Vector-backed insights | OFF | userId → agent → tenant → global |
| Procedures | Reusable tool workflows | OFF | userId → agent → tenant → global |
| Graph | Entity-relationship graph | OFF | userId |
The same memory config works identically across Agent, VoiceAgent, and BrowserAgent. The only difference: voice agents load context once at session start (not per turn), because the realtime session is persistent.
Sessions and History
Sessions are the foundation. Every agent.run() with a sessionId appends messages to a persistent history.
memory: {
storage: new SqliteStorage("app.db"),
maxMessages: 30, // Keep last 30 messages (15 turns)
}
What happens when maxMessages is exceeded: The oldest messages are removed from the session. If summaries is enabled (the default), those removed messages are first summarized so the context isn’t lost entirely.
Token-based trimming: Instead of counting messages, you can limit by tokens:
memory: {
storage,
maxTokens: 8000, // Keep history within ~8K tokens
}
Summaries
Default: ON. When session history overflows maxMessages, the overflow messages are summarized by the LLM and stored. These summaries are injected into the system prompt on future runs so the agent remembers older context.
SummaryConfig
| Property | Type | Default | Description |
|---|
maxCount | number | 10 | How many summary snippets to keep per conversation. Oldest pruned first |
maxTokens | number | 2000 | Token budget for the summary text shown to the agent each run |
Understanding the options:
maxCount caps how many “recap notes” exist for a single long conversation. Each overflow event creates one summary. More summaries = longer memory of the conversation’s arc, but older ones eventually drop off. Lower it for short-lived chats; raise it for long-running threads (support cases that span weeks).
maxTokens is the budget for how much of those summaries gets injected into the prompt. This directly affects cost — every run pays for these tokens. Lower it (e.g. 1000) to save money; raise it (e.g. 4000) when the conversation history is rich and the agent needs more of it.
// Default behavior (summaries ON)
memory: { storage }
// Smaller footprint — fewer, shorter summaries (cheaper)
memory: {
storage,
summaries: { maxCount: 5, maxTokens: 1000 },
}
// Disable summaries entirely
memory: {
storage,
summaries: false,
}
User Facts
Default: OFF. Automatically extracts persistent facts about the user from conversations: preferences, location, profession, interests, communication style.
Facts are stored per userId and injected into the system prompt on every run for that user, across all sessions.
UserFactsConfig
| Property | Type | Default | Description |
|---|
maxFacts | number | 100 | Maximum active facts kept per user. When exceeded, the least important + oldest are pruned |
Understanding maxFacts:
This is the ceiling on how much the agent remembers about one person. When a user accumulates more facts than this, the framework keeps the most important and recent ones (not just the newest — a high-importance fact like “allergic to peanuts” outranks a trivial recent one).
- Default
100 is plenty for most consumer apps.
- Lower it (e.g.
30) to keep prompts lean and costs down when you only care about a handful of key preferences.
- Raise it (e.g.
300) for power users or B2B accounts where the agent benefits from deep history. Higher values mean more storage and slightly larger prompts.
memory: {
storage,
userFacts: true, // Enable with defaults (max 100 facts)
}
// Lean setup — keep only the most important 30 facts
memory: {
storage,
userFacts: { maxFacts: 30 },
}
What gets extracted: The LLM analyzes each conversation and extracts concrete facts like:
- “User prefers dark mode”
- “User is a software engineer in Mumbai”
- “User’s favorite language is TypeScript”
Contradiction handling: If a new fact contradicts an old one (e.g., user moved from Mumbai to Berlin), the old fact is soft-deleted (invalidatedAt is set) and the new one takes over. Each invalidation records a reason:
"superseded" — a newer fact about the same subject replaced it (e.g. “moved to Berlin” supersedes “lives in Mumbai”). Superseded facts disappear silently — they are never shown to the model.
"forgotten" — the user explicitly asked the agent to forget the fact (e.g. “forget my birthday”). Forgotten facts are surfaced in a dedicated “the user asked you to forget these” block so the model knows not to restate them, and recall_user_facts will not return them.
This distinction matters: an over-cautious model that sees both the new fact and an old one in the “forget” block can refuse to answer at all. Separating the two reasons fixes that. Re-stating a previously forgotten fact reactivates it rather than dropping it.
Timezone-aware dates: facts like “my birthday is today” are resolved to an absolute date using the timezone config. Without it, a user near midnight gets the wrong date. Recurring events (birthdays, anniversaries) are stored without a year unless the user states one explicitly.
User Profile
Default: OFF. A structured profile object with built-in fields (name, role, timezone, language) plus custom fields. More structured than user facts.
UserProfileConfig
| Property | Type | Default | Description |
|---|
customFields | string[] | [] | Extra named fields the agent is allowed to fill in, beyond the built-ins |
Understanding customFields:
The profile always tracks four built-in fields automatically: name, role, timezone, language. customFields lets you add your own domain-specific fields the extractor will look for.
- It’s a whitelist — the agent can only populate the fields you list. It can’t invent a
salary field unless you ask for it. This keeps the profile clean and predictable.
- Use it for structured attributes your product cares about:
"company", "plan" (free/pro/enterprise), "department", "accountTier".
- Don’t use it for free-form preferences (“likes dark mode”) — those belong in User Facts, which is unstructured by design.
memory: {
storage,
userProfile: true, // Track only the built-ins: name, role, timezone, language
}
// Add product-specific fields
memory: {
storage,
userProfile: { customFields: ["company", "department", "plan"] },
}
Profile vs Facts: use Profile for a fixed set of structured attributes (one value each, like a form). Use Facts for open-ended things you can’t predict in advance.
Entity Memory
Default: OFF. Extracts companies, people, projects, and products mentioned in conversations. Each entity has facts, events, and relationships.
EntityConfig
| Property | Type | Default | Description |
|---|
namespace | string | "global" | A label that partitions entities into separate buckets |
Understanding namespace:
Entities are always scoped to the user who created them (privacy). namespace adds a second, orthogonal partition on top of that — think of it as which “team workspace” the entities belong to.
- Leave it
"global" (default) if you have one product and don’t need to separate entity sets. Simplest, works for most apps.
- Set a per-tenant namespace (
"acme", "meridian") when one deployment serves multiple organizations and you want each org’s entity knowledge kept in its own bucket.
- Use a hierarchical path (
"acme/engineering", "acme/sales") when you want sub-divisions within a tenant — e.g. the engineering team’s “Stripe” entity shouldn’t mix with the finance team’s.
// Default — one shared entity space
memory: { storage, entities: true }
// Per-team workspace
memory: { storage, entities: { namespace: "acme/engineering" } }
Rule of thumb: start with the default. Only set a namespace once you actually have separate teams or tenants whose entity knowledge should not mix.
What gets extracted: After each conversation, the LLM identifies entities and stores structured data:
- Entity: “Stripe” (type: company) — facts: [“Payment processor”, “Used for billing”]
- Entity: “Raj” (type: person) — facts: [“Frontend engineer”, “Works on checkout”]
Decision Log
Default: OFF. Records what the agent decided and why. Useful for auditing and learning from past decisions.
DecisionConfig
| Property | Type | Default | Description |
|---|
maxContextDecisions | number | 5 | How many recent decisions are reminded to the agent on each run |
Understanding maxContextDecisions:
Every decision the agent logs is stored permanently (you can always search the full history with the search_decisions tool). This setting only controls how many of the most recent decisions are automatically shown to the agent at the start of each run, so it stays consistent with what it decided before.
- Lower it (e.g.
3) to save prompt tokens, or if recent decisions aren’t very relevant to the next one.
- Raise it (e.g.
10) for agents that need strong continuity — e.g. a negotiation or approval agent that should remember its recent rulings to avoid contradicting itself.
- It does not limit how many decisions are stored — only how many are surfaced in context. Storage is unlimited.
memory: {
storage,
decisions: true, // remind the agent of its last 5 decisions
}
// High-continuity agent — remember more recent rulings
memory: {
storage,
decisions: { maxContextDecisions: 10 },
}
When enabled, the agent gets log_decision, record_outcome, and search_decisions tools automatically.
Learned Knowledge
Default: OFF. Vector-backed insights that the agent learns over time. Requires a vector store for semantic search.
LearningsConfig
| Property | Type | Required | Default | Description |
|---|
vectorStore | VectorStore | Yes | — | Where learnings are indexed for semantic search |
collection | string | No | "agentium_learnings" | The “table name” inside the vector store |
topK | number | No | 3 | How many of the most relevant learnings to inject each run |
Understanding the options:
vectorStore is required because learnings are recalled by meaning, not exact match — that needs a vector index (Qdrant, Pinecone, in-memory, etc.). This is why learnings takes a config object, not just true: there’s no default backend.
collection is the named bucket within that store. Change it only if you run multiple agents that should keep their learnings in separate collections within the same vector database.
topK controls how many learnings are pulled into the prompt per run. Higher = the agent considers more accumulated knowledge but spends more tokens and risks diluting focus. 3 is a good default; raise to 5–8 for knowledge-heavy agents (research, support playbooks), lower to 1–2 for tight token budgets.
import { InMemoryVectorStore, OpenAIEmbedding } from "@agentium/core";
memory: {
storage,
learnings: {
vectorStore: new InMemoryVectorStore(new OpenAIEmbedding()),
topK: 5, // pull the 5 most relevant insights each run
},
}
See Scope Hierarchy for sharing learnings across a team or tenant.
Graph Memory
Default: OFF. A knowledge graph where entities are connected by typed, directed relationships. Enables the agent to answer questions that require traversing relationships (e.g., “Who works with Raj?”).
GraphMemoryConfig
| Property | Type | Required | Default | Description |
|---|
store | GraphStore | Yes | — | Where the relationship graph lives (InMemoryGraphStore or Neo4jGraphStore) |
autoExtract | boolean | No | true | Whether to auto-detect entities & relationships from each conversation |
maxContextNodes | number | No | 10 | How many connected nodes to pull into the prompt |
Understanding the options:
store is required — the graph needs somewhere to live. Use InMemoryGraphStore for development and Neo4jGraphStore for production (persistent, queryable at scale).
autoExtract: true (default) means the agent automatically builds the graph as people talk (“Raj works at Acme” → creates Raj —works_at→ Acme). Set it to false if you want to build the graph manually via tools and avoid the extra extraction LLM call per turn.
maxContextNodes caps how much of the graph is summarized into the prompt. Higher = the agent sees more of the relationship web (better for “who connects to whom” questions) but uses more tokens. Raise it for richly-connected domains (org charts, supply chains); keep it low otherwise.
import { InMemoryGraphStore } from "@agentium/core";
memory: {
storage,
graph: {
store: new InMemoryGraphStore(),
maxContextNodes: 15, // surface up to 15 connected entities
},
}
See Graph Memory for traversal, Neo4j setup, and temporal awareness.
Procedural Memory
Default: OFF. Records successful multi-step tool workflows and suggests them when similar tasks arise.
ProceduresConfig
| Property | Type | Default | Description |
|---|
maxProcedures | number | 50 | Maximum saved playbooks before the least-used are pruned |
Understanding maxProcedures:
Each procedure is a learned, reusable playbook (“to do X, call these tools in this order”). This caps how many the agent keeps. When the limit is hit, the least-used / oldest procedures are dropped first, so frequently-successful workflows survive.
- Default
50 suits most agents — they only repeat a handful of distinct workflows.
- Raise it (e.g.
200) for agents that handle many distinct repeatable tasks (a broad ops agent).
- Lower it if you want the agent to only retain its few most-proven workflows.
memory: {
storage,
procedures: true, // keep up to 50 playbooks
}
// Broad ops agent with many distinct workflows
memory: {
storage,
procedures: { maxProcedures: 200 },
}
See Procedural Memory for details and the Scope Hierarchy for sharing procedures across a team.
Scope Hierarchy
Most memory is personal — “User prefers dark mode” belongs to one user. But some knowledge is genuinely shared: a workflow like “how to reconcile an invoice with a PO mismatch” belongs to a whole team, and a policy like “refunds over $500 need VP approval” belongs to the whole organization.
Learnings and Procedures support an explicit four-level scope so shared knowledge isn’t trapped in one user’s silo:
global ← rarely used; built-in defaults, visible to everyone
↑
tenant ← organization-wide (requires memory.tenantId)
↑
agent ← workflow / role knowledge, shared across all users of this agent
↑
user ← personal (default)
| Scope | Visible to | Requires |
|---|
"user" (default) | only the saving user | userId in run context |
"agent" | every user of that agent | agentName (auto-set by Agent) |
"tenant" | every user/agent in the tenant | memory.tenantId |
"global" | everyone | nothing |
Reads union all accessible scopes. When Alice (user) talks to the invoice-recon agent at tenant acme, a recall_procedure / search_learnings call returns her personal items plus the agent’s shared items plus the tenant’s items plus globals — but never another user’s personal scope.
Writes pick one scope. The save_learning tool exposes a scope parameter so the model can promote an insight to the team or org level:
// The agent's save_learning tool call:
save_learning({
title: "Vendor X line-item drift",
content: "Vendor X invoices consistently show $0.10–$0.50 drift per line.",
context: "invoice reconciliation",
scope: "agent", // ← share with everyone using the invoice-recon agent
})
Saving directly through the store:
const agent = new Agent({
name: "invoice-recon",
model: openai("gpt-4o"),
memory: {
storage,
learnings: { vectorStore },
tenantId: "acme-corp", // required for tenant-scoped reads
},
});
const learnings = agent.memory!.getLearnedKnowledge()!;
// Org-wide policy — every user/agent in acme-corp sees it
await learnings.saveLearning({
title: "Refund approval policy",
content: "Refunds above $500 require VP approval.",
context: "refunds",
tags: ["policy"],
namespace: "default",
scope: "tenant",
tenantId: "acme-corp",
});
// A union read returns user + agent + tenant + global learnings
const results = await learnings.searchLearnings("refund policy", {
userId: "alice",
agentName: "invoice-recon",
tenantId: "acme-corp",
topK: 5,
});
Auto-extracted learnings/procedures always save as scope: "user". The framework never auto-promotes an LLM-extracted insight to a shared scope — promotion requires an explicit caller decision. This prevents one user’s accidental statement from leaking to the whole team.
See Multi-User Isolation for the full scope contract and security guarantees.
Context Budget
Controls how the memory context string is distributed across sections when injected into the system prompt.
ContextBudgetConfig
| Property | Type | Default | Description |
|---|
maxTokens | number | undefined | A hard cap on how many tokens all memory combined may use in the prompt |
priorities | Record<string, number> | undefined | Relative weights deciding which memory types get the budget when space is tight |
Understanding the options:
Once you enable several memory types, their combined context can grow large. contextBudget is the spending limit for that combined block and the rule for who gets priority when it overflows.
maxTokens — the ceiling for the entire memory section of the prompt. Without it, every enabled store is injected in full (fine for small setups, risky once you have many). Set it to keep prompts predictable, e.g. 4000.
priorities — when the stores together exceed maxTokens, higher-weighted sections are kept and lower ones are trimmed or dropped first. Weights are relative, not absolute — { summaries: 3, userFacts: 2, entities: 1 } means summaries get roughly half the budget, facts a third, entities a sixth. Valid keys: "summaries", "userProfile", "userFacts", "entities", "decisions", "graph", "procedures". Any section you don’t list uses its sensible default weight.
Tuning by use case: a support agent might favor summaries (conversation continuity); a research agent might favor learnings; a CRM agent might favor userFacts + entities. Bump the weight of whatever matters most for your job.
memory: {
storage,
userFacts: true,
entities: true,
summaries: true,
contextBudget: {
maxTokens: 4000, // cap total memory at ~4K tokens
priorities: {
summaries: 3, // gets the largest share
userFacts: 2,
entities: 1, // trimmed first if space runs out
},
},
}
All background extraction (summaries, facts, entities, profiles) uses the agent’s primary model by default. This can be expensive. Set model to use a cheaper model:
memory: {
storage,
summaries: true,
userFacts: true,
entities: true,
model: openai("gpt-4o-mini"), // 10x cheaper for background work
}
Full-Featured Example
Every option enabled:
import {
Agent, openai, MongoDBStorage,
InMemoryVectorStore, OpenAIEmbedding, InMemoryGraphStore,
} from "@agentium/core";
const storage = new MongoDBStorage("mongodb://localhost:27017/myapp");
await storage.initialize();
const agent = new Agent({
name: "full-memory-agent",
model: openai("gpt-4o"),
memory: {
storage,
maxMessages: 30,
summaries: { maxCount: 10, maxTokens: 2000 },
userFacts: { maxFacts: 100 },
userProfile: { customFields: ["company", "plan"] },
entities: { namespace: "global" },
decisions: { maxContextDecisions: 5 },
learnings: {
vectorStore: new InMemoryVectorStore(new OpenAIEmbedding()),
topK: 3,
},
graph: {
store: new InMemoryGraphStore(),
maxContextNodes: 10,
},
procedures: { maxProcedures: 50 },
contextBudget: {
maxTokens: 5000,
priorities: { summaries: 3, userFacts: 2, entities: 1 },
},
model: openai("gpt-4o-mini"), // cheap model for background extraction
timezone: "Asia/Kolkata", // anchor date-relative extraction
tenantId: "acme-corp", // enable tenant-scoped learnings/procedures
// eventBus is auto-injected from the Agent — no need to set it here
},
});
Observability
Memory mutations and failures emit typed events on the agent’s EventBus. The most important is memory.error — without it, a broken extractor (malformed LLM JSON, an unreachable embedding service) fails silently and you only find out weeks later when a user complains.
// Surface silent extraction failures
agent.eventBus.on("memory.error", ({ store, error, agentName }) => {
console.error(`[${agentName}] ${store} extraction failed:`, error.message);
// forward to Sentry / Datadog / Langfuse
});
// Track how big the injected memory context is per run
agent.eventBus.on("memory.context.built", ({ totalTokens, sections }) => {
metrics.histogram("memory_context_tokens", totalTokens);
});
// React to fact mutations
agent.eventBus.on("memory.fact.added", ({ userId, fact, source }) => {
console.log(`[${userId}] learned (${source}): ${fact}`);
});
| Event | When | Payload |
|---|
memory.fact.added | A new user fact is stored | { userId, fact, source, importance } |
memory.fact.invalidated | A fact is superseded or forgotten | { userId, factId, reason } |
memory.extract | Background extraction kicked off | { sessionId, userId, agentName } |
memory.context.built | buildContext returned | { sessionId, totalTokens, sections } |
memory.error | Any background extraction failed | { store, error, agentName } |
These flow into @agentium/observability (OpenTelemetry, Prometheus, Langfuse) with no extra wiring.
Multi-Tenant Example
A single agent serving many organizations. Personal facts stay private; tenant policies are shared org-wide.
import { Agent, openai, MongoDBStorage, InMemoryVectorStore, OpenAIEmbedding } from "@agentium/core";
function agentForTenant(tenantId: string) {
return new Agent({
name: "support",
model: openai("gpt-4o"),
memory: {
storage: new MongoDBStorage(`mongodb://localhost/acme`),
userFacts: true,
learnings: { vectorStore: new InMemoryVectorStore(new OpenAIEmbedding()) },
tenantId, // scopes tenant-level learnings
timezone: "America/New_York",
model: openai("gpt-4o-mini"),
},
});
}
const acme = agentForTenant("acme-corp");
const meridian = agentForTenant("meridian-llc");
// Alice's personal facts are private to Alice within acme-corp
await acme.run("I prefer email over phone", { userId: "alice", sessionId: "s1" });
// A tenant-wide policy saved once, visible to every acme-corp user/agent
await acme.memory!.getLearnedKnowledge()!.saveLearning({
title: "Escalation policy",
content: "Tier-2 issues escalate to a human after 2 failed attempts.",
context: "support escalation",
tags: ["policy"],
namespace: "default",
scope: "tenant",
tenantId: "acme-corp",
});
// meridian-llc users never see acme-corp's policy or Alice's facts
await meridian.run("How do escalations work?", { userId: "bob", sessionId: "s2" });
Debugging Memory Context
If the model seems to “forget” something, inspect exactly what buildContext produced:
const mm = agent.memory!;
const ctx = await mm.buildContext(
"session-abc", // sessionId
"user-42", // userId
"what's my order?", // currentInput (drives relevance ranking)
"support", // agentName (drives agent-scoped reads)
);
console.log(ctx);
The output shows each store wrapped in a scope sentinel:
<memory section="userFacts" scope="current_user">
What you know about this user:
- User's name is Akash.
- Akash is based in Mumbai.
</memory>
<memory section="summaries" scope="current_user">
Previous conversation context (most recent first):
...
</memory>
If a section is missing, the store either isn’t enabled, has no data for that user, or wasn’t given the scope identifier it requires (e.g. an agent-scoped learning needs agentName).
When to Enable What
| Feature | Enable When | Cost Impact |
|---|
| Sessions + Summaries | Always (default) | Low — 1 LLM call per overflow |
| User Facts | You need cross-session personalization | Low — 1 LLM call per run for extraction |
| User Profile | You need structured user data (name, role, timezone) | Low |
| Entities | Your conversations reference companies, people, projects | Medium — extraction call per run |
| Decisions | You need audit trails or want the agent to learn from past choices | Negligible — no LLM calls, just storage |
| Learnings | You want vector-search over accumulated knowledge | Medium — requires a vector store |
| Graph | You need relationship traversal (“who works with whom”) | Higher — extraction + graph storage |
| Procedures | Your agent repeats similar multi-tool workflows | Low |
Storage Options
The storage field accepts any StorageDriver. Choose based on your needs:
| Driver | Persistence | Best For |
|---|
InMemoryStorage | None (lost on restart) | Development, testing |
SqliteStorage | File-based | Prototypes, single-server apps |
PostgresStorage | Durable | Production |
MongoDBStorage | Durable | Production |
MySQLStorage | Durable | Production |
RedisStorage | In-memory with persistence | High-throughput, caching |
DynamoDBStorage | Durable | AWS-native, serverless |
import { SqliteStorage } from "@agentium/core";
memory: { storage: new SqliteStorage("app.db") }
import { PostgresStorage } from "@agentium/core";
const storage = new PostgresStorage("postgresql://user:pass@localhost/db");
await storage.initialize();
memory: { storage }
import { MongoDBStorage } from "@agentium/core";
const storage = new MongoDBStorage("mongodb://localhost:27017/myapp");
await storage.initialize();
memory: { storage }
See Storage Overview for setup details for each driver.