Skip to Content
DocsMemoryBi-temporal facts

Bi-temporal facts

Every agent.run() against a real model auto-extracts structured (subject, predicate, object) claims into the bi-temporal fact store, partitioned by user_id. Facts are typed, validated, and queryable at any historical point in time.

What a Fact looks like

from loomflow import Fact from datetime import datetime, UTC fact = Fact( user_id="alice", subject="alice", predicate="lives_in", object="Tokyo", valid_from=datetime(2026, 1, 15, tzinfo=UTC), valid_until=None, # currently true source_episode_id="ep_01J...", confidence=0.94, )
FieldDescription
user_idMulti-tenant partition key. Every query is scoped by it.
subject / predicate / objectThe triple. Free-form strings.
valid_fromWhen this claim started being true.
valid_untilWhen it stopped being true. None = currently valid.
source_episode_idThe episode the consolidator extracted it from.
confidenceLLM-reported confidence in [0, 1].

Auto-extract pipeline

When auto_extract=True (the default for real-network models), every agent.run() triggers a small Consolidator pass after the run completes:

  1. The consolidator reads the new episode’s messages.
  2. A small LLM call (text-only, separate from the agent’s main model) is asked to return a JSON list of (subject, predicate, object, confidence) tuples.
  3. Each new fact is appended via memory.facts.append(fact).
  4. Supersession. When a new fact contradicts an existing one (same subject + predicate, different object), the old fact’s valid_until is set to the new fact’s valid_from. Historical facts are not deleted, just closed off.

Querying facts

# Currently-valid facts about Alice facts = await agent.memory.facts.query(user_id="alice", subject="alice") # Facts with a specific predicate facts = await agent.memory.facts.query( user_id="alice", subject="alice", predicate="lives_in", ) # Historical query — what did we know on Jan 1, 2026? from datetime import datetime, UTC facts_at_jan_1 = await agent.memory.facts.query( user_id="alice", subject="alice", valid_at=datetime(2026, 1, 1, tzinfo=UTC), )

valid_at= returns only facts whose [valid_from, valid_until) range covers the given timestamp. Without it, only currently-valid facts (valid_until is None) are returned.

Supersession in action

# Day 1 await agent.run( "Hi, I'm Alice and I live in Tokyo.", user_id="alice", ) # memory.facts now has: # Fact(subject="alice", predicate="lives_in", object="Tokyo", # valid_from=2026-01-15, valid_until=None) # Day 90 await agent.run( "Update — I moved to Berlin last month.", user_id="alice", ) # memory.facts now has: # Fact(subject="alice", predicate="lives_in", object="Tokyo", # valid_from=2026-01-15, valid_until=2026-04-10) # Fact(subject="alice", predicate="lives_in", object="Berlin", # valid_from=2026-04-10, valid_until=None)

Both facts are stored. Asking “where does Alice live?” gets Berlin (currently valid). Asking “where did Alice live in February 2026?” gets Tokyo (valid_at= Feb 2026).

Manual writes

Auto-extract is not always what you want. For facts ingested from a trusted source (CRM, LDAP, billing system), write them directly:

from loomflow import Fact 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, # not from a chat ))

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

Disabling auto-extract

agent = Agent("...", model="gpt-4o", auto_extract=False)

For cost-sensitive workloads where the consolidator’s per-run LLM call adds up. See Auto-extract observability.

Backend matrix

Each memory backend ships a paired fact store:

Memory backendFact store
InMemoryMemoryInMemoryFactStore
SqliteMemorySqliteFactStore
ChromaMemoryChromaFactStore
PostgresMemoryPostgresFactStore
RedisMemoryRedisFactStore

The bi-temporal contract is identical across all five. Supersession, valid_at queries, multi-tenant scoping all work the same way.

Why bi-temporal? Production systems need to answer “what did the agent believe at time T?”. For audit trails, compliance reviews, and debugging. Plain “last value wins” stores can’t do this. Bi-temporal facts are append-only at write time; supersession is a metadata update that preserves history.

Last updated on