ainl_memory/
anchored_summary.rs1use crate::node::{AinlMemoryNode, AinlNodeType, MemoryCategory, SemanticNode};
16use crate::GraphMemory;
17use uuid::Uuid;
18
19pub const ANCHORED_SUMMARY_TAG: &str = "anchored_summary";
21
22const ANCHORED_SUMMARY_NS: Uuid = Uuid::from_bytes([
26 0x9e, 0x4f, 0x4d, 0xb8, 0x1c, 0x1c, 0x4a, 0x6e, 0xa0, 0x77, 0xe2, 0x55, 0x12, 0x67, 0x3d, 0x2c,
27]);
28
29#[must_use]
33pub fn anchored_summary_id(agent_id: &str) -> Uuid {
34 Uuid::new_v5(&ANCHORED_SUMMARY_NS, agent_id.as_bytes())
35}
36
37impl GraphMemory {
38 pub fn upsert_anchored_summary(
44 &self,
45 agent_id: &str,
46 summary_payload: &str,
47 ) -> Result<Uuid, String> {
48 let id = anchored_summary_id(agent_id);
49 let semantic = SemanticNode {
50 fact: summary_payload.to_string(),
51 confidence: 1.0,
52 source_turn_id: id,
53 topic_cluster: None,
54 source_episode_id: String::new(),
55 contradiction_ids: Vec::new(),
56 last_referenced_at: chrono::Utc::now().timestamp() as u64,
57 reference_count: 0,
58 decay_eligible: false,
59 tags: vec![ANCHORED_SUMMARY_TAG.to_string()],
60 recurrence_count: 0,
61 last_ref_snapshot: 0,
62 };
63 let node = AinlMemoryNode {
64 id,
65 memory_category: MemoryCategory::Semantic,
66 importance_score: 1.0,
67 agent_id: agent_id.to_string(),
68 project_id: None,
69 node_type: AinlNodeType::Semantic { semantic },
70 edges: Vec::new(),
71 };
72 self.write_node(&node)?;
73 Ok(id)
74 }
75
76 pub fn fetch_anchored_summary(&self, agent_id: &str) -> Result<Option<String>, String> {
80 let id = anchored_summary_id(agent_id);
81 let node = self.store().read_node(id)?;
82 Ok(node.and_then(|n| match n.node_type {
83 AinlNodeType::Semantic { semantic } => Some(semantic.fact),
84 _ => None,
85 }))
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn anchored_summary_id_is_stable_per_agent() {
95 let a = anchored_summary_id("agent-alpha");
96 let b = anchored_summary_id("agent-alpha");
97 let c = anchored_summary_id("agent-beta");
98 assert_eq!(a, b);
99 assert_ne!(a, c);
100 }
101
102 #[test]
103 fn upsert_then_fetch_roundtrip() {
104 let dir = tempfile::tempdir().expect("tempdir");
105 let db_path = dir.path().join("anchored_summary_smoke.db");
106 let memory = GraphMemory::new(&db_path).expect("graph memory");
107
108 let payload_v1 = r#"{"schema_version":1,"sections":[{"id":"intent","label":"Intent","content":"v1"}],"token_estimate":1,"iteration":1}"#;
110 let id1 = memory
111 .upsert_anchored_summary("agent-rt", payload_v1)
112 .expect("upsert v1");
113 let fetched_v1 = memory
114 .fetch_anchored_summary("agent-rt")
115 .expect("fetch v1")
116 .expect("payload present");
117 assert_eq!(fetched_v1, payload_v1);
118
119 let payload_v2 = r#"{"schema_version":1,"sections":[{"id":"intent","label":"Intent","content":"v2"}],"token_estimate":1,"iteration":2}"#;
121 let id2 = memory
122 .upsert_anchored_summary("agent-rt", payload_v2)
123 .expect("upsert v2");
124 assert_eq!(id1, id2, "same agent must reuse the same node id");
125 let fetched_v2 = memory
126 .fetch_anchored_summary("agent-rt")
127 .expect("fetch v2")
128 .expect("payload present");
129 assert_eq!(fetched_v2, payload_v2);
130 }
131
132 #[test]
133 fn fetch_missing_returns_none() {
134 let dir = tempfile::tempdir().expect("tempdir");
135 let db_path = dir.path().join("anchored_summary_missing.db");
136 let memory = GraphMemory::new(&db_path).expect("graph memory");
137 let result = memory
138 .fetch_anchored_summary("nonexistent-agent")
139 .expect("query ok");
140 assert!(result.is_none());
141 }
142
143 #[test]
144 fn distinct_agents_isolated() {
145 let dir = tempfile::tempdir().expect("tempdir");
146 let db_path = dir.path().join("anchored_summary_isolation.db");
147 let memory = GraphMemory::new(&db_path).expect("graph memory");
148 memory
149 .upsert_anchored_summary("agent-a", r#"{"a":1}"#)
150 .expect("upsert a");
151 memory
152 .upsert_anchored_summary("agent-b", r#"{"b":2}"#)
153 .expect("upsert b");
154 let a = memory.fetch_anchored_summary("agent-a").unwrap().unwrap();
155 let b = memory.fetch_anchored_summary("agent-b").unwrap().unwrap();
156 assert_eq!(a, r#"{"a":1}"#);
157 assert_eq!(b, r#"{"b":2}"#);
158 }
159}