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 turn. Created at the end of every agent.run(), written by memory.remember(episode).

class Episode(BaseModel): id: str session_id: str user_id: str | None occurred_at: datetime input: str # the user message output: str # the agent's reply tool_calls: list[ToolCall] embedding: list[float] | None

What episodes are good for:

  • Conversation continuity. When a new run uses the same session_id, the framework rehydrates the prior episodes 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.

class Fact(BaseModel): id: str user_id: str | None subject: str # "alice" predicate: str # "lives_in" object: str # "Tokyo" confidence: float valid_from: datetime # when it started being true valid_until: datetime | None # None = currently true recorded_at: datetime # when we learned it sources: list[str] # episode ids this fact came from

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 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, sources=[], ))

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