Skip to main content

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.)
PropertyTypeDefaultDescription
storageStorageDriverRequiredStorage backend shared by all memory subsystems. See Storage
maxMessagesnumber50Maximum messages kept in session history. Oldest are trimmed first when exceeded
maxTokensnumberundefinedToken-based history trimming. When set, history is trimmed to fit within this token count (instead of message count)
summariesboolean | SummaryConfigtrue (ON)Long-term conversation summaries. Auto-summarizes overflow messages. Pass false to disable
userFactsboolean | UserFactsConfigfalse (OFF)Extract and store facts about the user (preferences, location, interests). Pass true for defaults
userProfileboolean | UserProfileConfigfalse (OFF)Structured user profile (name, role, timezone, language). Pass true for defaults
entitiesboolean | EntityConfigfalse (OFF)Entity memory — companies, people, projects extracted from conversations
decisionsboolean | DecisionConfigfalse (OFF)Decision audit trail — what the agent decided and why
learningsLearningsConfigundefined (OFF)Vector-backed learned insights from interactions. Requires a vectorStore
correctionsCorrectionsConfigundefined (OFF)Structured human corrections of agent output, retrieved at inference time. Requires a vectorStore. See Correction Capture
graphGraphMemoryConfigundefined (OFF)Knowledge graph — entity-relationship graph with temporal awareness. Requires a GraphStore
proceduresboolean | ProceduresConfigfalse (OFF)Procedural memory — records successful tool-call workflows for reuse
contextBudgetContextBudgetConfigundefinedToken budget allocation for how memory context is distributed
modelModelProviderAgent’s primary modelSeparate (cheaper) model used for background extraction (summaries, facts, entities, etc.)
timezonestringundefined (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
tenantIdstringundefinedTenant identifier. Required for scope: "tenant" learnings/procedures to be saved and read back. See Scope Hierarchy
eventBusEventBusAgent’s event busAuto-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:
  1. 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.
  2. 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

StoreHoldsDefaultScope
SessionsChat historyONsessionId
SummariesCompressed old historyONsessionId
User Facts”User lives in Mumbai”OFFuserId
User ProfileStructured identityOFFuserId
EntitiesCompanies/people/projectsOFFuserId
DecisionsAudit trail of choicesOFFagentName + sessionId
LearningsVector-backed insightsOFFuserIdagent → tenant → global
ProceduresReusable tool workflowsOFFuserIdagent → tenant → global
GraphEntity-relationship graphOFFuserId
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

PropertyTypeDefaultDescription
maxCountnumber10How many summary snippets to keep per conversation. Oldest pruned first
maxTokensnumber2000Token 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

PropertyTypeDefaultDescription
maxFactsnumber100Maximum 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

PropertyTypeDefaultDescription
customFieldsstring[][]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

PropertyTypeDefaultDescription
namespacestring"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

PropertyTypeDefaultDescription
maxContextDecisionsnumber5How 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

PropertyTypeRequiredDefaultDescription
vectorStoreVectorStoreYesWhere learnings are indexed for semantic search
collectionstringNo"agentium_learnings"The “table name” inside the vector store
topKnumberNo3How 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

PropertyTypeRequiredDefaultDescription
storeGraphStoreYesWhere the relationship graph lives (InMemoryGraphStore or Neo4jGraphStore)
autoExtractbooleanNotrueWhether to auto-detect entities & relationships from each conversation
maxContextNodesnumberNo10How 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

PropertyTypeDefaultDescription
maxProceduresnumber50Maximum 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)
ScopeVisible toRequires
"user" (default)only the saving useruserId in run context
"agent"every user of that agentagentName (auto-set by Agent)
"tenant"every user/agent in the tenantmemory.tenantId
"global"everyonenothing
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

PropertyTypeDefaultDescription
maxTokensnumberundefinedA hard cap on how many tokens all memory combined may use in the prompt
prioritiesRecord<string, number>undefinedRelative 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
    },
  },
}

Using a Cheaper Model for Extraction

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
}

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}`);
});
EventWhenPayload
memory.fact.addedA new user fact is stored{ userId, fact, source, importance }
memory.fact.invalidatedA fact is superseded or forgotten{ userId, factId, reason }
memory.extractBackground extraction kicked off{ sessionId, userId, agentName }
memory.context.builtbuildContext returned{ sessionId, totalTokens, sections }
memory.errorAny 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

FeatureEnable WhenCost Impact
Sessions + SummariesAlways (default)Low — 1 LLM call per overflow
User FactsYou need cross-session personalizationLow — 1 LLM call per run for extraction
User ProfileYou need structured user data (name, role, timezone)Low
EntitiesYour conversations reference companies, people, projectsMedium — extraction call per run
DecisionsYou need audit trails or want the agent to learn from past choicesNegligible — no LLM calls, just storage
LearningsYou want vector-search over accumulated knowledgeMedium — requires a vector store
GraphYou need relationship traversal (“who works with whom”)Higher — extraction + graph storage
ProceduresYour agent repeats similar multi-tool workflowsLow

Storage Options

The storage field accepts any StorageDriver. Choose based on your needs:
DriverPersistenceBest For
InMemoryStorageNone (lost on restart)Development, testing
SqliteStorageFile-basedPrototypes, single-server apps
PostgresStorageDurableProduction
MongoDBStorageDurableProduction
MySQLStorageDurableProduction
RedisStorageIn-memory with persistenceHigh-throughput, caching
DynamoDBStorageDurableAWS-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.