use std::sync::Arc;
use mnemo_core::embedding::NoopEmbedding;
use mnemo_core::index::usearch::UsearchIndex;
use mnemo_core::query::MnemoEngine;
use mnemo_core::query::forget::{ForgetRequest, ForgetStrategy};
use mnemo_core::query::recall::RecallRequest;
use mnemo_core::query::remember::RememberRequest;
use mnemo_core::search::tantivy_index::TantivyFullTextIndex;
const AGENT: &str = "automem-eval-agent";
const AGENT_MANAGED_TAG: &str = "agent-managed";
const N_TRACKED: usize = 12;
const N_VERSIONS: usize = 3;
const N_INCIDENTAL: usize = 12;
fn build_engine() -> MnemoEngine {
let storage =
Arc::new(mnemo_core::storage::duckdb::DuckDbStorage::open_in_memory().expect("duckdb"));
let index = Arc::new(UsearchIndex::new(8).expect("usearch"));
let embedding = Arc::new(NoopEmbedding::new(8));
let ft = Arc::new(TantivyFullTextIndex::open_in_memory().expect("tantivy"));
MnemoEngine::new(storage, index, embedding, AGENT.to_string(), None).with_full_text(ft)
}
fn tracked_content(i: usize, ver: usize) -> String {
format!("TOPIC{i:02} status update: the recorded value is VAL{i:02}v{ver}")
}
fn incidental_content(j: usize) -> String {
format!("DETAIL{j:02} note: incidental session info MISC{j:02}")
}
async fn write_plain(engine: &MnemoEngine, content: String, tags: Vec<String>) -> uuid::Uuid {
let mut req = RememberRequest::new(content);
if !tags.is_empty() {
req.tags = Some(tags);
}
engine.remember(req).await.expect("remember").id
}
async fn seed_pipeline(engine: &MnemoEngine) {
for i in 0..N_TRACKED {
for ver in 1..=N_VERSIONS {
write_plain(engine, tracked_content(i, ver), vec![]).await;
}
}
for j in 0..N_INCIDENTAL {
write_plain(engine, incidental_content(j), vec![]).await;
}
}
async fn seed_agent_managed(engine: &MnemoEngine) {
let tag = vec![AGENT_MANAGED_TAG.to_string()];
for i in 0..N_TRACKED {
let mut prev = write_plain(engine, tracked_content(i, 1), tag.clone()).await;
for ver in 2..=N_VERSIONS {
let mut fr = ForgetRequest::new(vec![prev]);
fr.strategy = Some(ForgetStrategy::SoftDelete);
engine.forget(fr).await.expect("forget");
prev = write_plain(engine, tracked_content(i, ver), tag.clone()).await;
}
}
}
async fn recall(engine: &MnemoEngine, query: &str, k: usize, agent_scoped: bool) -> Vec<String> {
let mut req = RecallRequest::new(query.to_string());
req.limit = Some(k);
req.strategy = Some("lexical".to_string());
if agent_scoped {
req.tags = Some(vec![AGENT_MANAGED_TAG.to_string()]);
}
engine
.recall(req)
.await
.expect("recall")
.memories
.into_iter()
.map(|m| m.content)
.collect()
}
#[derive(Default)]
struct Metrics {
recall_sum: f64,
precision_sum: f64,
n: usize,
}
impl Metrics {
fn add(&mut self, recall: f64, precision: f64) {
self.recall_sum += recall;
self.precision_sum += precision;
self.n += 1;
}
fn recall(&self) -> f64 {
self.recall_sum / self.n.max(1) as f64
}
fn precision(&self) -> f64 {
self.precision_sum / self.n.max(1) as f64
}
fn f1(&self) -> f64 {
let (p, r) = (self.precision(), self.recall());
if p + r == 0.0 {
0.0
} else {
2.0 * p * r / (p + r)
}
}
}
async fn current_fact_metrics(engine: &MnemoEngine, agent_scoped: bool) -> Metrics {
let mut m = Metrics::default();
for i in 0..N_TRACKED {
let gold = format!("VAL{i:02}v{N_VERSIONS}");
let hits = recall(engine, &format!("TOPIC{i:02}"), N_VERSIONS, agent_scoped).await;
let retrieved = hits.len().max(1);
let correct = hits.iter().filter(|c| c.contains(&gold)).count();
let recall = if hits.iter().any(|c| c.contains(&gold)) {
1.0
} else {
0.0
};
let precision = correct as f64 / retrieved as f64;
m.add(recall, precision);
}
m
}
async fn incidental_recall(engine: &MnemoEngine, agent_scoped: bool) -> Metrics {
let mut m = Metrics::default();
for j in 0..N_INCIDENTAL {
let gold = format!("MISC{j:02}");
let hits = recall(engine, &format!("DETAIL{j:02}"), 1, agent_scoped).await;
let recall = if hits.iter().any(|c| c.contains(&gold)) {
1.0
} else {
0.0
};
m.add(recall, recall);
}
m
}
#[tokio::test]
async fn agent_managed_vs_pipeline_crossover() {
let pipeline = build_engine();
seed_pipeline(&pipeline).await;
let pipe_current = current_fact_metrics(&pipeline, false).await;
let pipe_incidental = incidental_recall(&pipeline, false).await;
let agent = build_engine();
seed_agent_managed(&agent).await;
let agent_current = current_fact_metrics(&agent, true).await;
let agent_incidental = incidental_recall(&agent, true).await;
println!("\n=== AutoMEM crossover eval (arXiv:2606.04315) ===");
println!(
"fixture: {N_TRACKED} tracked facts × {N_VERSIONS} revisions, {N_INCIDENTAL} incidental details"
);
println!("| query family | mode | recall@k | precision | F1 |");
println!("|-----------------------------|-----------------|---------:|----------:|------:|");
println!(
"| long-horizon current-fact | fixed-pipeline | {:>8.3} | {:>9.3} | {:>5.3} |",
pipe_current.recall(),
pipe_current.precision(),
pipe_current.f1()
);
println!(
"| long-horizon current-fact | agent-managed | {:>8.3} | {:>9.3} | {:>5.3} |",
agent_current.recall(),
agent_current.precision(),
agent_current.f1()
);
println!(
"| single-shot incidental | fixed-pipeline | {:>8.3} | {:>9.3} | {:>5.3} |",
pipe_incidental.recall(),
pipe_incidental.precision(),
pipe_incidental.f1()
);
println!(
"| single-shot incidental | agent-managed | {:>8.3} | {:>9.3} | {:>5.3} |",
agent_incidental.recall(),
agent_incidental.precision(),
agent_incidental.f1()
);
assert!(
agent_current.f1() > pipe_current.f1(),
"agent-managed current-fact F1 ({:.3}) must beat fixed-pipeline ({:.3})",
agent_current.f1(),
pipe_current.f1()
);
assert!(
pipe_incidental.recall() > agent_incidental.recall(),
"fixed-pipeline incidental recall ({:.3}) must beat agent-managed ({:.3})",
pipe_incidental.recall(),
agent_incidental.recall()
);
}