Documentation/Memory Layer — API Reference
v2.0.0-beta.1Snapshot bebdcd1Markdown ↗

Memory Layer — API Reference

Detailed API reference for src/memory/ (v2).

All supported public symbols are exported from oneringai. There is currently no ./memory package subpath, so do not import internal src/memory/* files. The memory core remains architecturally self-contained; connector-backed helpers live in its integration layer but are re-exported at the package root.


Table of Contents

  1. Core Types
  2. MemorySystem
  3. Storage Adapters
  4. Entity Resolution
  5. Integration Layer
  6. Extraction Helpers
  7. Ranking
  8. Predicate Registry
  9. Access Control
  10. Events
  11. Errors
  12. Type Reference

Core Types

IEntity

ts
interface IEntity extends ScopeFields {
  id: EntityId;                 // opaque string; typically "ent_<uuid>"
  type: string;                 // open string; see well-known types below
  displayName: string;          // human-readable primary name
  aliases?: string[];           // alternate display forms (NOT lookup keys)
  identifiers: Identifier[];    // lookup keys (uniqueness-bearing)
  metadata?: Record<string, unknown>;  // type-specific fields
  archived?: boolean;
  identityEmbedding?: number[]; // for semantic entity resolution (populated async)
  version: number;              // optimistic concurrency token
  createdAt: Date;
  updatedAt: Date;
  groupId?: string;             // from ScopeFields — group-scoped entity
  ownerId?: string;             // from ScopeFields — user-scoped entity
}

Well-known type conventions:

Type Required identifier kinds Conventional metadata
person any of: email, slack_id, phone, github —
organization any of: domain, legal_name, ticker, duns —
project (group-scoped slug, optional) status, stakeholderIds
task external_id optional state, dueAt, priority, assigneeId, reporterId, projectId, completedAt
event external_id optional startTime, endTime, location, kind, attendeeIds, hostId
topic — —
cluster — anchorEntityIds, firstSeen, lastSeen

type is not an enum — you can add your own types. The memory layer's defaults (for getContext tiers like relatedTasks) look for type === 'task' and type === 'event' specifically.

IFact

ts
interface IFact extends ScopeFields {
  id: FactId;
  subjectId: EntityId;
  predicate: string;            // e.g. 'works_at', 'committed_to', 'profile'
  kind: 'atomic' | 'document';

  // Payload — atomic uses (objectId XOR value), document uses details.
  objectId?: EntityId;
  value?: unknown;
  details?: string;

  // Retrieval
  summaryForEmbedding?: string; // document kind: short gist, embedded in place of details
  embedding?: number[];         // populated async by the embedding queue
  isSemantic?: boolean;         // computed at write time; gates embedding eligibility

  // Quality + provenance
  confidence?: number;          // 0..1; default 1.0 for ranking
  sourceSignalId?: string;      // opaque signal reference (memory layer doesn't interpret)
  derivedBy?: string;           // rule-engine rule id if inferred

  // Salience
  importance?: number;          // 0..1; default 0.5. Ranking multiplier.

  // Multi-entity binding
  contextIds?: EntityId[];      // entities this fact is "about" beyond subject/object

  // Lifecycle
  supersedes?: FactId;          // predecessor id (archived when this fact is written)
  archived?: boolean;
  isAggregate?: boolean;        // $inc counters; never superseded

  // Temporal
  observedAt?: Date;
  validFrom?: Date;
  validUntil?: Date;

  metadata?: Record<string, unknown>;
  createdAt: Date;
  groupId?: string;
  ownerId?: string;
}

Writing rules:

  • Atomic fact: set exactly one of objectId (relational) or value (attribute). details is optional narrative.
  • Document fact: set details (long-form markdown). Leave objectId/value unset.
  • Canonical profile: predicate: 'profile', kind: 'document'. MemorySystem.getContext returns this automatically.
  • Supersession: set supersedes to the prior fact's id. MemorySystem.addFact writes the new fact first, then archives the predecessor (crash-safe ordering).
  • Signal provenance: always set sourceSignalId when the fact came from a signal. Opaque string; library users own the signal store.

Identifier

ts
interface Identifier {
  kind: string;           // 'email' | 'slack_id' | 'phone' | 'domain' | 'github' | 'legal_name' | 'ticker' | ...
  value: string;          // the id itself; stored lowercase internally
  isPrimary?: boolean;    // optional display hint
  verified?: boolean;
  source?: string;        // which signal/source added it
  addedAt?: Date;
}

Identifiers are stored lowercase internally for case-insensitive matching. Mixed-case inputs ("John@ACME.com") resolve correctly.

Scope

ts
interface ScopeFields {
  groupId?: string;
  ownerId?: string;
}
  • (none, none) — global — visible to all callers
  • (groupId, none) — group-wide — visible to members of the group
  • (none, ownerId) — user-private across all groups
  • (groupId, ownerId) — user-private within a group
ts
interface ScopeFilter {
  groupId?: string;   // caller's group
  userId?: string;    // caller's user id
}

Visibility rule:

A record is visible iff:

  • !record.groupId || record.groupId === filter.groupId, AND
  • !record.ownerId || record.ownerId === filter.userId.

Scope invariant (enforced on fact writes):

A fact's scope constraints must be a superset of its subject entity's scope:

  • If entity.groupId set → fact.groupId must equal it.
  • If entity.ownerId set → fact.ownerId must equal it.
  • Facts may narrow further (add ownerId when entity has only groupId).

Violations throw ScopeInvariantError.


MemorySystem

The facade. All business logic lives here.

Construction

ts
class MemorySystem implements IDisposable {
  constructor(config: MemorySystemConfig);
}

interface MemorySystemConfig {
  store: IMemoryStore;                         // required
  embedder?: IEmbedder;                         // optional; enables semantic search + identity embedding
  profileGenerator?: IProfileGenerator;         // optional; enables regenerateProfile + auto-regen
  ruleEngine?: IRuleEngine;                     // optional; enables deriveFactsFor
  profileRegenerationThreshold?: number;        // default 3 new atomic facts
  topFactsRanking?: RankingConfig;              // default half-life 90d, min confidence 0.2
  embeddingQueue?: EmbeddingQueueConfig;        // default concurrency 4, retries 3
  entityResolution?: EntityResolutionConfig;    // default threshold 0.90, minFuzzy 0.85, identityEmbedding enabled
  predicates?: PredicateRegistry;               // optional; canonicalization + defaults + auto-supersede (see below)
  predicateMode?: 'permissive' | 'strict';      // default 'permissive'; strict rejects unknowns
  predicateAutoSupersede?: boolean;             // default true; controls singleValued auto-supersede
  onChange?: (event: ChangeEvent) => void;      // fire-and-forget event hook
}

Entity operations

upsertEntity

Create or merge-identifiers-into an entity, keyed on matching identifiers.

ts
upsertEntity(
  input: Partial<IEntity> & {
    identifiers: Identifier[];   // may be empty array for projects/topics without external keys
    displayName: string;
    type: string;
  },
  scope: ScopeFilter,
): Promise<UpsertEntityResult>;

interface UpsertEntityResult {
  entity: IEntity;
  created: boolean;
  mergedIdentifiers: number;    // count of new identifiers added to existing entity
  mergeCandidates: EntityId[];  // other entities that matched by some identifiers
}

Dedup logic:

  1. For each identifier, call findEntitiesByIdentifier(kind, value, scope).
  2. Zero matches → create new entity (version 1).
  3. One match → merge new identifiers/aliases; bump version.
  4. Multi distinct matches → pick the one with the most identifier hits; others reported in mergeCandidates.

Scope defaults: if input.groupId/input.ownerId absent, defaults to scope.groupId/scope.userId (caller's context).

upsertEntityBySurface

Entry point for LLM extraction pipelines. Translates a surface form (e.g. "Microsoft") to an entity, creating or resolving.

ts
upsertEntityBySurface(
  input: UpsertBySurfaceInput,
  scope: ScopeFilter,
  opts?: UpsertBySurfaceOptions,
): Promise<UpsertBySurfaceResult>;

interface UpsertBySurfaceInput {
  surface: string;                   // raw text as extracted by LLM
  type: string;
  identifiers?: Identifier[];
  aliases?: string[];                // alternate forms spotted nearby
  contextEntityIds?: EntityId[];     // siblings in the extraction — for disambiguation
}

interface UpsertBySurfaceOptions {
  autoResolveThreshold?: number;     // override default (0.90)
}

interface UpsertBySurfaceResult {
  entity: IEntity;
  resolved: boolean;                 // true = matched existing, false = created new
  mergeCandidates: EntityCandidate[]; // near-matches for human review
}

Flow:

  1. Call resolveEntity with the same query.
  2. If top candidate confidence ≥ threshold, append surface + new identifiers as aliases on the matched entity (alias accumulation). Return.
  3. Otherwise create a new entity with the surface as displayName. Return near-matches as mergeCandidates.

Never auto-merges. mergeCandidates is advisory — use MemorySystem.mergeEntities to actually merge after human review.

resolveEntity

Return ranked candidate entities for a surface form.

ts
resolveEntity(
  query: ResolveEntityQuery,
  scope: ScopeFilter,
  opts?: ResolveEntityOptions,
): Promise<EntityCandidate[]>;

interface ResolveEntityQuery {
  surface: string;
  type?: string;
  identifiers?: Identifier[];
  contextEntityIds?: EntityId[];
}

interface ResolveEntityOptions {
  limit?: number;        // default 5
  threshold?: number;    // default 0.5 (inclusive minimum confidence)
}

interface EntityCandidate {
  entity: IEntity;
  confidence: number;    // 0..1
  matchedOn: 'identifier' | 'displayName' | 'alias' | 'fuzzy' | 'embedding';
}

Matching tiers:

Tier Match Confidence
1 Identifier (kind+value) 1.0
2 Exact displayName (after normalization — case/punctuation/suffix insensitive) 0.9
3 Exact alias 0.85
4 Fuzzy (normalized Levenshtein ≥ minFuzzyRatio) 0.6–0.84
5 Semantic (identityEmbedding cosine) 0.4–0.8

Context-aware disambiguation: when multiple candidates pass threshold and none is a perfect identifier match, candidates that share facts with entities in contextEntityIds get a confidence boost (+0.05 per overlap, capped at 1.0).

getEntity

ts
getEntity(id: EntityId, scope: ScopeFilter): Promise<IEntity | null>;

Returns null if entity is archived or not visible to caller.

searchEntities

Substring search over displayName, aliases, and identifier values.

ts
searchEntities(
  query: string,
  opts: { types?: string[]; limit?: number; cursor?: string },
  scope: ScopeFilter,
): Promise<Page<IEntity>>;

mergeEntities

Merge two entities. Winner keeps all of loser's identifiers + aliases. All facts where loser is subject/object are rewritten to winner. Loser is archived.

ts
mergeEntities(winnerId: EntityId, loserId: EntityId, scope: ScopeFilter): Promise<IEntity>;

Scope-window limitation (intentional, defence-in-depth): the fact-rewrite step only touches facts visible to the caller. Facts scoped more narrowly (e.g. other users' private facts on either entity) are left pointing at the archived loser. A "complete" merge requires a caller with broad-enough scope.

Emits entity.merge event.

archiveEntity

Soft-delete. Cascades to facts: all facts where this entity appears as subject or object are also archived.

ts
archiveEntity(id: EntityId, scope: ScopeFilter): Promise<void>;

Emits entity.archive event.

deleteEntity

ts
deleteEntity(id: EntityId, scope: ScopeFilter, opts?: { hard?: boolean }): Promise<void>;
  • Default (hard: false): same as archiveEntity — soft-delete, facts archived.
  • hard: true: permanent delete of the entity; referencing facts are archived (not deleted).

updateEntityMetadata

Shallow-merge a patch into entity.metadata. Version-bumping, scope-checked, emits event.

ts
updateEntityMetadata(
  id: EntityId,
  patch: Record<string, unknown>,
  scope: ScopeFilter,
): Promise<IEntity>;

Use this for task state changes, event metadata updates, etc. Does NOT re-queue identity embedding (metadata doesn't affect identity string).

ts
// Mark a task as done
await memory.updateEntityMetadata(taskId, { state: 'done', completedAt: new Date() }, scope);

Fact operations

addFact

Write a new fact. Enforces scope invariants, visibility checks on objectId + contextIds, supersession, embedding-queue enqueue, and profile auto-regen threshold.

ts
addFact(
  input: Partial<IFact> & {
    subjectId: EntityId;
    predicate: string;
    kind: FactKind;
  },
  scope: ScopeFilter,
): Promise<IFact>;

Validation:

  • Subject entity must be visible to caller.
  • If objectId set, object entity must be visible to caller.
  • If contextIds set, every entity in the list must be visible to caller.
  • Scope invariant against subject entity.

Auto-computed fields (if absent):

  • id: fact_<uuid>
  • createdAt: now
  • observedAt: now
  • isSemantic: defaults to true; isSemantic: false is the explicit opt-out.
  • groupId: input → subject entity → scope. ownerId: input → caller scope.userId → subject owner (system fallback).

Supersession: if supersedes set, the new fact is written first, then the predecessor is archived (crash-safe ordering).

Embedding queue: if isSemantic !== false and an embedder is configured, the fact composer runs; non-empty changed output is enqueued asynchronously. Writes return immediately.

Profile auto-regen: if kind === 'atomic' && profileGenerator configured, checks threshold and fires background regen if met.

Emits fact.add (and fact.supersede when supersession occurs).

addFacts

ts
addFacts(inputs: Array<...>, scope: ScopeFilter): Promise<IFact[]>;

Serial batch — each fact passes through full validation. Preserves order.

supersedeFact

Convenience wrapper around addFact with supersedes pre-filled.

ts
supersedeFact(
  oldId: FactId,
  newInput: Partial<IFact> & { predicate: string; kind: FactKind; subjectId: EntityId },
  scope: ScopeFilter,
): Promise<IFact>;

archiveFact

ts
archiveFact(id: FactId, scope: ScopeFilter): Promise<void>;

Emits fact.archive.

Retrieval

getContext

Primary retrieval API. Returns a rich view of an entity: profile + top ranked facts + optional tiers (documents, semantic, neighbors, related tasks, related events).

ts
getContext(
  entityId: EntityId,
  opts: ContextOptions,
  scope: ScopeFilter,
): Promise<EntityView>;

interface ContextOptions {
  topFactsLimit?: number;              // default 15
  include?: ('documents' | 'semantic' | 'neighbors')[];
  tiers?: 'full' | 'minimal';          // default 'full' → includes relatedTasks + relatedEvents
  documentPredicates?: string[];       // filter document tier
  semanticQuery?: string;              // required for semantic tier
  semanticTopK?: number;               // default 5
  neighborPredicates?: string[];
  neighborDepth?: number;              // default 1
  asOf?: Date;                         // temporal view
  relatedTasksLimit?: number;          // default 15
  relatedEventsLimit?: number;         // default 15
  recentEventsWindowDays?: number;     // default 90
}

interface EntityView {
  entity: IEntity;
  profile: IFact | null;               // most-specific visible profile; null if none
  topFacts: IFact[];                   // subject OR object OR contextIds, ranked
  relatedTasks?: RelatedTask[];        // default-on (unless tiers:'minimal')
  relatedEvents?: RelatedEvent[];      // default-on (unless tiers:'minimal')
  documents?: IFact[];                 // if include: ['documents']
  semantic?: Array<{ fact: IFact; score: number }>;  // if include: ['semantic']
  neighbors?: Neighborhood;            // if include: ['neighbors']
}

interface RelatedTask { task: IEntity; role: string; }  // role: 'assigned_to' | 'reporter_of' | 'project_of' | 'context_of'
interface RelatedEvent { event: IEntity; role: string; when?: Date; }

Top facts query: uses touchesEntity filter — returns facts where the entity is subject OR object OR in contextIds. Then ranked by scoreFact (see Ranking) and truncated to topFactsLimit.

Profile resolution precedence: ownerId match > groupId match > global. Most-specific visible.

Related tasks: queries task entities (type: 'task') where:

  • metadata.assigneeId === entityId (role: assigned_to)
  • metadata.reporterId === entityId (role: reporter_of)
  • metadata.projectId === entityId (role: project_of)
  • entity appears in contextIds of a fact whose subject/object is a task (role: context_of) Filtered to non-terminal states (pending, in_progress, blocked, deferred).

Related events: event entities in a recent window (recentEventsWindowDays) where entity is in metadata.attendeeIds or metadata.hostId, plus events reached via contextIds on facts about the subject.

Throws if the entity is not visible / archived / missing.

getProfile

ts
getProfile(entityId: EntityId, scope: ScopeFilter): Promise<IFact | null>;

Returns the most-specific visible profile document fact. Scope resolution order: ownerId match > groupId match > global.

traverse

Graph traversal. Uses native store.traverse if present, else falls back to genericTraverse (BFS over findFacts).

ts
traverse(
  entityId: EntityId,
  opts: TraversalOptions,
  scope: ScopeFilter,
): Promise<Neighborhood>;

interface TraversalOptions {
  predicates?: string[];        // filter edge predicates
  direction: 'out' | 'in' | 'both';
  maxDepth: number;             // required hard bound
  limit?: number;               // max nodes returned
  asOf?: Date;
}

interface Neighborhood {
  nodes: Array<{ entity: IEntity; depth: number }>;
  edges: Array<{ fact: IFact; from: EntityId; to: EntityId; depth: number }>;
}

semanticSearch

Vector search over facts. Requires embedder + store.semanticSearch capability.

ts
semanticSearch(
  query: string,
  filter: FactFilter,
  scope: ScopeFilter,
  topK?: number,    // default 5
): Promise<Array<{ fact: IFact; score: number }>>;

Throws SemanticSearchUnavailableError if either is missing.

Profile lifecycle

regenerateProfile

ts
regenerateProfile(
  entityId: EntityId,
  targetScope: ScopeFields,
  trigger?: 'threshold' | 'manual',
): Promise<IFact>;

Runs the profile generator against the entity's atomic facts visible at the target scope, writes the resulting document fact with predicate: 'profile', supersedes the prior profile at the same scope if present.

For current writes, pass targetScope.ownerId (optionally with groupId). The owner invariant prevents creation of new ownerless global/group profiles; ownerless variants are legacy-data fallbacks only.

Auto-triggers: addFact schedules this in the background (debounced per (entityId, scope) via an in-flight set) when the fact count for the entity reaches profileRegenerationThreshold.

Throws ProfileGeneratorMissingError if no generator is configured.

Emits profile.regenerate.

Rule engine

deriveFactsFor

ts
deriveFactsFor(entityId: EntityId, scope: ScopeFilter): Promise<IFact[]>;

Invokes the configured rule engine with a read-only IScopedMemoryView. Returns the derived facts after writing them through addFact. Returns [] if no engine is configured.

Sandboxing: the view exposes only getEntity and findFacts — rules cannot write. Derived fact specs go through the same validation pipeline as caller-initiated writes.

Embedding queue control

ts
flushEmbeddings(): Promise<void>;     // await all pending embedding jobs
pendingEmbeddings(): number;           // count of queued + in-flight

Lifecycle

ts
destroy(): void;                 // sync, idempotent; stops queue, rejects further ops
shutdown(): Promise<void>;       // flushes queue, then destroys; calls store.shutdown if present
isDestroyed: boolean;

Storage Adapters

IMemoryStore contract

Every adapter must implement this:

ts
// Input types — adapter assigns id + version + timestamps on create.
type NewEntity = Omit<IEntity, 'id' | 'version' | 'createdAt' | 'updatedAt'>;
type NewFact   = Omit<IFact,   'id' | 'createdAt'>;

interface IMemoryStore {
  // ===== Entities (required) =====
  createEntity(input: NewEntity): Promise<IEntity>;                      // returns with id populated
  createEntities(inputs: NewEntity[]): Promise<IEntity[]>;
  updateEntity(entity: IEntity): Promise<void>;                          // version must be stored+1
  getEntity(id: EntityId, scope: ScopeFilter): Promise<IEntity | null>;
  findEntitiesByIdentifier(kind: string, value: string, scope: ScopeFilter): Promise<IEntity[]>;
  searchEntities(query: string, opts: EntitySearchOptions, scope: ScopeFilter): Promise<Page<IEntity>>;
  listEntities(filter: EntityListFilter, opts: ListOptions, scope: ScopeFilter): Promise<Page<IEntity>>;
  archiveEntity(id: EntityId, scope: ScopeFilter): Promise<void>;
  deleteEntity(id: EntityId, scope: ScopeFilter): Promise<void>;

  // ===== Facts (required) =====
  createFact(input: NewFact): Promise<IFact>;                            // returns with id populated
  createFacts(inputs: NewFact[]): Promise<IFact[]>;
  getFact(id: FactId, scope: ScopeFilter): Promise<IFact | null>;
  findFacts(filter: FactFilter, opts: FactQueryOptions, scope: ScopeFilter): Promise<Page<IFact>>;
  updateFact(id: FactId, patch: Partial<IFact>, scope: ScopeFilter): Promise<void>;
  countFacts(filter: FactFilter, scope: ScopeFilter): Promise<number>;

  // ===== Graph (optional) =====
  traverse?(startId: EntityId, opts: TraversalOptions, scope: ScopeFilter): Promise<Neighborhood>;

  // ===== Vector (optional) =====
  semanticSearch?(
    queryVector: number[],
    filter: FactFilter,
    opts: SemanticSearchOptions,
    scope: ScopeFilter,
  ): Promise<Array<{ fact: IFact; score: number }>>;

  // ===== Lifecycle =====
  destroy(): void;
  shutdown?(): Promise<void>;
}

Adapter responsibilities:

  1. Apply ScopeFilter to every read (defence-in-depth; MemorySystem also filters).
  2. Assign primary ids on createEntity / createFact. Native mechanisms preferred (Mongo ObjectId → hex string; Meteor Random.id(); UUID for in-memory).
  3. Enforce optimistic concurrency on updateEntity: reject if incoming version !== stored.version + 1.
  4. Hide archived records by default; show only when filter.archived === true.
  5. Support asOf on fact queries (validFrom ≤ asOf ≤ validUntil ?? ∞ AND createdAt ≤ asOf).
  6. Support metadataFilter on listEntities — equality + { $in: [...] } at minimum.
  7. Support contextId and touchesEntity filters on findFacts.

InMemoryAdapter

ts
class InMemoryAdapter implements IMemoryStore {
  constructor(opts?: { entities?: IEntity[]; facts?: IFact[] });
}

Zero-dep default. Good for tests, single-process dev, and small deployments. Maintains:

  • Primary id indexes
  • Identifier lookup index (kind, value-lowercase) → entity ids
  • Fact indexes: bySubject, byObject, byContext
  • Scope filtering inline on every read

MongoMemoryAdapter

ts
class MongoMemoryAdapter implements IMemoryStore {
  constructor(opts: MongoMemoryAdapterOptions);
}

interface MongoMemoryAdapterOptions {
  entities: IMongoCollectionLike<IEntity>;
  facts: IMongoCollectionLike<IFact>;
  useNativeGraphLookup?: boolean;             // default false; requires facts.aggregate
  vectorIndexName?: string;                   // Atlas Vector Search index name
  vectorCandidateMultiplier?: number;         // default 10
  factsCollectionName?: string;               // required for useNativeGraphLookup
  defaultPageSize?: number;                   // default 100
}

Works identically with the raw mongodb driver (RawMongoCollection) and Meteor (MeteorMongoCollection). See Collection wrappers.

Collection wrappers

The adapter depends only on IMongoCollectionLike — a narrow structural contract. Two built-in wrappers, plus users can implement their own.

IMongoCollectionLike

ts
interface IMongoCollectionLike<T extends { id: string }> {
  // Writes
  insertOne(doc: T): Promise<void>;
  insertMany(docs: T[]): Promise<void>;
  updateOne(filter, update, opts?): Promise<MongoUpdateResult>;
  deleteOne(filter): Promise<void>;
  deleteMany(filter): Promise<void>;
  bulkWrite?(ops): Promise<void>;

  // Reads
  findOne(filter, opts?): Promise<T | null>;
  find(filter, opts?): Promise<T[]>;
  countDocuments(filter): Promise<number>;
  aggregate?(pipeline): Promise<unknown[]>;

  // Index management
  createIndex?(spec, opts?): Promise<void>;

  // Transactions
  withTransaction?<R>(fn: () => Promise<R>): Promise<R>;
}

RawMongoCollection

Wraps a mongodb-driver Collection. Supports all optional capabilities. Accepts an optional MongoClient for transactions.

ts
class RawMongoCollection<T> implements IMongoCollectionLike<T> {
  constructor(col: RawMongoDriverCollection<T>, client?: RawMongoClientLike);
}

MeteorMongoCollection

Wraps a Meteor Mongo.Collection. Writes flow through Meteor's async API (insertAsync, updateAsync, removeAsync) — preserving reactive publications. Complex reads (aggregate) fall back to rawCollection().

ts
class MeteorMongoCollection<T> implements IMongoCollectionLike<T> {
  constructor(col: MeteorCollectionLike<T>);
}

ensureIndexes

One-time index setup helper.

ts
ensureIndexes(args: { entities; facts }): Promise<void>;

Creates:

  • Entities: identifier lookup, list-by-type, primary-key, task metadata, event metadata.
  • Facts: by-subject (with observedAt sort), by-object, by-context, recent-by-predicate, primary-key.

No-op if the collection doesn't implement createIndex.

Writing your own adapter

Minimum required operations:

  • Entity CRUD + optimistic concurrency on updateEntity; adapter-assigned ids on createEntity
  • Identifier lookup
  • Fact CRUD + all filter options (subjectId, objectId, contextId, touchesEntity, predicate, kind, archived, asOf, minConfidence, temporal)
  • Scope filtering on every read
  • metadataFilter on listEntities
  • searchEntities (case-insensitive substring on displayName, aliases, identifier values)

Optional capabilities (detected by MemorySystem via duck-typing):

  • traverse — otherwise generic BFS is used
  • semanticSearch — otherwise semantic tier is unavailable

See src/memory/adapters/inmemory/InMemoryAdapter.ts as the reference implementation.


Entity Resolution

EntityResolver

Usually called via MemorySystem.resolveEntity / upsertEntityBySurface. Direct use is possible for custom orchestration.

ts
class EntityResolver {
  constructor(hooks: ResolverMemoryHooks, config?: EntityResolutionConfig);
  resolve(query, scope, opts?): Promise<EntityCandidate[]>;
  upsertBySurface(input, scope, opts?): Promise<UpsertBySurfaceResult>;
}

interface EntityResolutionConfig {
  autoResolveThreshold?: number;       // default 0.90 (conservative)
  minFuzzyRatio?: number;              // default 0.85
  enableIdentityEmbedding?: boolean;   // default true
}

fuzzy helpers

ts
function normalizedLevenshteinRatio(a: string, b: string): number;  // 0..1
function normalizeSurface(s: string): string;

Normalization:

  • lowercase
  • strip non-alphanumeric (except whitespace)
  • strip corporate suffixes (Inc, Corp, LLC, Ltd, Limited, GmbH, S.A., PLC, Corporation)
  • collapse whitespace

So "Microsoft Inc." and "MICROSOFT" normalize to "microsoft".

buildIdentityString

ts
function buildIdentityString(args: {
  type: string;
  displayName: string;
  aliases: string[];
  identifiers: Identifier[];
}): string;

Produces the short string that's embedded for IEntity.identityEmbedding. Format: "<type>: <displayName> | aliases: <top 3> | ids: <up to 3 primary+secondary>".


Integration Layer

Helpers that bridge oneringai Connectors into the memory layer's IEmbedder / IProfileGenerator. Optional — you can pass your own impls directly.

ConnectorEmbedder

Adapts IEmbeddingProvider (wired via a named Connector) to IEmbedder.

ts
class ConnectorEmbedder implements IEmbedder {
  constructor(config: ConnectorEmbedderConfig);
  static withProvider(args: { provider; model; dimensions; requestedDimensions? }): ConnectorEmbedder;
  readonly dimensions: number;
  embed(text): Promise<number[]>;
  embedBatch(texts): Promise<number[][]>;
}

interface ConnectorEmbedderConfig {
  connector: string;              // registered connector name
  model: string;                  // e.g. 'text-embedding-3-small'
  dimensions: number;             // matches model output (or MRL-reduced target)
  requestedDimensions?: number;   // passed to provider for MRL models
}

Connector must be registered via Connector.create(...) before constructing.

ConnectorProfileGenerator

Adapts an LLM connector + model into IProfileGenerator via Agent.runDirect with JSON response format.

ts
class ConnectorProfileGenerator implements IProfileGenerator {
  constructor(config: ConnectorProfileGeneratorConfig);
  static withAgent(args: { agent; promptTemplate?; temperature?; maxOutputTokens? }): ConnectorProfileGenerator;
  generate(input: ProfileGeneratorInput): Promise<{ details: string; summaryForEmbedding: string }>;
  destroy(): void;
}

interface ConnectorProfileGeneratorConfig {
  connector: string;
  model: string;                                     // e.g. 'claude-sonnet-4-6'
  promptTemplate?: (ctx: ProfileGeneratorInput) => string; // override defaultProfilePrompt
  temperature?: number;                              // default 0.3
  maxOutputTokens?: number;                          // default undefined (provider ceiling)
}

Parses the response as JSON with {details, summaryForEmbedding}. Falls back gracefully (using the raw output as details) if parsing fails.

createMemorySystemWithConnectors

One-call factory.

ts
function createMemorySystemWithConnectors(
  config: MemorySystemWithConnectorsConfig,
): MemorySystem;

type MemorySystemWithConnectorsConfig = Omit<MemorySystemConfig, 'embedder' | 'profileGenerator'> & {
  connectors?: {
    embedding?: ConnectorEmbedderConfig;
    profile?: ConnectorProfileGeneratorConfig;
  };
};

defaultProfilePrompt

Default prompt for profile regeneration. Takes entity + atomic facts + prior profile + target scope, returns structured JSON. Override via ConnectorProfileGeneratorConfig.promptTemplate.

ts
function defaultProfilePrompt(ctx: ProfileGeneratorInput): string;

Extraction Helpers

For the "raw signal → structured memory" pipeline.

defaultExtractionPrompt

ts
function defaultExtractionPrompt(ctx: ExtractionPromptContext): string;

interface ExtractionPromptContext {
  signalText?: string;
  signalThread?: SignalThreadMessage[];
  signalSourceDescription?: string;       // e.g. "email from john@acme.com"
  targetScope?: ScopeFields;
  knownEntities?: IEntity[];              // pre-loaded candidates to hint at
  referenceDate?: Date;                   // for relative dates like "next Friday"
  predicateRegistry?: PredicateRegistry;
  maxPredicatesPerCategory?: number;
  preResolvedBindings?: PreResolvedBinding[];
  extractableEntityTypes?: readonly string[];
  priorFacts?: IFact[];                   // enables reconciliation operations
  anchors?: Anchor[];
  eagerness?: EagernessProfile;
  anchorContextDescription?: string;
  negativeExamples?: Array<{ snippet: string; reason?: string }>;
  priorThreadContext?: Array<{ header: string; body: string }>;
  subjectOfHintsEnabled?: boolean;
}

Instructs the LLM to emit JSON with mentions (local label → surface + type + identifiers + aliases) and facts (triples referencing mention labels, not entity IDs).

DEFAULT_EXTRACTION_PROMPT_VERSION is currently 13; use it when pinning prompt snapshots. DEFAULT_EXTRACTABLE_ENTITY_TYPES exposes the default seven-type mention vocabulary. Every task extraction still assumes task is allowed—the built-in prompt's task guidance is intentionally structural, not a fully removable optional paragraph.

ExtractionResolver

Converts raw LLM output into resolved entities + persisted facts.

ts
class ExtractionResolver {
  constructor(memory: MemorySystem);

  resolveAndIngest(
    output: ExtractionOutput,
    sourceSignalId: string,
    scope: ScopeFilter,
    opts?: ExtractionResolverOptions,
  ): Promise<IngestionResult>;
}

interface ExtractionOutput {
  mentions: Record<string, ExtractionMention>;   // label → mention
  facts: ExtractionFactSpec[];
}

interface ExtractionMention {
  surface: string;
  type: string;
  identifiers?: Identifier[];
  aliases?: string[];
}

interface ExtractionFactSpec {
  subject: string;                         // mention label
  predicate: string;
  object?: string;                         // mention label (relational)
  value?: unknown;                         // attribute value
  details?: string;
  summaryForEmbedding?: string;
  confidence?: number;
  importance?: number;
  contextIds?: string[];                   // mention labels
  kind?: FactKind;                         // default 'atomic'
  validFrom?: string | Date;
  validUntil?: string | Date;
  observedAt?: string | Date;
}

interface IngestionResult {
  entities: Array<{
    label: string;
    entity: IEntity;
    resolved: boolean;
    mergeCandidates: EntityCandidate[];
  }>;
  facts: IFact[];
  mergeCandidates: Array<{
    label: string;
    surface: string;
    candidates: EntityCandidate[];
  }>;
  unresolved: Array<{ where: string; reason: string }>;
}

Flow:

  1. If opts.preResolved is supplied, its label → EntityId bindings are seeded into the label map up front — mentions that redeclare those labels are silently skipped (pre-resolved binding wins).
  2. For each mention: memory.upsertEntityBySurface(...). Passes already-resolved sibling labels as contextEntityIds for disambiguation.
  3. For each fact: translates subject/object/contextIds from mention labels to entity IDs; attaches sourceSignalId; calls memory.addFact.
  4. Returns per-item successes + errors.

One bad mention or fact does NOT abort the rest — errors collected in unresolved.

ts
interface ExtractionResolverOptions {
  autoResolveThreshold?: number;
  preResolved?: Record<string, EntityId>;   // label → entity id
}

Signal ingestion pipeline

Higher-level facade: turns raw emails / plain text (or any custom source) into facts, handling participant seeding and label pre-binding automatically. See MEMORY_SIGNALS.md for recipes.

ts
class SignalIngestor {
  constructor(config: SignalIngestorConfig);
  registerAdapter(adapter: SignalSourceAdapter<unknown>): this;
  hasAdapter(kind: string): boolean;
  ingest<TRaw>(input: IngestSignalInput<TRaw>): Promise<IngestionResult>;
  ingestText(input: IngestTextInput): Promise<IngestionResult>;
  ingestExtracted(input: IngestExtractedInput): Promise<IngestionResult>;
}

interface SignalSourceAdapter<TRaw = unknown> {
  readonly kind: string;
  extract(raw: TRaw): ExtractedSignal;
}

interface ExtractedSignal {
  signalText: string;
  signalSourceDescription?: string;
  participants: ParticipantSeed[];
}

interface ParticipantSeed {
  role: string;                    // 'from' | 'to' | 'cc' | 'author' | …
  identifiers: Identifier[];       // MUST be non-empty (strong only)
  displayName?: string;
  aliases?: string[];
  type?: string;                   // default 'person'
}

interface IExtractor {
  extract(prompt: string): Promise<ExtractionOutput>;
}

Ships two reference adapters:

  • PlainTextAdapter — trivial passthrough for raw text signals.
  • EmailSignalAdapter — seeds from/to/cc as person participants and (opt-in, default on) from/to/cc non-free email domains as organization participants; BCC is dropped for privacy.

Ships one reference extractor:

  • ConnectorExtractor — wraps a Connector + model; uses runDirect with responseFormat: { type: 'json_object' } and parses defensively.

Ranking

ts
function scoreFact(fact: IFact, config: RankingConfig, now: Date): number;
function rankFacts(facts: IFact[], config: RankingConfig, now: Date): IFact[];

interface RankingConfig {
  predicateWeights?: Record<string, number>;     // default 1.0
  recencyHalfLifeDays?: number;                  // default 90
  minConfidence?: number;                        // default 0.2
}

Formula: score = confidence × recencyDecay × predicateWeight × importanceMultiplier

Where:

  • recencyDecay = 0.5 ^ (ageDays / halfLifeDays) — age from observedAt (fallback createdAt).
  • importanceMultiplier = 0.3 + importance × 1.4 — default importance 0.5 → multiplier 1.0. Max (importance 1.0) → 1.7×.
  • If confidence < minConfidence, score is 0.

rankFacts returns the sorted list descending, with zero-scored facts filtered out.


Predicate Registry

See MEMORY_PREDICATES.md for usage recipes and walkthroughs. This section is the API reference.

Pluggable vocabulary for fact predicates. Optional — when no registry is configured, predicates remain free-form strings (canonicalization is a no-op). When a registry is attached, addFact gains:

  • Canonicalization. worksAt, works-at, employed_by all collapse to works_at before storage.
  • Default importance. When the caller omits importance, the registry's defaultImportance is applied (falls back to ranking default 0.5 otherwise).
  • Default isAggregate. For aggregate predicates like interaction_count.
  • Auto-supersede for singleValued predicates. Writing current_title twice for the same subject auto-archives the first. Disable globally via predicateAutoSupersede: false.
  • Ranking weights. Registry rankingWeight values fold into RankingConfig.predicateWeights. User-supplied weights win on collision.
  • LLM prompt rendering. registry.renderForPrompt() produces a markdown vocabulary block for injection into extraction prompts.

Shape

ts
interface PredicateDefinition {
  name: string;                         // canonical snake_case — the id
  description: string;
  category: string;                     // 'identity' | 'task' | 'communication' | …
  payloadKind?: 'relational' | 'attribute' | 'narrative';
  subjectTypes?: string[];
  objectTypes?: string[];
  inverse?: string;                     // e.g. reports_to ↔ manages
  aliases?: string[];                   // alternate surface forms → this
  defaultImportance?: number;           // 0..1
  rankingWeight?: number;               // default 1.0 in RankingConfig
  isAggregate?: boolean;                // updates in place; mutually exclusive with singleValued
  singleValued?: boolean;               // auto-supersede prior on new write
  examples?: string[];
}

class PredicateRegistry {
  static standard(): PredicateRegistry;                // 44-predicate starter set
  static empty(): PredicateRegistry;

  register(def: PredicateDefinition): this;
  registerAll(defs: PredicateDefinition[]): this;
  unregister(name: string): this;

  get(nameOrAlias: string): PredicateDefinition | null;
  has(nameOrAlias: string): boolean;

  canonicalize(input: string): string;

  list(filter?: { categories?: string[]; subjectType?: string }): PredicateDefinition[];
  categories(): string[];

  renderForPrompt(opts?: {
    categories?: string[];
    subjectType?: string;
    maxPerCategory?: number;                            // default 5
  }): string;

  toRankingWeights(base?: Record<string, number>): Record<string, number>;
}

Standard library

PredicateRegistry.standard() returns a fresh registry with 44 predicates across 10 categories:

Category Predicates
identity works_at, reports_to, current_title, current_role, located_in, is_member_of, founded
organizational part_of, subsidiary_of, manages, owns, acquired, merged_with
task committed_to, blocked_by, depends_on, prepares_for
decision decision_made
communication emailed, called, messaged, met_with, mentioned, cc_ed, responded_to, interaction_count (aggregate)
observation observed_topic, expressed_concern, expressed_interest, acknowledged, noted
event attended, hosted
priority tracks_priority, priority_affects
document profile, biography, memo, meeting_notes, research_note, has_document
social knows, works_with, colleague_of

singleValued: current_title, current_role. isAggregate: interaction_count.

Single-entity attributes are metadata, not facts. Task state lives on task.metadata.state (set at creation, transitioned via MemorySystem.transitionTaskState). The same applies to dueAt, priority, completedAt, createdBy/createdAt, and event/task scheduling on metadata.startTime/endTime. No state_changed / has_status / current_status / completed / created / reviewed / has_due_date / has_priority / occurred_on / scheduled_for / started_on / ended_on / cancelled_due_to predicates exist. Inter-entity relationships such as prepares_for, blocked_by, and depends_on remain facts. committed_to is retained for backward-compatible queries but excluded from the default extraction prompt. See transitioning task state.

Note: profile is consumed by MemorySystem.getContext (document fact with predicate=profile is the canonical per-entity profile). Do not rename.

Usage

ts
import { MemorySystem, PredicateRegistry } from 'oneringai';

// Use the standard library as-is.
const memory = new MemorySystem({
  store,
  predicates: PredicateRegistry.standard(),
});

// Extend the standard library with your own predicates.
const custom = PredicateRegistry.standard();
custom.register({
  name: 'invested_in',
  description: 'Investor relationship.',
  category: 'task',
  rankingWeight: 1.3,
  defaultImportance: 0.9,
});
const memory2 = new MemorySystem({ store, predicates: custom });

// Build your own vocabulary from scratch.
const domain = PredicateRegistry.empty().registerAll([
  { name: 'patient_of', description: 'Patient-doctor relation.', category: 'clinical' },
  // ...
]);
const memory3 = new MemorySystem({ store, predicates: domain, predicateMode: 'strict' });

Strict vs. permissive mode

  • permissive (default): unknown predicates are accepted and canonicalized; they show up in IngestionResult.newPredicates for vocabulary-drift review.
  • strict: addFact throws when the predicate is not in the registry. Strict rejections through ExtractionResolver land in IngestionResult.unresolved AND newPredicates.

Setting predicateMode: 'strict' without a predicates registry throws at MemorySystem construction.

Auto-supersession scope isolation

Auto-supersession for singleValued predicates is scope-bounded: it only archives prior facts visible to the caller. A user-scoped write cannot implicitly archive a group-scoped prior (and vice-versa). This means a singleValued predicate can have multiple per-scope "current" values — intentional for isolation, surprising if unexpected.

Prompt rendering

ts
const prompt = defaultExtractionPrompt({
  signalText,
  predicateRegistry: PredicateRegistry.standard(),
  maxPredicatesPerCategory: 5,         // default
});

The rendered block is appended to the default extraction prompt. LLM output predicates canonicalize + dedupe at ingest time; unknowns surface in IngestionResult.newPredicates.


Access Control

See MEMORY_PERMISSIONS.md for the full model, defaults, recipes, and migration. This section is the API reference.

Three-principal access model layered on top of scope:

ts
type AccessLevel = 'none' | 'read' | 'write';  // 'write' implies 'read'
type Permission = 'read' | 'write';

interface Permissions {
  group?: AccessLevel;   // what group members (non-owner) can do
  world?: AccessLevel;   // what callers outside the group (or everyone, when groupless) can do
}

Attached to every IEntity and IFact as an optional permissions field.

Invariants

  • Owner required. Every record must have ownerId. MemorySystem.upsertEntity / addFact throw OwnerRequiredError when neither input nor scope provides one. Admin delegation is allowed — input.ownerId can be any user id; equality with scope.userId is NOT enforced.
  • Owner always wins. If record.ownerId === caller.userId, the caller has full access regardless of permissions.
  • The permissions.{group, world} block is write-once via the first-class API. MemorySystem.upsertEntity does NOT rewrite permissions on existing records; the dirty path silently preserves stored permissions. Use the store's updateEntity / updateFact as an admin escape hatch, or re-emit as the owner. (Explicit acl grants ARE mutable — via setAccess; see Principal ACLs below.) See MEMORY_PERMISSIONS.md § Changing access.
  • Profile regeneration preserves privacy. regenerateProfile inherits the prior profile's permissions when one exists (so a private profile stays private across auto-regen). First-time generation goes through visibilityPolicy, falling back to library defaults only when no policy applies.
  • Supersession chains stay per-subject. addFact with supersedes now validates that predecessor.subjectId === input.subjectId and throws otherwise — prevents retrieval corruption where "what superseded F1?" returns a fact about a different subject.

Defaults (when permissions is omitted)

ts
DEFAULT_GROUP_LEVEL = 'read'    // meaningful only when groupId is set
DEFAULT_WORLD_LEVEL = 'read'

Records are public-read / owner-write by default. Set permissions.world = 'none' for group-private access, or set both permissions.group = 'none' and permissions.world = 'none' for owner-only access. permissions.group is silently ignored when record.groupId is unset (the group principal does not exist for groupless records).

Pure evaluators

Exposed from oneringai (or src/memory/AccessControl.ts):

ts
function canAccess(
  record: { ownerId?: string; groupId?: string; permissions?: Permissions },
  caller: ScopeFilter,
  need: Permission,
): boolean;

function effectivePermissions(
  record,
): { group: AccessLevel; world: AccessLevel };   // applies defaults

function assertCanAccess(
  record: { id?: string; ... },
  caller: ScopeFilter,
  need: Permission,
  recordKind: 'entity' | 'fact',
): void;    // throws PermissionDeniedError on denial

function levelGrants(level: AccessLevel, need: Permission): boolean;

Error classes

ts
class OwnerRequiredError extends Error {
  recordKind: 'entity' | 'fact';
}

class PermissionDeniedError extends Error {
  recordId: string;
  recordKind: 'entity' | 'fact';
  operation: Permission;
}

Enforcement surface

Operation Read filter Write check
getEntity, getFact, findFacts, listEntities, etc. ✓ (adapter) —
searchEntities, findEntitiesByIdentifier, traverse ✓ (adapter) —
semanticSearch, countFacts ✓ (adapter) —
addFact (when supersedes set) — ✓
archiveFact, supersedeFact — ✓
archiveEntity, deleteEntity, mergeEntities — ✓
upsertEntity (dirty path — adding identifier to existing) — ✓

Reads that aren't visible return empty/null; writes that aren't authorized throw PermissionDeniedError.

Cascade caveat

archiveEntity, deleteEntity, and mergeEntities cascade to referencing facts, using BOTH the scope window (visibility) and the permission window (writability). Facts the caller can see but not write are skipped silently — intended (no privilege escalation via cascade), but it means partial cleanup is possible for callers without broad write access.

Principal ACLs (explicit grants)

An opt-in layer for per-identity grants beyond owner/group/world. Full model + recipes + migration: MEMORY_PERMISSIONS.md § Principal-based ACLs. API surface:

Record fields (on IEntity and IFact):

ts
acl?: ACLEntry[];           // explicit grants — caller-set
readPrincipals?:  string[]; // materialized at the storage boundary — LIBRARY-OWNED, never set by hand
writePrincipals?: string[]; // ⊆ readPrincipals

interface ACLEntry {
  principal: Principal;               // a token string (see helpers)
  actions: Array<'read' | 'write'>;   // [] grants nothing
}

Scope field (on ScopeFilter):

ts
principals?: string[];   // PRESENCE is authoritative (any length, incl. empty).
                         // Present → principal model (readPrincipals ∩ principals); userId/groupId ignored for access.
                         // Empty → authorizes nothing. Absent → legacy owner/group/world path.

MemorySystem methods:

ts
// Replace a record's explicit acl and re-materialize its token arrays. Requires write. Returns the updated record.
setAccess(kind: 'entity' | 'fact', id: string, acl: ACLEntry[], scope: ScopeFilter): Promise<IEntity | IFact>;

// THE MIGRATION GATE. Materialize read/writePrincipals on every entity + fact (live AND archived) from their
// ownerId/groupId/permissions/acl. Idempotent; { force } rewrites all; batchSize clamps to [1, 1000] (default 500).
// MUST run to completion before any caller passes scope.principals (principal callers have no legacy fallback).
backfillAccessPrincipals(
  scope: ScopeFilter,
  opts?: { batchSize?: number; force?: boolean },
): Promise<{ entitiesScanned: number; entitiesUpdated: number; factsScanned: number; factsUpdated: number }>;

mergeEntities(winner, loser, scope) additionally rewrites every entity:<loser> grant to entity:<winner> (the "account links later" substrate), via the optional store capability rewriteEntityPrincipal(from, to, scope).

The principal kit (exported from oneringai / src/access/principals.ts) — token builders, parser, and the storage-agnostic materializer, usable by hosts to authorize their OWN collections:

ts
// Token builders + parser
const PRINCIPAL_WORLD: 'world';
function principalUser(id): Principal;     function principalEntity(id): Principal;
function principalGroup(id): Principal;    function principalService(name): Principal;
function parsePrincipal(p): { kind: 'user'|'entity'|'group'|'service'|'world'|'unknown'; id?: string };

// Projection + materialization
function materializePrincipals(input: AccessInput): { readPrincipals: string[]; writePrincipals: string[] };
function fromLibraryPermissions(perms, ownerId, groupId): AccessInput;   // {group, world} defaults
function fromNimbleAudit(isPublic, ownerId, groupId, acl?): AccessInput; // {isPublic} host shape
function principalsForLibraryRecord(rec): { readPrincipals: string[]; writePrincipals: string[] };

// Filters + checks + merge helpers
function readFilterForPrincipals(p: string[]):  { readPrincipals:  { $in: string[] } };
function writeFilterForPrincipals(p: string[]): { writePrincipals: { $in: string[] } };
function canByPrincipals(record, principals: string[], need: 'read' | 'write'): boolean;
function patchTouchesAccessFields(patch): boolean;   // gate re-materialization on partial updates
function rewritePrincipalReferences(arr, fromEntityId, toEntityId): string[] | undefined;
function rewriteAclPrincipalReferences(acl, fromEntityId, toEntityId): ACLEntry[] | undefined;

// Types: Principal, PrincipalKind, ParsedPrincipal, ACLEntry, AccessInput, MaterializedPrincipals

Events

Optional onChange callback fires on every state-changing operation. Fire-and-forget — listener errors never impact the data path.

ts
type ChangeEvent =
  | { type: 'entity.upsert'; entity: IEntity; created: boolean }
  | { type: 'entity.archive'; entityId: EntityId }
  | { type: 'entity.merge'; winnerId: EntityId; loserId: EntityId }
  | { type: 'fact.add'; fact: IFact }
  | { type: 'fact.archive'; factId: FactId }
  | { type: 'fact.supersede'; oldId: FactId; newId: FactId }
  | { type: 'profile.regenerate'; entityId: EntityId; scope: ScopeFields; factId: FactId };

Errors

Exported error classes:

Class From Meaning
ScopeInvariantError MemorySystem Fact scope is broader than subject entity's scope
SemanticSearchUnavailableError MemorySystem No embedder or no store.semanticSearch
ProfileGeneratorMissingError MemorySystem regenerateProfile called without a generator
OptimisticConcurrencyError InMemoryAdapter Version mismatch on updateEntity
ScopeViolationError InMemoryAdapter Write target not visible to caller
MongoOptimisticConcurrencyError MongoMemoryAdapter Version mismatch; duplicate key on insert

Best practice: callers catch MongoOptimisticConcurrencyError / OptimisticConcurrencyError and retry with a fresh read of the entity.


Type Reference

Full list of exported types:

Entities + facts

  • EntityId, FactId, FactKind
  • IEntity, IFact, Identifier
  • NewEntity, NewFact (input shapes for createEntity / createFact)
  • ScopeFields, ScopeFilter

Store

  • IMemoryStore
  • EntityListFilter, EntitySearchOptions, ListOptions
  • FactFilter, FactOrderBy, FactQueryOptions
  • Page<T>
  • SemanticSearchOptions

Retrieval

  • EntityView, ContextOptions, ContextTier
  • RelatedTask, RelatedEvent
  • Neighborhood, TraversalOptions
  • UpsertEntityResult

Resolution

  • EntityCandidate
  • ResolveEntityQuery, ResolveEntityOptions
  • UpsertBySurfaceInput, UpsertBySurfaceOptions, UpsertBySurfaceResult
  • EntityResolutionConfig

Extension points

  • IEmbedder, IProfileGenerator
  • IRuleEngine, IScopedMemoryView

Integration

  • ConnectorEmbedderConfig, ConnectorProfileGeneratorConfig
  • MemoryConnectorsConfig, MemorySystemWithConnectorsConfig
  • ProfileGeneratorInput (profile prompt), ExtractionPromptContext, PreResolvedBinding
  • ExtractionMention, ExtractionFactSpec, ExtractionOutput
  • IngestionResolvedEntity, IngestionError, IngestionResult
  • ExtractionResolverOptions

Signal ingestion

  • SignalIngestor, SignalIngestorConfig
  • IngestSignalInput, IngestTextInput, IngestExtractedInput
  • SignalSourceAdapter, ExtractedSignal, ParticipantSeed
  • IExtractor, ConnectorExtractor, ConnectorExtractorConfig
  • PlainTextAdapter, PlainTextRaw
  • EmailSignalAdapter, EmailSignal, EmailAddress, EmailSignalAdapterOptions

Mongo adapter

  • IMongoCollectionLike, MongoFilter, MongoFindOptions, MongoUpdate, MongoUpdateOptions, MongoUpdateResult, MongoSort, MongoBulkOp
  • RawMongoDriverCollection, RawMongoClientLike
  • MeteorCollectionLike
  • MongoMemoryAdapterOptions
  • EnsureIndexesArgs

Events + config

  • ChangeEvent
  • MemorySystemConfig, EmbeddingQueueConfig, RankingConfig

Access control

  • AccessLevel, Permission, Permissions, AccessControlled
  • PermissionDeniedError, OwnerRequiredError
  • canAccess, effectivePermissions, assertCanAccess, levelGrants
  • DEFAULT_GROUP_LEVEL, DEFAULT_WORLD_LEVEL

For usage patterns, end-to-end examples, and architectural guidance, see MEMORY_GUIDE.md.

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

Search documentation

Type to search the v2 documentation.