Documentation/Self-Learning Memory (NextGen Plugin)
v2.0.0-beta.1Snapshot bebdcd1Markdown ↗

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:

What it is

Two first-class concepts:

  • Entities are pure identity. People, organizations, projects, tasks, events, topics — each carries a displayName, aliases, strong identifiers (email, domain, slack_id, github, custom canonical id), and type-specific metadata (e.g. task state / dueAt, event startTime / attendeeIds).
  • Facts are knowledge. Triples like (John, works_at, Microsoft) or document facts (long-form prose). Facts carry confidence, importance, sourceSignalId, temporal validity (validFrom / validUntil), supersession links, and can bind to additional entities via contextIds ("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:

  1. The user tells the agent something ("I prefer concise answers"), or a background ingestor extracts facts from the conversation.
  2. A fact lands in the store via memory_remember (LLM-driven) or SessionIngestorPluginNextGen (passive).
  3. 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.
  4. 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)

typescript
// 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.memory and context.features.memoryWrite flags are independent. memoryWrite: true requires memory: true. Enable just memory for a retrieval-only agent (and pair with SessionIngestorPluginNextGen to 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.
typescript
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, and ensureNormalizedNameUniqueIndex(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):

text
## 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 }). The agent entity still exists for graph queries and as the this_agent subject for memory_set_agent_rule, but its profile is not synthesised or injected.

Plugin config

typescript
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:

typescript
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:

json
{"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_entity task 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.

typescript
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:

typescript
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:

typescript
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 / groupId are ignored for the access decision — encode them as tokens instead.
  • The host owns token-set correctness (the library trusts it like scope.userId): include PRINCIPAL_WORLD to see public records and the caller's own user:<id> for owner access (both fail closed if omitted), and include only group: 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):

typescript
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 a scope.principals caller. Before any host code starts passing scope.principals, run the idempotent backfill to completion:

typescript
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:

  • userId and groupId come from plugin config, NEVER from tool arguments. Tools silently ignore any groupId passed by the LLM. (Otherwise a user in group A could ask the agent to call memory_remember({..., groupId: "B"}) and escalate.)
  • No ghost-writes. memory_remember and memory_link reject writes whose subject is owned by another user — the memory layer enforces fact.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 should memory_upsert_entity its own entity instead.
  • contextIds fail-safe restriction. A write specifying contextIds that 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.
  • kind is a strict enum. Every fact is 'atomic' or 'document'. Anything else is rejected at both the tool and MemorySystem.addFact boundary.
  • value and objectId are 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:

typescript
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:

typescript
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.


Documentation snapshot · 2026-11-02Suggest a correction ↗

Search documentation

Type to search the v2 documentation.