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
- Core Types
- MemorySystem
- Storage Adapters
- Entity Resolution
- Integration Layer
- Extraction Helpers
- Ranking
- Predicate Registry
- Access Control
- Events
- Errors
- Type Reference
Core Types
IEntity
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
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) orvalue(attribute).detailsis optional narrative. - Document fact: set
details(long-form markdown). LeaveobjectId/valueunset. - Canonical profile:
predicate: 'profile',kind: 'document'.MemorySystem.getContextreturns this automatically. - Supersession: set
supersedesto the prior fact's id.MemorySystem.addFactwrites the new fact first, then archives the predecessor (crash-safe ordering). - Signal provenance: always set
sourceSignalIdwhen the fact came from a signal. Opaque string; library users own the signal store.
Identifier
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
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
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.groupIdset →fact.groupIdmust equal it. - If
entity.ownerIdset →fact.ownerIdmust equal it. - Facts may narrow further (add
ownerIdwhen entity has onlygroupId).
Violations throw ScopeInvariantError.
MemorySystem
The facade. All business logic lives here.
Construction
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.
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:
- For each identifier, call
findEntitiesByIdentifier(kind, value, scope). - Zero matches → create new entity (version 1).
- One match → merge new identifiers/aliases; bump version.
- 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.
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:
- Call
resolveEntitywith the same query. - If top candidate confidence ≥ threshold, append surface + new identifiers as aliases on the matched entity (alias accumulation). Return.
- Otherwise create a new entity with the surface as
displayName. Return near-matches asmergeCandidates.
Never auto-merges. mergeCandidates is advisory — use MemorySystem.mergeEntities to actually merge after human review.
resolveEntity
Return ranked candidate entities for a surface form.
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
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.
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.
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.
archiveEntity(id: EntityId, scope: ScopeFilter): Promise<void>;
Emits entity.archive event.
deleteEntity
deleteEntity(id: EntityId, scope: ScopeFilter, opts?: { hard?: boolean }): Promise<void>;
- Default (
hard: false): same asarchiveEntity— 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.
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).
// 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.
addFact(
input: Partial<IFact> & {
subjectId: EntityId;
predicate: string;
kind: FactKind;
},
scope: ScopeFilter,
): Promise<IFact>;
Validation:
- Subject entity must be visible to caller.
- If
objectIdset, object entity must be visible to caller. - If
contextIdsset, every entity in the list must be visible to caller. - Scope invariant against subject entity.
Auto-computed fields (if absent):
id:fact_<uuid>createdAt: nowobservedAt: nowisSemantic: defaults totrue;isSemantic: falseis the explicit opt-out.groupId: input → subject entity → scope.ownerId: input → callerscope.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
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.
supersedeFact(
oldId: FactId,
newInput: Partial<IFact> & { predicate: string; kind: FactKind; subjectId: EntityId },
scope: ScopeFilter,
): Promise<IFact>;
archiveFact
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).
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
contextIdsof 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
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).
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.
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
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
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
flushEmbeddings(): Promise<void>; // await all pending embedding jobs
pendingEmbeddings(): number; // count of queued + in-flight
Lifecycle
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:
// 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:
- Apply
ScopeFilterto every read (defence-in-depth; MemorySystem also filters). - Assign primary ids on
createEntity/createFact. Native mechanisms preferred (Mongo ObjectId → hex string; Meteor Random.id(); UUID for in-memory). - Enforce optimistic concurrency on
updateEntity: reject if incomingversion !== stored.version + 1. - Hide archived records by default; show only when
filter.archived === true. - Support
asOfon fact queries (validFrom ≤ asOf ≤ validUntil ?? ∞ANDcreatedAt ≤ asOf). - Support
metadataFilteronlistEntities— equality +{ $in: [...] }at minimum. - Support
contextIdandtouchesEntityfilters onfindFacts.
InMemoryAdapter
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
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
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.
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().
class MeteorMongoCollection<T> implements IMongoCollectionLike<T> {
constructor(col: MeteorCollectionLike<T>);
}
ensureIndexes
One-time index setup helper.
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 oncreateEntity - Identifier lookup
- Fact CRUD + all filter options (
subjectId,objectId,contextId,touchesEntity,predicate,kind,archived,asOf,minConfidence, temporal) - Scope filtering on every read
metadataFilteronlistEntitiessearchEntities(case-insensitive substring on displayName, aliases, identifier values)
Optional capabilities (detected by MemorySystem via duck-typing):
traverse— otherwise generic BFS is usedsemanticSearch— 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.
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
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
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.
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.
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.
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.
function defaultProfilePrompt(ctx: ProfileGeneratorInput): string;
Extraction Helpers
For the "raw signal → structured memory" pipeline.
defaultExtractionPrompt
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.
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:
- If
opts.preResolvedis supplied, itslabel → EntityIdbindings are seeded into the label map up front — mentions that redeclare those labels are silently skipped (pre-resolved binding wins). - For each mention:
memory.upsertEntityBySurface(...). Passes already-resolved sibling labels ascontextEntityIdsfor disambiguation. - For each fact: translates
subject/object/contextIdsfrom mention labels to entity IDs; attachessourceSignalId; callsmemory.addFact. - Returns per-item successes + errors.
One bad mention or fact does NOT abort the rest — errors collected in unresolved.
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.
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— seedsfrom/to/ccas person participants and (opt-in, default on)from/to/ccnon-free email domains asorganizationparticipants; BCC is dropped for privacy.
Ships one reference extractor:
ConnectorExtractor— wraps a Connector + model; usesrunDirectwithresponseFormat: { type: 'json_object' }and parses defensively.
Ranking
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 fromobservedAt(fallbackcreatedAt).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_byall collapse toworks_atbefore storage. - Default importance. When the caller omits
importance, the registry'sdefaultImportanceis applied (falls back to ranking default 0.5 otherwise). - Default
isAggregate. For aggregate predicates likeinteraction_count. - Auto-supersede for
singleValuedpredicates. Writingcurrent_titletwice for the same subject auto-archives the first. Disable globally viapredicateAutoSupersede: false. - Ranking weights. Registry
rankingWeightvalues fold intoRankingConfig.predicateWeights. User-supplied weights win on collision. - LLM prompt rendering.
registry.renderForPrompt()produces a markdown vocabulary block for injection into extraction prompts.
Shape
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 viaMemorySystem.transitionTaskState). The same applies todueAt,priority,completedAt,createdBy/createdAt, and event/task scheduling onmetadata.startTime/endTime. Nostate_changed/has_status/current_status/completed/created/reviewed/has_due_date/has_priority/occurred_on/scheduled_for/started_on/ended_on/cancelled_due_topredicates exist. Inter-entity relationships such asprepares_for,blocked_by, anddepends_onremain facts.committed_tois 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
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.newPredicatesfor vocabulary-drift review. - strict:
addFactthrows when the predicate is not in the registry. Strict rejections throughExtractionResolverland inIngestionResult.unresolvedANDnewPredicates.
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
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:
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/addFactthrowOwnerRequiredErrorwhen neither input nor scope provides one. Admin delegation is allowed —input.ownerIdcan be any user id; equality withscope.userIdis NOT enforced. - Owner always wins. If
record.ownerId === caller.userId, the caller has full access regardless ofpermissions. - The
permissions.{group, world}block is write-once via the first-class API.MemorySystem.upsertEntitydoes NOT rewritepermissionson existing records; the dirty path silently preserves stored permissions. Use the store'supdateEntity/updateFactas an admin escape hatch, or re-emit as the owner. (Explicitaclgrants ARE mutable — viasetAccess; see Principal ACLs below.) See MEMORY_PERMISSIONS.md § Changing access. - Profile regeneration preserves privacy.
regenerateProfileinherits the prior profile'spermissionswhen one exists (so a private profile stays private across auto-regen). First-time generation goes throughvisibilityPolicy, falling back to library defaults only when no policy applies. - Supersession chains stay per-subject.
addFactwithsupersedesnow validates thatpredecessor.subjectId === input.subjectIdand throws otherwise — prevents retrieval corruption where "what superseded F1?" returns a fact about a different subject.
Defaults (when permissions is omitted)
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):
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
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):
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):
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:
// 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:
// 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.
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,FactKindIEntity,IFact,IdentifierNewEntity,NewFact(input shapes forcreateEntity/createFact)ScopeFields,ScopeFilter
Store
IMemoryStoreEntityListFilter,EntitySearchOptions,ListOptionsFactFilter,FactOrderBy,FactQueryOptionsPage<T>SemanticSearchOptions
Retrieval
EntityView,ContextOptions,ContextTierRelatedTask,RelatedEventNeighborhood,TraversalOptionsUpsertEntityResult
Resolution
EntityCandidateResolveEntityQuery,ResolveEntityOptionsUpsertBySurfaceInput,UpsertBySurfaceOptions,UpsertBySurfaceResultEntityResolutionConfig
Extension points
IEmbedder,IProfileGeneratorIRuleEngine,IScopedMemoryView
Integration
ConnectorEmbedderConfig,ConnectorProfileGeneratorConfigMemoryConnectorsConfig,MemorySystemWithConnectorsConfigProfileGeneratorInput(profile prompt),ExtractionPromptContext,PreResolvedBindingExtractionMention,ExtractionFactSpec,ExtractionOutputIngestionResolvedEntity,IngestionError,IngestionResultExtractionResolverOptions
Signal ingestion
SignalIngestor,SignalIngestorConfigIngestSignalInput,IngestTextInput,IngestExtractedInputSignalSourceAdapter,ExtractedSignal,ParticipantSeedIExtractor,ConnectorExtractor,ConnectorExtractorConfigPlainTextAdapter,PlainTextRawEmailSignalAdapter,EmailSignal,EmailAddress,EmailSignalAdapterOptions
Mongo adapter
IMongoCollectionLike,MongoFilter,MongoFindOptions,MongoUpdate,MongoUpdateOptions,MongoUpdateResult,MongoSort,MongoBulkOpRawMongoDriverCollection,RawMongoClientLikeMeteorCollectionLikeMongoMemoryAdapterOptionsEnsureIndexesArgs
Events + config
ChangeEventMemorySystemConfig,EmbeddingQueueConfig,RankingConfig
Access control
AccessLevel,Permission,Permissions,AccessControlledPermissionDeniedError,OwnerRequiredErrorcanAccess,effectivePermissions,assertCanAccess,levelGrantsDEFAULT_GROUP_LEVEL,DEFAULT_WORLD_LEVEL
For usage patterns, end-to-end examples, and architectural guidance, see MEMORY_GUIDE.md.