Skip to main content

RushDB as a Memory Layer: Facts, Episodes, and References

Stateless LLM calls forget everything between turns. Retrieval from flat vector stores returns similar chunks without regard for how pieces of information connect. Neither approach gives an agent the ability to reason over connected context — who said what, when, in response to what, tied to which entities they already know.

RushDB can act as a structured memory layer that stores and links three kinds of information:

Memory typeWhat it storesExample
FactDurable properties of entitiesCustomer's plan tier, user's language preference, a product's spec
EpisodeTime-stamped interactions between entitiesA support conversation, a search session, a tool call result
ReferenceExternal documents or data linked into the graphA knowledge-base article, a policy document, an API response

Connecting all three lets an agent answer: "What did we decide last time this user asked about pricing, and what docs were cited in that conversation?"


Graph shape

The graph stores facts once and links episodes back to them. New facts can supersede old ones without deleting evidence.


Step 1: Store durable facts about an entity

from datetime import datetime, timezone
from rushdb import RushDB

db = RushDB("RUSHDB_API_KEY", base_url="https://api.rushdb.com/api/v1")

now = datetime.now(timezone.utc).isoformat()

user = db.records.create("ENTITY", {
"entityId": "user-42",
"type": "user",
"name": "Lena Müller"
})

plan_fact = db.records.create("FACT", {
"key": "plan",
"value": "enterprise",
"setAt": now,
"source": "billing-api"
})

lang_fact = db.records.create("FACT", {
"key": "language",
"value": "de",
"setAt": now,
"source": "profile"
})

db.records.attach(user.id, plan_fact.id, {"type": "HAS_FACT"})
db.records.attach(user.id, lang_fact.id, {"type": "HAS_FACT"})

Step 2: Record an episodic interaction

Each conversation, tool call, or workflow run becomes an EPISODE node. This allows replaying history without re-embedding full transcripts.

episode = db.records.create("EPISODE", {
"episodeId": "chat-2025-03-15",
"type": "support-chat",
"startedAt": "2025-03-15T09:00:00Z",
"summary": "User asked about upgrading. Agent cited pricing doc.",
"outcome": "resolved"
})

db.records.attach(user.id, episode.id, {"type": "HAD_EPISODE"})
db.records.attach(episode.id, plan_fact.id, {"type": "LED_TO"})

Reference nodes represent external documents, policy pages, or API responses that were cited during an episode. Link them to the episode so provenance is preserved.

kb = db.records.create("ENTITY", {
"entityId": "kb-main",
"type": "knowledge-base",
"name": "Product Knowledge Base"
})

pricing_ref = db.records.create("REFERENCE", {
"referenceId": "pricing-v3",
"title": "Pricing and Plans (v3)",
"url": "https://docs.example.com/pricing",
"version": "3.0",
"updatedAt": "2025-02-01T00:00:00Z"
})

db.records.attach(pricing_ref.id, kb.id, {"type": "PART_OF"})
db.records.attach(episode.id, pricing_ref.id, {"type": "CITED"})

Step 4: Retrieve connected context for an agent turn

When the user returns, retrieve all three memory types in one query so the agent starts from full context.

# Current facts
facts = db.records.find({
"labels": ["FACT"],
"where": {
"ENTITY": {
"$alias": "$user",
"$relation": {"type": "HAS_FACT", "direction": "in"},
"entityId": "user-42"
}
},
"select": {
"key": "$record.key",
"value": "$record.value",
"setAt": "$record.setAt"
}
})

# Recent episodes
recent = db.records.find({
"labels": ["EPISODE"],
"where": {
"ENTITY": {
"$relation": {"type": "HAD_EPISODE", "direction": "in"},
"entityId": "user-42"
}
},
"orderBy": {"startedAt": "desc"},
"limit": 5
})

# References from most recent episode
if recent.data:
latest_id = recent.data[0].get("episodeId")
cited = db.records.find({
"labels": ["REFERENCE"],
"where": {
"EPISODE": {
"$relation": {"type": "CITED", "direction": "in"},
"episodeId": latest_id
}
}
})

Step 5: Update a fact without deleting history

When a fact changes, create a new FACT node, link it with HAS_FACT, and optionally mark the old one superseded. This preserves the history chain.

upgrade_episode = db.records.create("EPISODE", {
"episodeId": "chat-2025-03-20",
"type": "billing",
"startedAt": "2025-03-20T14:00:00Z",
"outcome": "resolved"
})

db.records.attach(user.id, upgrade_episode.id, {"type": "HAD_EPISODE"})

new_plan = db.records.create("FACT", {
"key": "plan",
"value": "unlimited",
"setAt": datetime.now(timezone.utc).isoformat(),
"source": "billing-api"
})

db.records.attach(user.id, new_plan.id, {"type": "HAS_FACT"})
db.records.attach(upgrade_episode.id, new_plan.id, {"type": "LED_TO"})

# Mark old fact superseded
db.records.update(plan_fact.id, {"supersededAt": datetime.now(timezone.utc).isoformat()})

Step 6: Retrieve only current (non-superseded) facts

current_facts = db.records.find({
"labels": ["FACT"],
"where": {
"supersededAt": {"$exists": False},
"ENTITY": {
"$relation": {"type": "HAS_FACT", "direction": "in"},
"entityId": "user-42"
}
}
})

Production caveat

History-preserving fact graphs grow unbounded. Apply a retention policy: periodically archive EPISODE nodes older than your SLA window, and prune FACT nodes where supersededAt is more than N days old. Use db.records.delete with a where filter rather than record-by-record deletion for scale.


Next steps