Agent memory that can explain itself
A customer success agent flags Acme Corp as a churn risk. The VP of Customer Success asks: "Why?" The agent says "based on the available data." That's not an answer. Which data? Which logic? What would need to change for Acme to not be at risk? If the agent can't show its work, the VP can't trust the flag, can't prioritize it, and can't act on it.
This is the fundamental problem with agent memory stored as text chunks in a vector database. The agent retrieves relevant passages, generates a conclusion, and the reasoning is whatever happened inside the model. There's no trace to inspect, no rule to audit, no way to ask "what would need to change for this conclusion to be different?"
When agents store observations as structured facts and derive conclusions through explicit rules, every conclusion has a proof tree. You can ask why. You can ask why not. And when the underlying facts change, the conclusions update automatically - no stale beliefs lingering in a context window.
The setup
A customer success agent monitors two accounts. It has stored three observations about each.
// Acme: enterprise customer, $150K contract, declining usage, renewal in April
+customer[("acme", "enterprise", 150000)]
+usage_trend[("acme", "declining")]
+renewal[("acme", "2026-04-15")]
// Globex: startup, $25K contract, growing usage, renewal in September
+customer[("globex", "startup", 25000)]
+usage_trend[("globex", "growing")]
+renewal[("globex", "2026-09-01")]
Six facts. Each one is a specific, testable observation - not a text passage that might or might not be retrieved in the right context window.
The rules
Churn risk requires three conditions: the customer is high-value, their usage is dropping, and their renewal is imminent. Each condition is its own rule with clear criteria.
// High-value: enterprise tier with contract over $100K
+high_value(C) <- customer(C, "enterprise", Amt), Amt > 100000
// Engagement dropping
+engagement_drop(C) <- usage_trend(C, "declining")
// Renewal within the next few months
+renewal_soon(C) <- renewal(C, Date), Date < "2026-06-01"
// Churn risk requires all three
+churn_risk(C) <- high_value(C), engagement_drop(C), renewal_soon(C)
These rules are readable, auditable, and versionable. When the definition of "high-value" changes (say the threshold moves from $100K to $75K), you change one rule and every derivation updates.
Who is at risk?
?churn_risk(C)
┌────────┐
│ C │
├────────┤
│ "acme" │
└────────┘
1 rows
Acme is flagged. Globex is not.
Why is Acme flagged?
This is the point of the entire page. The .why command returns the proof tree - the exact chain of facts and rules that produced the conclusion.
why ?churn_risk("acme")
The proof tree shows three branches: Acme is high-value because it's an enterprise customer with a $150K contract (which exceeds the $100K threshold). Acme has an engagement drop because its usage trend is "declining." Acme's renewal is soon because April 15th is before June 1st. All three conditions met, so the churn_risk rule fired.
This isn't a model's interpretation. It's a deterministic chain you can inspect, challenge, and reproduce. The VP sees exactly which conditions triggered the flag, and can decide which intervention addresses which condition.
Why is Globex NOT flagged?
Equally important: understanding why a conclusion was not reached.
why_not churn_risk("globex")
churn_risk("globex") was NOT derived:
Rule: churn_risk (clause 0)
churn_risk(C) <- high_value(C), engagement_drop(C), renewal_soon(C)
Blocker: high_value("globex") - No matching tuples
Globex is not at churn risk because it's not high-value. It's a startup with a $25K contract, which doesn't meet the enterprise + >$100K threshold. The blocker is specific: the first condition in the rule failed, and here's why. No ambiguity.
The situation changes
The customer success team runs an intervention. Acme's usage stabilizes.
-usage_trend("acme", "declining")
+usage_trend[("acme", "stable")]
?churn_risk(C)
No results.
The churn risk retracted. The engagement_drop condition no longer holds, so the conclusion disappeared automatically. No cleanup logic, no manual flag removal, no stale belief sitting in the agent's memory. The agent's next answer reflects the current state of the world.
Why this matters
Three properties work together here. The rules guarantee that every conclusion follows from specific, inspectable conditions - not from whatever a model generates in a particular context window. The proof tree (.why and .why_not) makes the reasoning transparent and auditable. And correct retraction means that when facts change, conclusions update - the agent never acts on a belief that's no longer supported by the evidence.
Every code block on this page runs against a live InputLayer instance. Paste them into the demo to see the results yourself.