use crate::node::{AinlMemoryNode, AinlNodeType, MemoryCategory, SemanticNode};
use crate::GraphMemory;
use uuid::Uuid;
pub const ANCHORED_SUMMARY_TAG: &str = "anchored_summary";
const ANCHORED_SUMMARY_NS: Uuid = Uuid::from_bytes([
0x9e, 0x4f, 0x4d, 0xb8, 0x1c, 0x1c, 0x4a, 0x6e, 0xa0, 0x77, 0xe2, 0x55, 0x12, 0x67, 0x3d, 0x2c,
]);
#[must_use]
pub fn anchored_summary_id(agent_id: &str) -> Uuid {
Uuid::new_v5(&ANCHORED_SUMMARY_NS, agent_id.as_bytes())
}
impl GraphMemory {
pub fn upsert_anchored_summary(
&self,
agent_id: &str,
summary_payload: &str,
) -> Result<Uuid, String> {
let id = anchored_summary_id(agent_id);
let semantic = SemanticNode {
fact: summary_payload.to_string(),
confidence: 1.0,
source_turn_id: id,
topic_cluster: None,
source_episode_id: String::new(),
contradiction_ids: Vec::new(),
last_referenced_at: chrono::Utc::now().timestamp() as u64,
reference_count: 0,
decay_eligible: false,
tags: vec![ANCHORED_SUMMARY_TAG.to_string()],
recurrence_count: 0,
last_ref_snapshot: 0,
};
let node = AinlMemoryNode {
id,
memory_category: MemoryCategory::Semantic,
importance_score: 1.0,
agent_id: agent_id.to_string(),
project_id: None,
node_type: AinlNodeType::Semantic { semantic },
edges: Vec::new(),
plugin_data: None,
};
self.write_node(&node)?;
Ok(id)
}
pub fn fetch_anchored_summary(&self, agent_id: &str) -> Result<Option<String>, String> {
let id = anchored_summary_id(agent_id);
let node = self.store().read_node(id)?;
Ok(node.and_then(|n| match n.node_type {
AinlNodeType::Semantic { semantic } => Some(semantic.fact),
_ => None,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anchored_summary_id_is_stable_per_agent() {
let a = anchored_summary_id("agent-alpha");
let b = anchored_summary_id("agent-alpha");
let c = anchored_summary_id("agent-beta");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn upsert_then_fetch_roundtrip() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("anchored_summary_smoke.db");
let memory = GraphMemory::new(&db_path).expect("graph memory");
let payload_v1 = r#"{"schema_version":1,"sections":[{"id":"intent","label":"Intent","content":"v1"}],"token_estimate":1,"iteration":1}"#;
let id1 = memory
.upsert_anchored_summary("agent-rt", payload_v1)
.expect("upsert v1");
let fetched_v1 = memory
.fetch_anchored_summary("agent-rt")
.expect("fetch v1")
.expect("payload present");
assert_eq!(fetched_v1, payload_v1);
let payload_v2 = r#"{"schema_version":1,"sections":[{"id":"intent","label":"Intent","content":"v2"}],"token_estimate":1,"iteration":2}"#;
let id2 = memory
.upsert_anchored_summary("agent-rt", payload_v2)
.expect("upsert v2");
assert_eq!(id1, id2, "same agent must reuse the same node id");
let fetched_v2 = memory
.fetch_anchored_summary("agent-rt")
.expect("fetch v2")
.expect("payload present");
assert_eq!(fetched_v2, payload_v2);
}
#[test]
fn fetch_missing_returns_none() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("anchored_summary_missing.db");
let memory = GraphMemory::new(&db_path).expect("graph memory");
let result = memory
.fetch_anchored_summary("nonexistent-agent")
.expect("query ok");
assert!(result.is_none());
}
#[test]
fn distinct_agents_isolated() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("anchored_summary_isolation.db");
let memory = GraphMemory::new(&db_path).expect("graph memory");
memory
.upsert_anchored_summary("agent-a", r#"{"a":1}"#)
.expect("upsert a");
memory
.upsert_anchored_summary("agent-b", r#"{"b":2}"#)
.expect("upsert b");
let a = memory.fetch_anchored_summary("agent-a").unwrap().unwrap();
let b = memory.fetch_anchored_summary("agent-b").unwrap().unwrap();
assert_eq!(a, r#"{"a":1}"#);
assert_eq!(b, r#"{"b":2}"#);
}
}