Rust (Advanced)
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, open_default_in_memory};
// File-backed — durable across restarts.let engine = open_default("/path/to/agent.db")?;
// 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, open_with_oracle_in_memory};use std::sync::Arc;
// Implement OraclePort for your resolver:let engine = open_with_oracle("/path/to/agent.db", 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) }),
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 |
Committed after reconciliation narrowed to a winner |
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}).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); if let Some(vt) = &primary.valid_time { println!("start={:?} end={:?}", vt.start, vt.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),}).await?;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.
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.
Valid-time succession
Section titled “Valid-time succession”When you supply non-overlapping, high-confidence valid-time windows, the reconciler
classifies the relationship as ConflictType::Succession 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()?), }), 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, }), // ...}).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.2" # facade: re-exports mempill-core + mempill-sqlite
# Postgres topology-b (multi-agent shared database): replace mempill with:# mempill-postgres = "0.2"# mempill-core = "0.2"
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.2.0. 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