Brainby arc-labs/docs
Concept

Entities

The entity graph backing Brain — canonical nouns with a stable EntityId, resolved per tenant, that statements are about and relations connect.

The entity graph is what separates Brain from a flat vector index. When the extractor lifts "Priya works at Arc Labs" from a memory, it resolves two entities (Priya, Arc Labs), creating them if they don't exist, and writes a relation between them. Later, "tell me about Priya" can return statements she's the subject of, events about her, and memories where she appears as the object ("I interviewed Priya"). That last category is where flat similarity search fails — Priya isn't the lexical center of "I interviewed Priya," but she's clearly the topic.

The entity record

An entity is the identity of a referent — distinct from the memories that mention it, the statements about it, and the relations that connect it.

struct Entity {
    id: EntityId,               // UUIDv7 — stable across renames, aliases, attribute edits
    namespace_id: NamespaceId,  // owning tenant
    agent_id: AgentId,          // owning agent
    entity_type_id: EntityTypeId,
    canonical_name: String,     // normalized display name
    aliases: Vec<String>,       // alternative surface forms
    attributes: EntityAttributes,
    created_at_unix_nanos: u64,
    last_seen_at_unix_nanos: u64,
    tombstoned: bool,
    merged_into: Option<EntityId>,  // Some(id) after a merge — a redirect
}

The EntityId is stable: renames change canonical_name, alias additions extend the list, and attribute updates happen in place — the id never moves. It changes only through a merge, where the merged-away entity's id becomes a redirect.

Entity types

entity_type_id is interned from the active schema's type declarations. The seeded brain: system schema ships built-in types; user schemas add deployment-specific ones.

Built-in typeDescription
PersonA named human
OrganizationA company, group, or institution
ProjectA named body of work
PlaceA geographic location or venue
ConceptA topic, idea, or fallback
EventA named occurrence

An entity's owning namespace is distinct from the namespace of its type: an entity owned by tenant acme can carry the shared brain:Person type.

Resolution is per tenant

Turning a surface form like "Priya" into an EntityId runs through the resolver gauntlet — exact-name, then alias, then trigram similarity, and (for hard cases) an LLM tier. All of those indexes are keyed under a leading (namespace_id, agent_id) scope prefix, so a surface form resolves only against entities the caller's own tenant owns. "Priya Patel" in tenant acme and "Priya Patel" in tenant globex are two separate entities with two separate ids — one tenant's resolution can never reach another's rows.

Aliases are what let "Priya," "Priya P.," and "Priya Patel" collapse to one entity. When resolution is genuinely ambiguous, the extractor keeps the surface form rather than mis-attributing it.

Traversal at retrieval time

The graph retriever operates on the entity graph — entities, relations, and statement subjects — from an anchor entity resolved out of the query. It runs in three modes:

  • Star — anchor → its outgoing/incoming relations → neighbouring entities; returns statements about them and memories that mention them.
  • Path — from entity A to entity B, find connecting paths up to a depth bound; returns the relations and entities along the way.
  • Subgraph — anchor → its k-hop neighbourhood; returns the set of entities, relations, and statements.

Traversal defaults to depth 3 (capped at 5); one to two hops are O(log N) fast, deeper hops are cost-capped by the planner. Results carry a proximity score (1 / (hop_distance + 1)) that feeds RRF fusion as one lane among three — see Read pipeline. The two directions aren't symmetric: "what do we know about Priya" (she's the subject) and "where does Priya appear" (she's an object) surface different, both-useful sets.

Merging duplicates

Names drift — "Arc Labs," "Arc Labs Inc.," "ArcLabs" — and the resolver isn't perfect. When two entity rows should be one, a merge collapses them. The survivor becomes canonical; the merged entity's merged_into points at the survivor and its id transparently redirects; every statement and relation that referenced the merged id is re-routed to the survivor, aliases and attributes fold in, and an audit row records the merge. A merge is complete and unmerge-able the moment its transaction commits, with a grace-period unmerge path.

Merges are driven either by the resolver (when the auto-merge confidence threshold passes) or by an operator via the ENTITY_MERGE wire op. Over the HTTP edge, entity administration is exposed under /v1/entities — see API: entities.

Why this is the wedge

Without typed entities, "tell me about Priya" is a similarity search against the verbatim string "tell me about Priya" — a flat store returns whatever's lexically closest, possibly a generic biography, possibly nothing. With entities, the retriever resolves the anchor and expands: run the graph lane from Priya, weight subject hits, walk relations one hop for context, and fuse with semantic and lexical search. That's a meaningfully different recall surface, and it exists only because the graph knows what an entity is.

Was this page helpful?

On this page