Rust (Advanced)
import { Aside } from ‘@astrojs/starlight/components’;
This guide goes beyond the Rust quickstart to cover the complete API surface: all field semantics, valid-time succession, the oracle constructors, reconcile, and audit. Every symbol shown here exists in source and is verified against the real engine.
Opening an engine
Section titled “Opening an engine”SQLite (development and single-process production)
Section titled “SQLite (development and single-process production)”use mempill_sqlite::{open_default_for_agent, open_default_in_memory};use std::path::Path;
// File-backed — durable across restarts. File derived as base_dir/agent_{agent_id}.db.let engine = open_default_for_agent(Path::new("/path/to/data"), "my-agent")?;
// In-memory — ephemeral; useful for tests.let engine = open_default_in_memory()?;Both return DefaultEngine, a type alias for
EngineHandle<SqlitePersistenceStore, NoOpOracle, NoOpVector>.
SQLite with an oracle
Section titled “SQLite with an oracle”When a conflict requires external resolution, use the oracle constructors:
use mempill_sqlite::{open_with_oracle_for_agent, open_with_oracle_in_memory};use std::path::Path;use std::sync::Arc;
// Implement OraclePort for your resolver. File derived as base_dir/agent_{agent_id}.db.let engine = open_with_oracle_for_agent(Path::new("/path/to/data"), "my-agent", Arc::new(my_oracle))?;// or in-memory:let engine = open_with_oracle_in_memory(Arc::new(my_oracle))?;The returned type is OracleEngine<O>, another alias defined in mempill-sqlite/src/lib.rs.
See Writing an Oracle for the OraclePort trait contract.
PostgreSQL
Section titled “PostgreSQL”use mempill_postgres::{open_postgres, open_postgres_with_oracle, PostgresEngine};use mempill_core::{EngineConfig, NoOpOracle, NoOpVector};
let engine: PostgresEngine<NoOpOracle, NoOpVector> = open_postgres( "host=localhost port=5432 user=mempill dbname=mempill password=secret", None, // no oracle (use open_postgres_with_oracle for oracle wiring) None, // no vector EngineConfig::default(),)?;See PostgreSQL Backend for the full topology-b topology.
Ingesting a claim
Section titled “Ingesting a claim”EngineHandle::ingest_claim is async and takes IngestClaimRequest:
use mempill_core::application::IngestClaimRequest;use mempill_types::{ AgentId, Cardinality, Confidence, Criticality, ExternalKind, ProvenanceLabel, ValidTime,};use chrono::DateTime;
let resp = engine.ingest_claim(IngestClaimRequest { agent_id: AgentId("my-agent".into()), subject: "acme:ceo".into(), predicate: "held_by".into(), value: serde_json::json!("Alice"),
// Provenance: External(UserAsserted) or External(ExternalFirstHand) // means this is first-hand evidence — cheap-path eligible. provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand),
// Functional = at most one authoritative value at any valid-time instant. // SetValued = multiple co-existing values are legitimate. cardinality: Cardinality::Functional,
// valid_time = the real-world interval the claim covers. // None means "I don't know the validity window" — falls back to tx-time ordering. valid_time: Some(ValidTime { start: Some(DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")?.into()), end: None, // open-ended (still true) valid_time_confidence: 0.95, // confidence in the window itself start_granularity: None, // optional precision hint (Year/Month/Day/Instant) end_granularity: None, }),
confidence: Confidence { value_confidence: 0.95, // how confident in the value itself valid_time_confidence: 0.95, // how confident the window is correct },
// Criticality gates the adjudication threshold: Low, Medium, High, Critical. criticality: Criticality::Medium,
// For ModelDerived claims: UUIDs of source claims this was derived from. derived_from: vec![],}).await?;
println!("ref={} disposition={:?}", resp.claim_ref, resp.disposition);// ref=8b1c02ad-... disposition=CommittedCheapProvenance labels
Section titled “Provenance labels”ProvenanceLabel is an enum in mempill-types:
| Variant | Cheap-path eligible | Meaning |
|---|---|---|
External(ExternalKind::UserAsserted) |
Yes | First-hand human assertion |
External(ExternalKind::ExternalFirstHand) |
Yes | Tool result, system-of-record, sensor |
RecallReEntry |
No | Content served earlier, re-entering the write path |
ModelDerived |
No | Model-emitted / inferred content |
ModelDerived claims are committed down-weighted and cannot overturn External claims without anchoring. RecallReEntry is caught by the Amplification Guard (C6) — re-ingesting the same value five times does not strengthen the belief.
Reading the response
Section titled “Reading the response”IngestClaimResponse has three fields:
pub struct IngestClaimResponse { pub claim_ref: ClaimRef, // stable UUID reference pub disposition: Disposition, // one of the 12-state model variants pub contested_with: Vec<ClaimRef>, // non-empty when Contested or PendingConflict}Common dispositions after ingest:
| Disposition | Meaning |
|---|---|
CommittedCheap |
Fast-path commit — no conflict, external provenance |
CommittedInferred |
Non-conflicting ModelDerived claim — committed down-weighted, ineligible to overturn an existing belief until anchored to external evidence |
Contested |
Conflict with an existing claim; no winner; both preserved |
QueuedForAdjudication |
Oracle wired — conflict handed to the oracle queue |
Superseded |
This claim superseded an older one (valid-time succession) |
Quarantined |
Amplification Guard blocked a RecallReEntry echo |
For the full 12-state model see Concepts: Contested and Dispositions.
Querying memory
Section titled “Querying memory”use mempill_core::application::QueryMemoryRequest;
let result = engine.query_memory(QueryMemoryRequest { agent_id: AgentId("my-agent".into()), subject: "acme:ceo".into(), predicate: "held_by".into(), as_of_tx_time: None, // None = current moment (transaction-time axis) valid_at: None, // None = same instant as as_of_tx_time (valid-time axis)}).await?;
let belief = &result.belief;println!("status={:?}", belief.status);
if let Some(primary) = &belief.primary { println!("value={}", primary.fact.value); println!("value_conf={}", primary.confidence.value_confidence); // `valid_time` is not optional on `Belief` — start/end are the optional parts. println!("start={:?} end={:?}", primary.valid_time.start, primary.valid_time.end);}
// When status is Contested, primary is None and alternatives has all candidates:for alt in &belief.alternatives { println!("alternative: {}", alt.fact.value);}Point-in-time (time travel) query
Section titled “Point-in-time (time travel) query”Pass as_of_tx_time to read the engine’s belief as it was at a past transaction time:
use chrono::{Duration, Utc};
let seven_days_ago = Utc::now() - Duration::days(7);let result = engine.query_memory(QueryMemoryRequest { agent_id: agent.clone(), subject: "acme:ceo".into(), predicate: "held_by".into(), as_of_tx_time: Some(seven_days_ago), valid_at: None,}).await?;To independently select by real-world validity window (rather than transaction time),
set valid_at instead of, or together with, as_of_tx_time — see
Valid-Time Succession and
Query Patterns.
Reconcile
Section titled “Reconcile”reconcile re-runs conflict resolution for a set of (subject, predicate) subject lines.
Pass an empty subject_lines list to reconcile every subject line the agent owns.
Succession classification checks a new claim against every live claim on the subject line, not only the one in effect now. A claim overlapping any live incumbent is Contested instead of being committed as a succession. reconcile never writes supersessions; a contested line stays Contested until resolved through adjudication (submit_adjudication / sweep_adjudications). Repeated reconcile calls are idempotent.
use mempill_core::application::ReconcileRequest;
let result = engine.reconcile(ReconcileRequest { agent_id: AgentId("my-agent".into()), // Reconcile a specific subject line: subject_lines: vec![("acme:ceo".into(), "held_by".into())], // Or reconcile everything: subject_lines: vec![]}).await?;
println!("oracle_escalations={}", result.oracle_escalations);for (claim_ref, disposition) in &result.outcomes { println!(" {} -> {:?}", claim_ref, disposition);}When an oracle is wired, oracle_escalations is the count of subject lines that could not be
resolved without oracle input. Those lines are left QueuedForAdjudication.
Query audit
Section titled “Query audit”The audit ledger is an append-only record of every disposition event. Use query_audit for
provenance forensics, debugging, and compliance:
use mempill_core::application::AuditQueryRequest;
// Query full ledger for an agent (last 100 entries):let audit = engine.query_audit(AuditQueryRequest { agent_id: AgentId("my-agent".into()), claim_ref: None, // None = all claims from_tx_time: None, // None = from the beginning limit: 100,}).await?;
for entry in &audit.entries { println!( "{:?} {} ref={}", entry.event_kind, entry.recorded_at, entry.claim_ref );}Filter to a specific claim by supplying claim_ref:
let audit = engine.query_audit(AuditQueryRequest { agent_id: agent.clone(), claim_ref: Some(resp.claim_ref.clone()), from_tx_time: None, limit: 50,}).await?;LedgerEventKind variants include: ClaimCommitted, ValidityAsserted,
AdjudicationRequested, AdjudicationResolved, RecallReEntryDetected,
Quarantined, AdjudicationExpired.
from_tx_time is an inclusive lower bound, so a paged walk must de-duplicate by entry_id,
and pages should be large enough to hold all entries sharing one timestamp.
Ending a fact / recording a succession (0.4.0)
Section titled “Ending a fact / recording a succession (0.4.0)”When an open-ended claim needs to be closed and a new successor begins, use assert_validity with Bound to explicitly end the incumbent at a specific instant:
use mempill_core::{AssertValidityRequest, ValidityAssertionInput};
// Step 1: Record the initial open-ended claim (Alice as CEO, no end date).let r1 = engine.ingest_claim(IngestClaimRequest { agent_id: AgentId("my-agent".into()), subject: "acme:ceo".into(), predicate: "held_by".into(), value: serde_json::json!("Alice"), provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand), cardinality: Cardinality::Functional, valid_time: Some(ValidTime { start: Some("2020-01-01T00:00:00Z".parse()?), end: None, // open-ended valid_time_confidence: 0.95, start_granularity: None, end_granularity: None, }), confidence: Confidence { value_confidence: 0.95, valid_time_confidence: 0.95 }, criticality: Criticality::Medium, derived_from: vec![],}).await?;println!("Incumbent (Alice): {:?}", r1.disposition); // CommittedCheap
// Step 2: Later, explicitly end the open-ended fact at a specific instant (March 14, 2023).let bound_resp = engine.assert_validity(AssertValidityRequest { agent_id: AgentId("my-agent".into()), target: r1.claim_ref.clone(), assertion: ValidityAssertionInput::Bound { at: "2023-03-14T23:59:59Z".parse()?, }, provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand), confidence: Confidence { value_confidence: 0.95, valid_time_confidence: 0.95 },}).await?;println!("Bound at: {:?}", bound_resp.disposition); // Supersededprintln!("no_op: {} effective_at: {:?}", bound_resp.no_op, bound_resp.effective_at);
// Step 3: Ingest the successor (Bob as CEO, starting the day after the bound).let r2 = engine.ingest_claim(IngestClaimRequest { agent_id: AgentId("my-agent".into()), subject: "acme:ceo".into(), predicate: "held_by".into(), value: serde_json::json!("Bob"), provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand), cardinality: Cardinality::Functional, valid_time: Some(ValidTime { start: Some("2023-03-15T00:00:00Z".parse()?), end: None, // open-ended valid_time_confidence: 0.95, start_granularity: None, end_granularity: None, }), confidence: Confidence { value_confidence: 0.95, valid_time_confidence: 0.95 }, criticality: Criticality::Medium, derived_from: vec![],}).await?;println!("Successor (Bob): {:?}", r2.disposition); // CommittedCheap (clean succession)
// Step 4: Query the history to see the clean succession without conflict.let history = engine.history( AgentId("my-agent".into()), "acme:ceo".into(), "held_by".into(), None,).await?;
// entry.value is serde_json::Value (implements Display); valid_from / valid_until are// Option<DateTime<Utc>> (no Display impl — use Debug or .map()/.unwrap_or() to print).for entry in &history.entries { println!( "{:12} {:20?} {:20?} {:?}", entry.value, entry.valid_from, entry.valid_until, entry.status );}// Output (example):// Alice Some(2020-01-01T00:00:00Z) Some(2023-03-14T23:59:59Z) Superseded// Bob Some(2023-03-15T00:00:00Z) None CurrentKey points:
assert_validitytakes an explicitClaimRef(the incumbent’s UUID);AssertValidityRequest.confidenceis required (no default).- The bound is a validity assertion — a separate ledger record that does not modify the original claim. It is applied at read time.
- After bounding the incumbent, a later non-overlapping claim folds as a clean
Succession(no oracle needed, no conflict). - History status changes: an incumbent explicitly bounded via
assert_validity/end_factis no longer live, so it reportsSuperseded(notEnded) — the same status a claim gets when a later non-overlapping successor closes it.Endedis reserved for a claim whose own valid-time window expires with no explicit bound and no live successor. The successor reportsCurrent. - Do not write a bounded duplicate (e.g., re-ingest Alice with an explicit end): that creates two overlapping claims on the same line, causing
Contestedand polluting the belief until resolved. - Errors:
MemError::InsufficientProvenanceForOverturn(provenance notExternal(*)),MemError::ClaimNotFound(target missing / wrong agent),MemError::IncoherentTemporalWindow(atprecedes the claim’s ownvalid_time.start),MemError::AlreadyBound(a different bound already active ontarget— reachable only viaassert_validitycalled directly, never viaend_fact). - Ergonomic alternative (when you have only the subject-line): use the facade
mempill::end_fact(engine, agent_id, subject, predicate, at, opts)— all string-ish args areimpl Into<String>,atis a lenient date string,opts: EndFactOptions { provenance: Option<ProvenanceLabel>, confidence: Option<f32> }. It resolves the incumbent claim internally by(subject, predicate)via the same canonical fold asrecall/history(resolve_live_claim_for_line_ergo), returningEndFactReceipt { claim_ref, disposition, effective_at, no_op }. Its errors areMempillDxError::NoLiveClaimForClose(0 live claims — including a repeat call on an already-closed line, which is not a no-op) andMempillDxError::Engine(MemError::AmbiguousLineForClose)(>1 live claims).MemError::AlreadyBoundis unreachable throughend_fact— resolution only ever targets a live claim.
Reopening a bounded fact
Section titled “Reopening a bounded fact”assert_validity (raw or facade passthrough) is the only way to reopen — end_fact only binds:
use mempill::assert_validity;
let reopen_resp = assert_validity(&engine, AssertValidityRequest { agent_id: AgentId("my-agent".into()), target: bound_resp.claim_ref.clone(), assertion: ValidityAssertionInput::Reopen, provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand), confidence: Confidence { value_confidence: 0.95, valid_time_confidence: 0.95 },}).await?;println!("{:?}", reopen_resp.disposition); // Reinstatedresolve_live_claim_for_line_ergo(agent_id, subject, predicate) (the lookup end_fact uses internally) is also available directly on CanAssertValidity, returning LiveClaimResolution::{Empty, Single(ClaimRef), Ambiguous(usize)}.
Valid-time succession
Section titled “Valid-time succession”When you supply non-overlapping, high-confidence valid-time windows, the reconciler
classifies the relationship as a clean temporal succession (internally Succession,
a distinct classification from a genuine conflict) and commits both claims without
conflict. The fold then answers temporal queries like “who was CEO in February?”:
// Alice: 2020–2023 (open-ended in practice; Bob supersedes)engine.ingest_claim(IngestClaimRequest { subject: "acme:ceo".into(), predicate: "held_by".into(), value: serde_json::json!("Alice"), provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand), cardinality: Cardinality::Functional, valid_time: Some(ValidTime { start: Some("2020-01-01T00:00:00Z".parse()?), end: Some("2023-03-14T23:59:59Z".parse()?), valid_time_confidence: 0.95, start_granularity: None, end_granularity: None, }), confidence: Confidence { value_confidence: 0.95, valid_time_confidence: 0.95 }, // ...}).await?;
// Bob: from 2023-03-15 onwardengine.ingest_claim(IngestClaimRequest { subject: "acme:ceo".into(), predicate: "held_by".into(), value: serde_json::json!("Bob"), valid_time: Some(ValidTime { start: Some("2023-03-15T00:00:00Z".parse()?), end: None, valid_time_confidence: 0.95, start_granularity: None, end_granularity: None, }), // ...}).await?;
// Query now -> Bob; query 2022 -> Alice.The succession shortcut fires when: every valid-time window has valid_time_confidence ≥ 0.7,
windows are bounded and pairwise non-overlapping, and there is exactly one live incumbent.
Overlapping or low-confidence windows fall back to Contested.
See Concepts: Valid-Time Succession.
Cargo dependencies
Section titled “Cargo dependencies”[dependencies]# SQLite topology-a (most common starting point):mempill = "0.4" # facade: re-exports mempill-core + mempill-sqlite
# Postgres topology-b (multi-agent shared database): replace mempill with:# mempill-postgres = "0.4"# mempill-core = "0.4"
tokio = { version = "1", features = ["full"] }serde_json = "1"anyhow = "1"chrono = { version = "0.4", features = ["serde"] }All crates are published on crates.io at 0.4.0, including
the per-agent SQLite entry points and PoolConfig used throughout this guide. See
Changelog for version history.
Related guides
Section titled “Related guides”- Quickstart (Rust) — five-minute intro
- Writing an Oracle — implementing the
OraclePorttrait - PostgreSQL Backend — topology-b setup
- Query Patterns — belief shape, Contested handling, audit patterns