Reasoning with N3 · Chapter 1 of 10
Chapter 1
Why rules in agent memory
A memory that only stores what it was told cannot say what follows from it. Rules can. In tiramemsu, what a rule concludes keeps its reasons, respects both clocks, and falls when its reasons go.
What follows from memory
An agent has written two facts: Alice works at Acme, and Acme is in Berlin. A user asks where Alice works. No statement says Alice works in Berlin. The answer follows from two that do.
There are three usual ways out, and each has a cost:
- Infer it in the prompt, every time. The model does the join again on every question. It costs tokens, and two runs may disagree.
- Write the conclusion as a fact. It is fast to read. But when Alice changes jobs, the old conclusion stays, and nothing says it was only ever a conclusion.
- Query for it. A SPARQL join works for one question. It does not chain, and a hundred such questions become a hundred queries to maintain.
A rule states the inference once, in a form a machine can run and a person can read:
where-people-work.n3
@prefix v: <urn:tiramemsu:v:>.
{ ?p v:worksAt ?c. ?c v:locatedIn ?l } => { ?p v:worksIn ?l }.
Read it as: for any ?p, ?c and ?l, if ?p works at ?c and ?c is located in ?l, then ?p works in ?l. This is Notation3, or N3, the rule language of the RDF world. Chapter 2 teaches enough of it to read every example in this book.
What a plain reasoner gets wrong
Run that rule with any N3 reasoner over an export of the memory and you get an answer. In a memory with two clocks, it is often the wrong one.
- Time in the world. Alice worked at Acme from 2020 to 2022. Acme moved to Berlin in 2024. A plain reasoner joins the two facts and says Alice worked in Berlin. She never did. In tiramemsu, two facts that were never true at the same time never combine. Chapter 5 shows how.
- Time in the database. An agent made a decision last Tuesday. Why? The answer depends on what it knew last Tuesday and on the rules it had then, not on today’s. In tiramemsu a reasoning run can happen on any past view, with the rules believed then. Chapter 4.
- Reasons that go away. A user says Alice never worked at Acme. The fact is retracted. A conclusion drawn from it must go too, at once, without anyone remembering to clean up. Chapter 7.
Grounded conclusions
Tiramemsu already keeps one promise about grounding: a layer never outlives the fact it is about. Retract a fact and its confidence, its source and the beliefs that cite it are retracted in the same transaction. The companion article Memory that stays grounded explains why that cascade takes exactly what it must.
Reasoning extends the promise to conclusions. When tiramemsu writes what a rule concludes, it writes a derived belief: a statement with one support link to every premise it used and one to the stored rule set. A conclusion is grounded when all of its supports are live. The cascade follows support links, so:
- retract or correct a premise, and the conclusion is retracted in that transaction;
- edit or retract the rule set, and every conclusion it produced is retracted in that transaction;
- retract a conclusion, and every conclusion drawn from it goes with it.
No background job and no reconciliation pass are involved. It is the same cascade that retracts layers, with one more kind of edge to follow.
Never forget, still
Tiramemsu never deletes. A reasoner that writes must keep that rule too. It does:
- Materialization only asserts and supersedes. It never edits a stored row.
- A retracted conclusion stays in history with its support links, so an
as_ofview shows what was concluded then and from what. - Widening a conclusion’s valid interval is a supersede. Editing a rule set is a supersede. Both are visible in history.
Three verbs
The whole feature is three operations:
| Verb | Writes? | What it does |
|---|---|---|
View::reason(rules, &ReasonOptions) | No | Runs N3 rules over a view and returns the conclusions, each with a proof made of statement ids. Accepts every builtin, including negation and the clock. |
Tx::store_rules(rule_set, n3) | Yes | Keeps a rule set as one statement, so rules have history and can be corrected. |
Db::materialize(rule_set, &MaterializeOptions) | Yes | Reasons with a stored rule set and writes the conclusions as derived beliefs. Monotonic rules only. |
A first taste, in Rust. The rule finds who is mortal. Nothing is written.
use tiramemsu::{ReasonOptions, Rules};
let rules = r#"@prefix v: <urn:tiramemsu:v:>.
{ ?x a v:Human } => { ?x a v:Mortal }."#;
let result = db.now().reason(Rules::Text(rules.into()), &ReasonOptions::default())?;
for c in &result.conclusions {
// (v:socrates rdf:type v:Mortal), proof { rule: 0, premises: [e1] }
println!("{} {} {} rule {} premises {:?}", c.s, c.p, c.o, c.proof.rule, c.proof.premises);
}
The premises are statement ids, not copies of triples. Each one points at the exact statement the conclusion rests on, with its layers, its clocks and its history.
Before you start
n3, off by default (OpenSpec change add-n3-reasoning, built on add-derived-beliefs). The reasoner is eyeron by Jos De Roo (MIT). Until eyeron is on crates.io the feature is available only when building from the repository.You need to know tiramemsu’s two clocks and what a layer is. If either is new, read Time travel and bitemporality, explained and Layered graphs, explained first. You do not need to know N3. The next chapter teaches it.