Self-Learning Memory (NextGen Plugin)
Self-Learning Memory (NextGen Plugin)
The Self-Learning Memory system is a brain-like, queryable knowledge store that lets agents learn from observation — the user's profile and any user-given behavior rules are injected into the system message on every turn, and the agent can read or mutate the knowledge graph mid-conversation through 12 dedicated tools. It supersedes both PersistentInstructionsPluginNextGen and UserInfoPluginNextGen.
The underlying MemorySystem is a first-class standalone module: it has no
dependency on Agent or the context plugins. Server code can use its entity and
fact model, vector embeddings, document search, graph traversal, bitemporal
queries, signal ingestion, profile generation, storage adapters, and permission
model directly. The context plugins are adapters that expose that same system
to an LLM safely; they are not the memory implementation itself.
This section is the user-guide-level walkthrough. For the full conceptual model, adapter setup, signal ingestion pipeline, predicate vocabulary, and resolution tiers, see:
- docs/MEMORY_GUIDE.md — the canonical memory layer guide.
- docs/MEMORY_API.md — full
MemorySystemAPI reference. - docs/MEMORY_PERMISSIONS.md — three-principal permission model.
- docs/MEMORY_SIGNALS.md — signal → fact extraction.
- docs/MEMORY_PREDICATES.md — predicate registry.
What it is
Two first-class concepts:
- Entities are pure identity. People, organizations, projects, tasks, events, topics — each carries a
displayName,aliases, strongidentifiers(email, domain, slack_id, github, custom canonical id), and type-specificmetadata(e.g. taskstate/dueAt, eventstartTime/attendeeIds). - Facts are knowledge. Triples like
(John, works_at, Microsoft)or document facts (long-form prose). Facts carryconfidence,importance,sourceSignalId, temporal validity (validFrom/validUntil), supersession links, and can bind to additional entities viacontextIds("this fact is about John but also relates to the Acme deal").
Facts preserve history through supersession; task entities are mutable through
the state-machine API and keep a bounded stateHistory. Every record has an
owner, can be tenant-scoped by group, supports owner/group/world permissions
plus explicit principal ACLs, and is retrievable as profile + ranked facts +
related tasks + related events in one query.
Self-learning loop:
- The user tells the agent something ("I prefer concise answers"), or a background ingestor extracts facts from the conversation.
- A fact lands in the store via
memory_remember(LLM-driven) orSessionIngestorPluginNextGen(passive). - When the new-facts threshold is crossed (default 3), incremental profile regeneration fires in the background — the configured LLM gets the prior profile + new facts + invalidated IDs and returns an evolved profile document.
- On the next turn, the system message reflects the change. No manual prompt engineering.
When to use which plugin
| Plugin | Feature flag | Purpose |
|---|---|---|
WorkingMemoryPluginNextGen |
workingMemory (default true) |
Ephemeral per-session scratchpad with tiered eviction (raw / summary / findings). NOT a knowledge store. |
InContextMemoryPluginNextGen |
inContextMemory (default true) |
Live KV values rendered directly in the system message. Use for small, high-signal state the LLM must always see. |
MemoryPluginNextGen |
memory (default false) |
Recommended. Self-learning knowledge store. Read-side: profile injection + 6 retrieval tools. |
MemoryWritePluginNextGen |
memoryWrite (default false) |
Optional sidecar — adds the 6 write memory_* tools. Requires memory: true. |
SessionIngestorPluginNextGen |
n/a (registered manually) | Background pipeline — extracts facts from each batch of messages and writes them to the same MemorySystem. Pair with memory: true (no memoryWrite) for retrieval-only agents whose memory updates happen passively. |
PersistentInstructionsPluginNextGen, UserInfoPluginNextGen |
persistentInstructions, userInfo |
⚠️ Deprecated — use MemoryPluginNextGen instead. |
Quick Start (in-process, dev)
// Documentation check: complete example
import {
Agent,
createMemorySystemWithConnectors,
InMemoryAdapter,
PredicateRegistry,
} from 'oneringai';
const predicates = PredicateRegistry.standard().register({
name: 'prefers',
description: 'A durable user preference.',
category: 'preference',
payloadKind: 'attribute',
subjectTypes: ['person'],
lifecycle: 'stable',
});
// 1. Build the MemorySystem. Embedder + profile-generator are optional but
// recommended (without them: no semantic search, no profile auto-regen).
const memory = createMemorySystemWithConnectors({
store: new InMemoryAdapter(),
connectors: {
embedding: { connector: 'openai', model: 'text-embedding-3-small', dimensions: 1536 },
profile: { connector: 'anthropic', model: 'claude-sonnet-4-6' },
},
predicates,
predicateMode: 'strict',
visibilityPolicy: () => ({ group: 'read', world: 'none' }),
});
// 2. Agent with feature flags + plugin config (under `context.features` /
// `context.plugins`).
const agent = Agent.create({
connector: 'anthropic',
model: 'claude-sonnet-4-6',
userId: 'alice', // REQUIRED — memory's owner invariant
context: {
model: 'claude-sonnet-4-6',
agentId: 'my-assistant', // optional — auto-generated if omitted
features: {
memory: true, // reads: profile injection + 6 retrieval tools
memoryWrite: true, // writes: 6 mutation tools (omit for retrieval-only)
},
plugins: {
memory: {
memory, // the MemorySystem instance — shared by both plugins
// groupId: 'team-A', // optional, trusted from your auth layer
// userDisplayName: 'Alice Smith',
// userProfileInjection: { topFacts: 20, relatedTasks: true },
},
// `memoryWrite` inherits memory / agentId / userId from plugins.memory.
},
},
});
await agent.run('Remember that I prefer concise answers');
await agent.run('What are my preferences?');
// → The fact is available to retrieval immediately and appears in injected
// top facts; the synthesized profile refreshes after the configured threshold.
The
context.features.memoryandcontext.features.memoryWriteflags are independent.memoryWrite: truerequiresmemory: true. Enable justmemoryfor a retrieval-only agent (and pair withSessionIngestorPluginNextGento keep memory growing passively).
Storage backends
MemorySystem is an IMemoryStore adapter away from any database. Three adapters ship out of the box; full setup details live in docs/MEMORY_GUIDE.md § Choosing a storage backend.
| Adapter | When to use |
|---|---|
InMemoryAdapter |
Tests, REPL, single-user desktop apps that snapshot externally. Zero deps. |
MongoMemoryAdapter + RawMongoCollection |
Production servers using the mongodb driver. Supports native $graphLookup and Atlas Vector Search. |
MongoMemoryAdapter + MeteorMongoCollection |
Meteor apps — writes flow through Meteor's async API and trigger reactive publications. |
import { MongoClient } from 'mongodb';
import {
MongoMemoryAdapter,
RawMongoCollection, type IEntity, type IFact,
} from 'oneringai';
const client = await new MongoClient(url).connect();
const db = client.db('myapp');
const entitiesColl = new RawMongoCollection<IEntity>(db.collection('memory_entities'), client);
const factsColl = new RawMongoCollection<IFact>(db.collection('memory_facts'), client);
const memoryAdapter = new MongoMemoryAdapter({
entities: entitiesColl,
facts: factsColl,
factsCollectionName: 'memory_facts', // required for native $graphLookup
useNativeGraphLookup: true,
vectorIndexName: 'memory_facts_vector',
entityVectorIndexName: 'memory_entities_identity_vector',
entityContentVectorIndexName: 'memory_entities_content_vector',
});
const memory = createMemorySystemWithConnectors({
store: memoryAdapter,
connectors: {
embedding: { connector: 'openai', model: 'text-embedding-3-small', dimensions: 1536 },
profile: { connector: 'anthropic', model: 'claude-sonnet-4-6' },
},
});
// Run once on startup — creates all required performance + correctness indexes.
await memory.ensureAdapterIndexes();
// For semantic search at scale, also create Atlas Vector Search indexes
// programmatically (UI creation is a security footgun — see MEMORY_GUIDE.md).
await memoryAdapter.ensureVectorSearchIndexes({ dimensions: 1536 });
Mongo deployments need two host-managed uniqueness migrations in addition to
ensureAdapterIndexes(): a unique partial index on{identifiers.kind: 1, identifiers.value: 1}to prevent connector/bootstrap identity races, andensureNormalizedNameUniqueIndex(entitiesColl)to make normalized-name find-or-create atomic across processes. For an existing database, backfill normalized fields and deduplicate before installing either index; the library intentionally cannot make that destructive migration for you. See the Memory Guide's Mongo deployment checklist.
What gets injected into the system message
Up to four blocks are rendered when features.memory: true (the rules block and the priorities block only appear when applicable facts exist):
## Your persona — how YOU (the assistant) present yourself
_These lines define the ASSISTANT — you, the AI — NOT the user. Each line begins with `[ruleId=<id>]` — pass that exact id to `memory_set_agent_rule.replaces` to supersede or to `memory_forget.factId` to drop._
- [ruleId=fact_abc123_…] Be terse in replies.
- [ruleId=fact_def456_…] Reply in English again.
## About the User (Alice Smith)
Alice prefers concise replies, leads product strategy at Acme, …
### Recent top facts (up to 20)
- prefers: "concise answers" (conf=1.00)
- works_at: Acme (conf=0.95)
- role: "product lead" (conf=0.9)
## User's Active Priorities ← only when the user has tracks_priority facts
- **Ship NA launch** _(horizon=Q, weight=0.80, deadline=2026-06-30T00:00:00.000Z)_
## About the User's Organization (Acme) ← only when groupBootstrap is set
Acme is a 500-person SaaS company in the logistics space …
### Recent top facts (up to 20)
- …
The blocks are rendered before every LLM call, never trimmed by compaction (isCompactable() === false). Empty blocks are omitted — a brand-new user with no facts gets nothing rendered until something is learned.
Global agent personality / base instructions are NOT auto-rendered any more — set them via
Agent.create({ instructions }). Theagententity still exists for graph queries and as thethis_agentsubject formemory_set_agent_rule, but its profile is not synthesised or injected.
Plugin config
interface MemoryPluginConfig {
memory: MemorySystem; // REQUIRED
agentId: string; // auto-filled from context.agentId
userId: string; // auto-filled from agent.userId
groupId?: string; // TRUSTED — from your auth layer
principals?: string[]; // TRUSTED host-resolved ACL principal set
timezone?: string; // Viewer IANA timezone for rendered instants
// Optional shared persona subject for multiple agent variants.
personaEntityId?: string;
// Permissions stamped on the bootstrapped user / agent / group entities.
userEntityPermissions?: { group?: 'none'|'read'|'write'; world?: 'none'|'read'|'write' };
agentEntityPermissions?: { group?: 'none'|'read'|'write'; world?: 'none'|'read'|'write' };
// Optional org bootstrap — when set AND groupId is set, a third entity
// (`organization`, identifier `system_group_id`) is upserted and rendered
// as "About the User's Organization". Visibility of facts on it is controlled
// by your MemorySystem.visibilityPolicy + per-write permissions.
groupBootstrap?: {
displayName: string;
identifiers?: { kind: string; value: string }[]; // e.g. [{kind:'domain', value:'acme.com'}]
permissions?: { group?: 'none'|'read'|'write'; world?: 'none'|'read'|'write' };
};
// What to inject for each profile block. Defaults to { profile:true, topFacts:20,
// recentActivity: { limit: 20, windowDays: 7 } }.
userProfileInjection?: ProfileInjection;
groupProfileInjection?: ProfileInjection;
// Legacy compatibility field. The current write tools do not consult it when
// the LLM omits visibility; configure MemorySystem.visibilityPolicy instead.
defaultVisibility?: {
forUser?: 'private' | 'group' | 'public';
forAgent?: 'private' | 'group' | 'public';
forOther?: 'private' | 'group' | 'public';
};
autoResolveThreshold?: number; // Fuzzy-match threshold for {surface}. Default 0.9.
userDisplayName?: string; // Used on first bootstrap; ignored if entity exists.
agentDisplayName?: string; // Same.
}
interface ProfileInjection {
profile?: boolean; // Include profile.details. Default true.
topFacts?: number; // Top N ranked facts. 0 disables. Default 20 (cap 100).
factPredicates?: string[]; // Restrict topFacts to these predicates.
relatedTasks?: boolean; // Include active related tasks. Default false.
relatedEvents?: boolean; // Include recent related events. Default false.
identifiers?: boolean; // Render entity identifier list. Default false.
maxFactLineChars?: number; // Cap each rendered fact line. Default undefined (no cap).
recentActivity?: { // Time-ordered "Recent activity" tail. Default ON, limit 20, 7d.
limit?: number; // 0 disables.
windowDays?: number;
predicates?: string[]; // Optional predicate allowlist.
};
}
interface MemoryWritePluginConfig {
memory: MemorySystem; // REQUIRED
agentId: string; // auto-filled
userId: string; // auto-filled
groupId?: string;
predicates?: PredicateRegistry; // Usually inherited from MemorySystem.
defaultVisibility?: { /* same shape as above */ };
autoResolveThreshold?: number;
forgetRateLimit?: { maxCallsPerWindow?: number; windowMs?: number }; // default 10/60s/user; also fallback for setAgentRuleRateLimit
setAgentRuleRateLimit?: { maxCallsPerWindow?: number; windowMs?: number }; // default: falls back to forgetRateLimit, then 10/60s/user
forbiddenEntityTypes?: readonly string[]; // Reject host-managed entity types.
}
The 12 memory_* tools
All tools accept a flexible SubjectRef so the LLM never has to know an entity id:
type SubjectRef =
| string // entity id, "me", or "this_agent"
| { id: string }
| { identifier: { kind: string; value: string } } // exact identifier match
| { surface: string }; // fuzzy resolution by name/alias
Read tools (6) — registered when features.memory: true:
| Tool | What it does | Example |
|---|---|---|
memory_recall |
Profile + top facts + optional tiers (documents / semantic / neighbors) |
{"subject":"me"} · {"subject":{"surface":"Acme deal"},"include":["neighbors"]} |
memory_graph |
N-hop graph traversal (Mongo $graphLookup for direction:'out'/'in'; iterative BFS for 'both') |
{"start":"me","direction":"out","maxDepth":2} |
memory_search |
Semantic text search across embedded facts | {"query":"deployment incidents last quarter","topK":10} |
memory_search_documents |
Search long-form documents (type='document') by content. mode:'semantic'|'keyword'; filters by attachedTo / role. |
{"query":"Q3 launch brief"} · {"query":"TODO","mode":"keyword","role":"notes"} |
memory_find_entity |
Lookup or list by id, identifier, surface, or type+metadata (read-only — actions: find / list) |
{"by":{"identifier":{"kind":"email","value":"alice@a.com"}}} |
memory_list_facts |
Paginated raw fact enumeration; archivedOnly: true returns audit view |
{"subject":"me","predicate":"prefers"} |
Write tools (6) — registered when features.memoryWrite: true (requires memory: true):
| Tool | What it does | Example |
|---|---|---|
memory_remember |
Write an atomic fact (or document fact via kind: "document" + details) |
{"subject":"me","predicate":"prefers","value":"concise"} |
memory_link |
Write a relational fact between two entities | {"from":{"surface":"Alice"},"predicate":"attended","to":{"surface":"Q3 planning"}} |
memory_upsert_entity |
Create or merge an entity by identifiers (multi-ID auto-merge) | {"type":"person","displayName":"Alice","identifiers":[{"kind":"email","value":"alice@a.com"}]} |
memory_forget |
Archive a fact (optionally supersede with a correction). Rate-limited 10/60s/user. | {"factId":"fact_xyz","replaceWith":{"predicate":"role","value":"senior engineer"}} |
memory_restore |
Un-archive a previously forgotten fact (undo). Rejects when superseded by a live successor. | {"factId":"fact_xyz"} |
memory_set_agent_rule |
Record a user-specific behavior rule for THIS agent. Rendered back into the system-message rules block. | {"rule":"Be terse in replies."} · {"rule":"Reply in Russian.","replaces":"fact_abc123"} |
Write visibility: the LLM-facing schemas for memory_remember,
memory_link, and memory_upsert_entity deliberately do not expose a
visibility argument. When those tools write a record, they pass no explicit
permissions and MemorySystem.visibilityPolicy decides what to stamp. Configure
that policy at the memory-system boundary; do not rely on prompt instructions
for authorization. Programmatic callers using MemorySystem directly can pass
explicit permissions.
Multi-ID enrichment — memory_upsert_entity auto-merges identifiers when any one matches an existing entity. Useful when the LLM observes a person via Slack today and via email tomorrow:
{"type":"person","displayName":"Alice Smith",
"identifiers":[{"kind":"email","value":"alice@a.com"},
{"kind":"slack_user_id","value":"U07ABC"}]}
If any identifier already belongs to an entity, the others are added to it — so future memory_find_entity lookups by either identifier resolve to the same Alice.
Behavior rules — memory_set_agent_rule
When the user gives a directive about the agent itself ("be terse", "stop apologizing", "reply in Russian", "your name is Jason now"), the LLM calls memory_set_agent_rule. The rule is stored as a fact on the agent entity scoped to the calling user (ownerId = userId, predicate = 'agent_behavior_rule', importance 0.95, private visibility) and rendered back into the ## Your persona — how YOU (the assistant) present yourself block on every subsequent turn. Each rule shows its ruleId so the LLM can pass replaces to supersede it cleanly when the user contradicts a prior rule.
The plugin's instructions teach the LLM a narrow trigger:
- ✅ Identity / name / persona, role assignment, tone, format, language, meta-interaction rules.
- ❌ Task creation ("remind me to X" →
memory_upsert_entitytask or a tracker connector), calendar actions, factual corrections, user statements about themselves.
Rules are scoped per-user-per-agent — another user of the same agent gets a different set, all derived from the same single agentEntityId plus per-fact ownerId filtering.
Background ingestion via SessionIngestorPluginNextGen
memory_* write tools depend on the agent remembering to call them. For deployments where the agent should be retrieval-only or where you want belt-and-suspenders capture, register SessionIngestorPluginNextGen alongside MemoryPluginNextGen. It hooks onBeforePrepare, snapshots the conversation slice since its watermark, and (when the slice meets minBatchMessages) kicks off an async LLM extraction that writes facts directly to the same MemorySystem.
import { SessionIngestorPluginNextGen } from 'oneringai';
// After Agent.create(...):
agent.context.registerPlugin(new SessionIngestorPluginNextGen({
memory,
agentId: agent.context.agentId,
userId: 'alice',
groupId: 'team-A', // optional, trusted
connectorName: 'fast-extractor', // REQUIRED — typically a cheap, NON-reasoning model
model: 'gpt-4.1',
diligence: 'normal', // 'minimal' | 'normal' | 'thorough'
minBatchMessages: 6, // default
}));
Pair memory: true with the ingestor (and without memoryWrite) for an agent that reads memory but never mutates it directly — useful when the agent operator wants ambient learning without trusting the LLM to write. See docs/MEMORY_GUIDE.md § Learning from agent runs for batching, watermark, dedup-merge, and graceful-shutdown details.
On graceful shutdown the host MUST
await ingestor.flush()before destroying the agent —flush()ignores the batch threshold and awaits completion, so the trailing batch is captured.
For non-agent flows (process this email / text now, no conversation involved), use SignalIngestor directly — see docs/MEMORY_SIGNALS.md. Reference adapters: EmailSignalAdapter, CalendarSignalAdapter, PlainTextAdapter. Custom adapters implement SignalSourceAdapter.
Permissions and scope
Every entity and fact carries:
ownerId(required) — the user who created the record. Owner always has full access.groupId(optional) — the group the record belongs to.permissions: { group?, world? }—'none' | 'read' | 'write'. Defaults:group: 'read',world: 'read'(public-read, owner-write).
Reads are filtered at the storage adapter (defence-in-depth); writes are checked at the MemorySystem layer (assertCanAccess(..., 'write') throws PermissionDeniedError). Scope visibility cuts orthogonal: a record with groupId: 'A' is invisible to callers in group 'B' regardless of permissions.
The plugin config still accepts defaultVisibility for compatibility, but the
current write-tool implementations do not apply it when the LLM omits
visibility. Use MemorySystem.visibilityPolicy for deployment-wide defaults.
memory_set_agent_rule is a special case: it always writes the user-specific
agent rule as private. Writes whose contextIds include foreign or
unverifiable entities are fail-safe restricted to owner-only visibility.
Full model: docs/MEMORY_PERMISSIONS.md.
Principal-based ACLs (explicit grants)
Owner / group / world handles the common cases, but it can't say "this specific other person, agent, or service can read this record." The principal-ACL layer adds that — explicit per-identity grants that sit alongside owner/group/world. It is fully opt-in and backwards compatible: set no acl and pass no scope.principals, and nothing below changes.
Principals are opaque tokens — user:<id>, entity:<id> (typically a person), group:<id>, service:<name>, and world — built with the exported helpers principalUser, principalEntity, principalGroup, principalService, and PRINCIPAL_WORLD. (parsePrincipal(token) parses one back; an unknown prefix is kind: 'unknown', never world.)
Grant access with an acl on any entity or fact:
import { principalEntity } from 'oneringai';
await memory.addFact(
{
subjectId: deal.id,
predicate: 'note',
kind: 'atomic',
details: 'Pricing discussed with Alice',
permissions: { group: 'none', world: 'none' }, // owner-private base
acl: [{ principal: principalEntity('alice'), actions: ['read'] }], // …plus Alice
},
{ userId: 'owner', groupId: 'g1' },
);
ACLEntry is { principal, actions: Array<'read' | 'write'> }; an empty actions array grants nothing (never a silent read). On every write the storage boundary projects ownerId + groupId + permissions + acl into two library-owned arrays on the record — readPrincipals and writePrincipals (writePrincipals ⊆ readPrincipals). Never set these by hand; they're recomputed on each write.
Query in principal mode by passing scope.principals — its presence (any length) is authoritative, and a record is visible iff the token sets intersect:
import { principalUser, principalGroup, principalEntity, PRINCIPAL_WORLD } from 'oneringai';
const scope = {
principals: [
principalUser('alice'), // own user token → owner access
principalGroup('g1'), // groups alice belongs to
principalEntity(alicePersonId), // alice's Person entity (account-link grants)
PRINCIPAL_WORLD, // see public records
],
};
await memory.getFact(id, scope); // visible iff tokens ∩ readPrincipals ≠ ∅
- Empty set (
{ principals: [] }) → sees/authorizes nothing. Absent → the legacy owner/group/world path runs unchanged. - When present,
userId/groupIdare ignored for the access decision — encode them as tokens instead. - The host owns token-set correctness (the library trusts it like
scope.userId): includePRINCIPAL_WORLDto see public records and the caller's ownuser:<id>for owner access (both fail closed if omitted), and include onlygroup:tokens the caller truly belongs to (over-including one leaks that group — fails open).
Mutate access after creation with setAccess (requires write; replaces the acl and re-materializes the arrays — it does not touch ownerId or the permissions block):
await memory.setAccess('fact', factId, [{ principal: principalEntity('bob'), actions: ['read'] }], scope);
"Account links later." When two person entities turn out to be the same identity, mergeEntities(winner, loser, scope) rewrites every entity:<loser> grant to entity:<winner> across all records — so a linked contact's prior facts stay visible under the surviving identity.
⚠️ Migration gate. Principal callers have no legacy fallback: a row written before the principal model (no
readPrincipals) is invisible to ascope.principalscaller. Before any host code starts passingscope.principals, run the idempotent backfill to completion:await memory.backfillAccessPrincipals(scope, { batchSize: 500 }); // → { entitiesScanned, entitiesUpdated, factsScanned, factsUpdated }
The same primitives (materializePrincipals, fromLibraryPermissions, fromNimbleAudit, readFilterForPrincipals, …) are exported so a host can authorize its own collections with identical grammar. Full reference: docs/MEMORY_PERMISSIONS.md § Principal-based ACLs.
Security invariants
The library trusts scope ({userId, groupId}) because the host authenticates the caller. The plugin + tools preserve that trust boundary:
userIdandgroupIdcome from plugin config, NEVER from tool arguments. Tools silently ignore anygroupIdpassed by the LLM. (Otherwise a user in group A could ask the agent to callmemory_remember({..., groupId: "B"})and escalate.)- No ghost-writes.
memory_rememberandmemory_linkreject writes whose subject is owned by another user — the memory layer enforcesfact.ownerId == subject.ownerId, so without this guard a write against someone else's entity would silently attribute the fact to them. The tools return a structured error; the LLM shouldmemory_upsert_entityits own entity instead. contextIdsfail-safe restriction. A write specifyingcontextIdsthat include foreign-owned or unverifiable entities is forced to owner-only whenever the host policy could otherwise choose wider visibility. The response includes a warning. This prevents planting cross-owner facts that surface in another owner's graph walks.- Numeric clamping. All LLM-controllable numeric limits are capped (
maxDepth ≤ 5,topK ≤ 100,limit ≤ 200/500,topFactsLimit ≤ 100,neighborDepth ≤ 5,confidence/importance∈ [0, 1]). DoS and ranking-pollution mitigated. kindis a strict enum. Every fact is'atomic'or'document'. Anything else is rejected at both the tool andMemorySystem.addFactboundary.valueandobjectIdare mutually exclusive. Either relational or attribute, never both.
Using the tools without the plugin
If you want the LLM tools but not the plugin's profile injection (e.g. you already have a custom system prompt strategy), call the factories directly. All three share the same CreateMemoryToolsArgs:
import {
createMemoryReadTools,
createMemoryWriteTools,
createMemoryTools, // convenience: all 12
} from 'oneringai';
const readTools = createMemoryReadTools({
memory, // MemorySystem instance
agentId: 'my-agent',
defaultUserId: 'alice', // fallback when ToolContext.userId is unset
defaultGroupId: 'team-A', // TRUSTED — from your auth layer
defaultPrincipals: ['user:alice', 'group:team-A', 'world'],
// Optional: wire "me" / "this_agent" token resolution via your own bootstrap.
getOwnSubjectIds: () => ({ userEntityId, agentEntityId }),
autoResolveThreshold: 0.9,
});
agent.tools.registerMany(readTools);
// Or full read+write:
const writeTools = createMemoryWriteTools({ memory, agentId: 'my-agent', defaultUserId: 'alice' });
agent.tools.registerMany(writeTools);
Without getOwnSubjectIds, the "me" / "this_agent" SubjectRef tokens return a structured error; callers must reference entities by id, identifier, or surface.
Direct MemorySystem access
For server-side code (jobs, migrations, CLI tooling) you can bypass the agent layer entirely and use the MemorySystem API directly:
const scope = { userId: 'alice', groupId: 'team-A' };
// Upsert an entity
const { entity: john } = await memory.upsertEntity({
type: 'person',
displayName: 'John Doe',
identifiers: [{ kind: 'email', value: 'john@acme.com' }],
}, scope);
// Add a fact
await memory.addFact({
subjectId: john.id,
predicate: 'works_at',
kind: 'atomic',
objectId: acmeId,
importance: 0.8,
}, scope);
// Brain-like retrieval — profile + top facts + related tasks + related events
const view = await memory.getContext(john.id, {}, scope);
// Surface-form lookup (for callers that don't know the id yet)
const candidates = await memory.resolveEntity(
{ surface: 'John from Acme', type: 'person' },
scope,
);
// Graph traversal
const neighborhood = await memory.traverse(john.id, {
direction: 'out',
maxDepth: 2,
}, scope);
// Semantic search (requires embedder)
const hits = await memory.semanticSearch('budget concerns', {}, scope);
// Bitemporal — what did we know about this entity on a specific date?
const past = await memory.getContext(john.id, { asOf: new Date('2026-01-15') }, scope);
Full API reference: docs/MEMORY_API.md.