Skip to Content
DocsConceptsEpisode vs Fact

Episode vs Fact

Memory holds two different things, with different shapes and lifecycles.

Episode

An Episode is the raw record of one conversation. Created at the end of every agent.run(), written by memory.remember(episode).

@dataclass class Episode: id: str user_id: str | None session_id: str | None messages: list[Message] # the full conversation summary: str # auto-generated, short created_at: datetime metadata: dict[str, Any]

What episodes are good for:

  • Conversation continuity. When a new run uses the same session_id, the framework rehydrates the prior episodes’ messages into the message stack. You get coherent multi-turn dialogue across process restarts without writing any state code.
  • Vector recall, memory.recall(query, user_id=) returns the top-k episodes most similar to a query. The model sees them as context if relevant.
  • Audit and debug. Every conversation is in the store; you can read what the agent actually said.

Fact

A Fact is a structured (subject, predicate, object) claim the framework extracts from episodes. Bi-temporal. Every fact has valid_from and valid_until.

@dataclass class Fact: id: str user_id: str | None subject: str # "alice" predicate: str # "lives_in" object: str # "Tokyo" valid_from: datetime # when it started being true valid_until: datetime | None # None = currently true source_episode_id: str | None # which episode produced it confidence: float

What facts are good for:

  • Recall by structure. “What is Alice’s plan?” maps to memory.facts.query(user_id="alice", subject="alice", predicate="subscription_plan").
  • Historical queries. “What did we know on Jan 1?” maps to memory.facts.query(..., valid_at=datetime(2026, 1, 1)).
  • Cheap context. Facts are short. Surfacing the top facts in the system message scales; surfacing all episodes doesn’t.

Auto-extract turns episodes into facts

Every agent.run() against a real network model triggers a small Consolidator pass that reads the new episode’s messages and emits JSON (subject, predicate, object, confidence) tuples. Each new fact goes through bi-temporal supersession. Contradicting an older claim closes off the older fact’s valid_until.

This is on by default for OpenAIModel / AnthropicModel / LiteLLMModel. Off for EchoModel / ScriptedModel. Toggle via Agent(auto_extract=True/False).

When the model sees them

SourceWhere it lands in the message stack
Working memory blocksA “User profile:” section in the system message.
Recalled factsA “Known facts:” section in the system message (auto-extracted).
Recent episodesVector-recalled and surfaced as a “Relevant past:” section if recall_k > 0.
Rehydrated sessionPrior turns of the SAME session_id are rebuilt as actual role=user / assistant messages.

Lifecycles

EpisodeFact
Created atend of every agent.run()by Consolidator (auto) or memory.facts.append() (manual)
Updatednever (immutable)supersession (older fact’s valid_until updated)
Deleted bymemory.forget(user_id=)memory.forget(user_id=)
Multi-tenantpartitioned by user_idpartitioned by user_id
Backendepisode storefact store (paired with the memory backend)

When to write facts manually

Auto-extract is great for chat, but for trusted ingestion (CRM, LDAP, billing), write directly:

from loomflow import Fact from datetime import datetime, UTC await agent.memory.facts.append(Fact( user_id="alice", subject="alice", predicate="subscription_plan", object="enterprise", valid_from=datetime.now(UTC), confidence=1.0, source_episode_id=None, ))

Manual writes go through the same supersession logic. Appending “enterprise” when there’s an existing “professional” closes the older one off.

Why two stores? Episodes are what was said. Long, free-form, useful for recall. Facts are what’s true. Short, structured, useful for querying. Conflating the two would produce a store that’s either too big to query cheaply or too lossy to recall accurately. Two stores, one Memory protocol.

Read more

Last updated on