Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.agentium.in/llms.txt

Use this file to discover all available pages before exploring further.

Multi-User Isolation

Production agents serve many users. Without strict isolation, a memory layer becomes a privacy and compliance liability — Alice’s facts surface in Bob’s prompt, last week’s customer’s transactions leak into today’s chat, and auto-extracted “learnings” cross-pollinate between unrelated tenants. Agentium’s memory layer is built so that every read and every write is scoped to the calling user. This page documents the exact contract.

The contract

StoreDefault scopeStorage keyRead pathWrite path
SessionssessionsessionIdsession-scopedsession-scoped
Summariessession${sessionId}: (prefix-terminated)session-scopedsession-scoped
User factsuseruserIduser-scopeduser-scoped
User profileuseruserIduser-scopeduser-scoped
Entity memoryusermemory:entities:${namespace}:user:${userId}user-scopeduser-scoped
Graph memoryusernodes/edges tagged with _userId propertyuser-filtereduser-scoped (ids namespaced as ${userId}:${id})
Procedure memoryuser → agent → tenant → globalmemory:procedures:${scope}:${owner}union of scopes the caller is inscope chosen at save time
Decision logagent + session${agentName}:${id}, filtered by sessionIdsession+agent-scopedsession+agent-scoped
Learningsuser → agent → tenant → globalrow carries scope + the matching owner; vector search over-fetches then filtersunion of scopes the caller is inscope chosen at save time

Scope hierarchy (Learnings & Procedures)

Some knowledge is genuinely shared — a workflow template like “invoice reconciliation” belongs to a team, not to whoever first noticed it. Learnings and Procedures support an explicit scope:
global       ← rarely used; built-in defaults

tenant       ← organization-wide policies ("Refunds > $500 need VP sign-off")

agent        ← workflow / role knowledge ("Vendor X invoices always drift")

user         ← personal preference (default)
When the agent saves a learning or procedure, it picks the scope:
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
});
When the agent reads, it sees the union of every scope it’s authorized to access — its personal scope, its current agent’s scope, and its tenant’s scope. There is no “see another user’s personal scope” path, ever.

Defaults are safe

  • scope defaults to "user" when omitted on a save — so existing code that didn’t know about scopes keeps the same isolation guarantees.
  • Auto-extracted learnings/procedures always save as "user" — the framework never auto-promotes an LLM-extracted insight to a shared scope without the caller explicitly choosing that.
  • Pre-v2.3 data without a scope field is treated as user-scoped on read (the safe fallback).

Required identifiers per scope

A scope is only useful if the framework knows the relevant identifier:
ScopeRequired identifierSource
useruserIdRunContext.userId
agentagentNameRunContext.metadata.agentName (auto-set by Agent)
tenanttenantIdmemory.tenantId config field
globalnonealways visible
If a caller tries to save with scope: "agent" but no agentName is in context, the save fails with a clear error. There is no silent fallback that could leak data.

Visibility example

A user alice chatting with the invoice-recon agent inside tenant acme sees the union of:
  • Her personal user-scoped learnings (saved under userId: "alice")
  • The agent’s shared workflow knowledge (saved under agentName: "invoice-recon")
  • The org’s policies (saved under tenantId: "acme")
  • All global learnings (the built-in defaults)
She does not see:
  • Bob’s personal learnings (different userId)
  • Learnings saved against the hr-agent (different agentName)
  • Any other tenant’s policies (different tenantId)
Every prefix-list query terminates the prefix with the key separator (${sessionId}: rather than ${sessionId}) so that session "abc" cannot match "abc123".

What the agent sees in the prompt

When MemoryManager.buildContext(sessionId, userId, currentInput, agentName) returns, each section is wrapped in an explicit scope marker:
<memory section="userFacts" scope="current_user">
What you know about this user:
Facts the user told you directly (high confidence):
- User's name is Akash.
- Akash is based in Mumbai.
</memory>

<memory section="entities" scope="current_user">
Known entities:
- Acme Corp (company): Logistics platform
</memory>
The scope="current_user" attribute is a deliberate signal to the model: these are facts about the user you are currently talking to, not about “some user in the database”. This is the LLM-side complement to the storage-side scoping.

What it means for direct API callers

If you bypass the agent’s auto-exposed tools and call a memory store directly, you must pass a userId. The signatures enforce it at the TypeScript layer:
const entities = agent.memory!.getEntityMemory()!;

// ✅ Required
await entities.upsertEntity("user-123", { name: "Acme", entityType: "company" });
const list = await entities.listEntities("user-123");

// ❌ TypeScript error: Expected 1-2 arguments, but got 0.
await entities.listEntities();
The same applies to GraphMemory.extractFromConversation(), ProcedureMemory.saveProcedure(), and friends. There is no “global” read path on these stores anymore — by design.

What stores share data intentionally

A small number of paths share data deliberately. None of them leak user content.
  • Decision Log — keyed by agentName + sessionId. Two different users in two different sessions of the same agent will not see each other’s decisions, but the log is queryable across users by agent for auditing.
  • Tenant namespace on Entity Memory — the namespace field on EntityMemory is a tenant-level partition orthogonal to userId. Set it per-tenant to prevent userId collisions across tenants (alice@apex vs alice@meridian).
  • Learnings vector store — the vector index itself is shared (cost-saving) but every result is post-filtered by userId before it leaves the store.

Right to be forgotten

The Curator’s clearAll({ userId }) wipes every store’s data for that user — user facts, profile, entities, and so on. It does not wipe other users’ data even when called from a “global” code path:
await agent.memory!.curator.clearAll({ userId: "user-123" });
// Removes:
//   - user-123's facts
//   - user-123's profile
//   - user-123's entities
//   - user-123's procedures (if enabled)
// Does NOT touch any other user's data.
This is your GDPR Article 17 implementation path. Pair it with audit logs (see Compliance) for a fully documented erasure flow.

Forgotten facts vs superseded facts

When a fact is invalidated, the framework records why:
  • invalidationReason: "superseded" — a newer fact about the same subject replaced it. Typical case: user said “I work at Google now” after the store knew “I work at Shipment”. The old fact becomes a silent tombstone used only by the Curator for analytics.
  • invalidationReason: "forgotten" — the user explicitly asked the agent to forget the fact (e.g. “forget my birthday”). The fact is surfaced in the prompt’s “user asked to forget” block with a strict instruction never to restate it, and recall_user_facts will not return it.
The distinction matters because, without it, an over-cautious LLM will refuse to answer “where do I work?” — seeing both the new active fact and the superseded one in the forget block, it concludes it shouldn’t talk about employment at all.

What this gives you for free

  • Multi-tenant SaaS — set a tenant-level namespace on entity memory, user-scope handles the rest.
  • GDPR / CCPA erasure — one curator call wipes a single user.
  • Audit trail — every memory mutation emits an event on the bus.
  • Strict TypeScript — calling memory APIs without a userId is a compile-time error, not a runtime privacy bug.

Cross-References