Events vs state
The litmus test for what belongs in durable memory.
Store events. Don't store state that another system owns — fetch it live when you need it. That one distinction keeps pgmem from becoming a stale copy of your billing system, your CRM, or your database.
Why it matters
An agent cancels a subscription, the call succeeds, and it feels natural to
remember "subscription sub_123 is cancelled." Three weeks later the customer
re-subscribes in your billing portal. pgmem never hears about it. On the next call
the agent tells them their subscription is cancelled — from a memory that's now
wrong.
The truth lives in the system of record (Stripe, here). The moment you copy current state into memory, you've bet that pgmem will be told about every future change to it. For state another system owns, that's a bet you can't win.
The litmus test
Ask one question of every candidate memory:
If this value changed in the source system tomorrow and pgmem never found out, would serving the stored value be wrong or harmful?
- Yes → it is current state owned elsewhere (a subscription's status, an account balance, a ticket's priority, an inventory level). Don't store it. Fetch it live at compile time — see How it works below.
- No → it is an event (the user asked to cancel on Monday), a derived preference (they prefer email over SMS), a decision as of a moment, or a narrative thread. That is genuinely pgmem's to own.
The cancellation example resolves cleanly under the test: remember the
request ("user requested cancellation of sub_123") as a durable event;
never store the status ("is cancelled"). When the agent needs the current
status, it asks the system of record.
How it works
The split is point-in-time vs interval, and it falls straight out of pgmem's bi-temporal model (see The bi-temporal model):
- Events are points.
valid_atis enough; the interaction happened, so the fact that it happened is permanently true. It can't go stale. - State is an interval whose endpoints someone else can move without telling pgmem. You cannot maintain a valid-time interval correctly for a fact whose source you don't control — so don't try.
That gives you three concrete moves.
Remember an event (Tier 3 episodic memory, the agent owns it):
await session.observe(
subject=SubjectRef(kind="subscription", identifier="sub_123"),
predicate="subscription.cancellation_requested",
value=True,
authority=Authority.user,
confidence=1.0,
evidence=[Evidence(kind="user_turn", reference="turn-1", authority=Authority.user)],
)Assert an event from a tool, verbatim (durable, with a pointer back to the source system, no LLM re-derivation):
from pgmem import ExternalFact, ExternalReference
await mem.add_episode(
content=stripe_webhook_payload,
group_id="acme",
external_facts=[
ExternalFact(
content="Subscription sub_123 cancellation requested by user-789",
external_refs=[ExternalReference(system="stripe", id="sub_123")],
source=Authority.application,
),
],
)Fetch current state live — never store it. Read the system of record while the rest of the turn runs, and hand the result to the compiler as Tier 6 live context (it is injected into the prompt but never persisted):
sub = await stripe.subscriptions.retrieve("sub_123")
context = await session.compile_context(
query="Is my subscription still cancelled?",
injected_context=[f"Stripe: sub_123 status={sub.status}"],
)The agent now sees the event and the live status side by side. There's no conflict to resolve, because memory never claimed to know the status — it only recorded that a request happened. See Use live system-of-record state for the full wiring.
Tradeoffs & pitfalls
add_episode(content=...) runs LLM extraction over raw text. If you feed it a
state-laden payload ("subscription is cancelled"), the extractor may mint a
state edge in the graph. Assert events through external_facts (no extraction),
and keep current-state text out of the extraction content.
- pgmem can't enforce the litmus test — it gives you the rule, the typed event
paths, and the live slot. The judgment stays in your code, because only you
know that
sub_123lives in Stripe. Centralise it with a ClaimPolicy. - Anything the extractor does pull into the graph is bounded by
valid_at("true as we understood it then"), and the live slot supersedes it at read time — but don't rely on that to launder state into memory.
Related
- How a turn flows — where live state is fetched and injected
- The bi-temporal model — why events are points and state is an interval
- Decide what to remember — centralise the litmus decision
- Use live system-of-record state — the Tier 6 wiring