pgmem
Concepts

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_at is 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_123 lives 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.

On this page