Guides
Memory graph: entities, relationships, and facts
Give your agent a structured memory: typed entities, directed relationships between them, and temporal facts that keep their history when they change. Where document memory remembers what was said, the memory graph tracks what is true — and what used to be.
Plain document memory (remember / recall) stores prose and finds it again semantically. That is the right tool for context: session notes, conversation turns, things worth re-reading. It is the wrong tool for state. "Who does Jane work for now?" shouldn't depend on which transcript ranks first — it's a lookup, not a search. The memory graph gives your agent that system of record:
- Entities are typed nodes — a person, a project, a preference — with a display name, aliases, and scalar attributes.
- Relationships are directed, typed edges between entities:
jane —works_at→ acme. - Facts are
predicate = valueassertions with temporal semantics: a newer conflicting fact supersedes the old one, and the old one is retained in history.
An agent that has accumulated months of entities, relationships, and fact history isn't just retrieving better — it knows things no freshly built index can reproduce.
The graph lives on the same Memory facade as document memory: construct it once with an entity_id (a user, customer, patient, or agent session) and every call — remember, recall, and all the graph methods below — is automatically scoped to that entity, plus your partition when the underlying client is partition-scoped. New to Memory? Start with a quickstart or the concepts overview.
Entities
upsert_entity creates or updates a typed node in the entity's graph. The type is caller-controlled — person, company, project, preference, whatever your domain needs — and each node can carry an optional display_name, aliases, and a map of scalar attributes (string, number, boolean, or null values; no nested objects). Omit the node id to mint a new one; pass an existing id to update that node.
from aether import Memory
memory = Memory("user-42")
acme = memory.upsert_entity(
"company",
display_name="Acme Corp",
attributes={"industry": "logistics"},
)
jane = memory.upsert_entity("person", display_name="Jane", aliases=["J."])
people = memory.list_entities(entity_type="person")
same = memory.get_entity(jane.memory_entity_id)
Every node gets an engine-minted memory_entity_id (you can supply your own to make upserts idempotent). Don't confuse it with the Memory's entity_id: the entity_id is the owner of the whole graph; memory_entity_ids name the typed nodes inside it.
Relationships
relate draws a directed, typed edge between two nodes. Direction matters — the arguments are from, to, type — and the type is caller-controlled (works_at, owns, prefers, reports_to, …). Edges can carry scalar attributes and an optional valid_from timestamp for when the relationship became true, if you know it.
rel = memory.relate(jane.memory_entity_id, acme.memory_entity_id, "works_at")
# Everything Jane is connected to
edges = memory.list_relationships(from_entity_id=jane.memory_entity_id)
# Point-in-time view: the edges that were active on Jan 1
january = memory.list_relationships(
relationship_type="works_at",
as_of="2026-01-01T00:00:00Z",
)
# Everything ever recorded, retracted/superseded edges included
all_edges = memory.list_relationships(include_inactive=True)
Each edge carries three timestamps: observed_at (when Aether ingested it), an optional valid_from (when it became true in the world, if known), and invalid_from — null while the edge is active, set once it is retracted or superseded. list_relationships returns active edges by default; as_of gives you the set that was active at a given instant, and include_inactive includes retired edges too.
Temporal facts
remember_fact asserts a predicate = value fact. By default the fact is about the owner — the entity the Memory is scoped to — so remember_fact("favorite_color", "blue") means user-42's favorite color is blue. Values are scalars: a string, number, boolean, or null.
The temporal part is what makes facts different from attributes: when the user changes their mind, you just assert again.
memory.remember_fact("favorite_color", "blue")
# Later, the user changes their mind — the new fact supersedes the old one
memory.remember_fact("favorite_color", "green")
active = memory.list_facts(predicate="favorite_color")
print(active[0].value) # "green" — one active fact
history = memory.fact_history("favorite_color")
for f in history:
print(f.value, "active" if f.invalid_from is None else "superseded")
The rule is latest wins, history preserved: for a single-valued predicate, a newer conflicting fact supersedes the old one — the old fact's invalid_from is set, the new fact's supersedes_fact_id points back at it, and nothing is deleted. Supersession is deterministic and scoped to your tenant. list_facts shows the active view; fact_history returns the full assertion chain for one predicate, so "since when?" and "what did it used to be?" are one call away.
Two temporal fields let you separate world-time from ingest-time: observed_at is when Aether recorded the fact (set for you unless you override it), and the optional valid_from is when the fact became true in the world — useful when the user tells you today about something that happened in March. You can also pass an as_of timestamp to list_facts to read the facts that were active at that instant, or supersedes_fact_id to remember_fact to explicitly replace one specific fact.
Facts about entities and relationships
Facts default to the owner, but any node or edge in the graph can be the subject — set subject_type to entity or relationship and pass the node/edge id as subject_id. And when a predicate legitimately holds several values at once, set cardinality to multi so new assertions accumulate instead of superseding.
# A fact about a graph node, not the owner
memory.remember_fact(
"status", "active",
subject_type="entity", subject_id=acme.memory_entity_id,
)
# Multi-valued predicates accumulate instead of superseding
memory.remember_fact("allergy", "peanuts", cardinality="multi")
memory.remember_fact("allergy", "shellfish", cardinality="multi")
facts = memory.list_facts(subject_type="entity", subject_id=acme.memory_entity_id)
Writing facts by hand vs. extracting them
The graph methods on this page store structured facts you write explicitly. If you want Aether to distill free text into fact-memories automatically at insert time (remember(..., extract=True) / extract_facts), that is a separate, document-based feature with its own guide — see Fact extraction. Extracted facts capture "what was said"; graph facts model "what is true" as typed data.
Consolidation
Long-running agents accrue redundant assertions — the same fact restated across sessions, values superseded many times over. consolidate compacts the entity's active fact set: redundant facts are soft-retracted (kept in history, so fact_history and include_inactive still show them) and the active view stays small and unambiguous. It returns a report of what changed.
report = memory.consolidate()
print(report.active_facts_before, "→", report.active_facts_after,
f"({report.retracted} retracted)")
The report has three fields: active_facts_before (active facts in scope before the run), active_facts_after (active facts remaining), and retracted (redundant facts soft-retracted, kept in history). Consolidation never deletes — it only moves facts from the active view into history.
Next steps
- Memory API — the full
/v1/memory/*REST reference behind these methods - Fact extraction — distill free text into fact-memories automatically
- Recency-weighted ranking — bias document recall toward what's current
- Multi-tenant patterns — partition scoping, which the graph inherits