ainl_memory/lib.rs
1//! AINL Memory - Graph-based agent memory substrate
2//!
3//! **Graph-as-memory for AI agents. Execution IS the memory.**
4//!
5//! AINL Memory implements agent memory as an execution graph. Every agent turn,
6//! tool call, and delegation becomes a typed graph node. No separate retrieval
7//! layer—the graph itself is the memory.
8//!
9//! # Quick Start
10//!
11//! ```no_run
12//! use ainl_memory::GraphMemory;
13//! use std::path::Path;
14//!
15//! let memory = GraphMemory::new(Path::new("memory.db")).unwrap();
16//!
17//! // Record an episode
18//! memory.write_episode(
19//! vec!["file_read".to_string(), "agent_delegate".to_string()],
20//! Some("agent-B".to_string()),
21//! None,
22//! ).unwrap();
23//!
24//! // Recall recent episodes
25//! let recent = memory.recall_recent(100).unwrap();
26//! ```
27//!
28//! # Architecture
29//!
30//! AINL Memory is designed as infrastructure that any agent framework can adopt:
31//! - Zero dependencies on specific agent runtimes
32//! - Simple trait-based API via `GraphStore`
33//! - Bring your own storage backend
34//!
35//! ## Node Types
36//!
37//! - **Episode**: What happened during an agent turn (tool calls, delegations)
38//! - **Semantic**: Facts learned with confidence scores
39//! - **Procedural**: Reusable compiled workflow patterns
40//! - **Persona**: Agent traits learned over time
41
42pub mod node;
43pub mod query;
44pub mod store;
45
46pub use node::{AinlEdge, AinlMemoryNode, AinlNodeType};
47pub use query::{find_high_confidence_facts, find_patterns, find_strong_traits, recall_recent, walk_from};
48pub use store::{GraphStore, SqliteGraphStore};
49
50use uuid::Uuid;
51
52/// High-level graph memory API - the main entry point for AINL memory.
53///
54/// Wraps a GraphStore implementation with a simplified 5-method API.
55pub struct GraphMemory {
56 store: SqliteGraphStore,
57}
58
59impl GraphMemory {
60 /// Create a new graph memory at the given database path.
61 ///
62 /// This will create the database file if it doesn't exist, and
63 /// ensure the AINL graph schema is initialized.
64 pub fn new(db_path: &std::path::Path) -> Result<Self, String> {
65 let store = SqliteGraphStore::open(db_path)?;
66 Ok(Self { store })
67 }
68
69 /// Create from an existing SQLite connection (for integration with existing memory pools)
70 pub fn from_connection(conn: rusqlite::Connection) -> Result<Self, String> {
71 let store = SqliteGraphStore::from_connection(conn)?;
72 Ok(Self { store })
73 }
74
75 /// Write an episode node (what happened during an agent turn).
76 ///
77 /// # Arguments
78 /// * `tool_calls` - List of tools executed during this turn
79 /// * `delegation_to` - Agent ID this turn delegated to (if any)
80 /// * `trace_event` - Optional orchestration trace event (serialized JSON)
81 ///
82 /// # Returns
83 /// The ID of the created episode node
84 pub fn write_episode(
85 &self,
86 tool_calls: Vec<String>,
87 delegation_to: Option<String>,
88 trace_event: Option<serde_json::Value>,
89 ) -> Result<Uuid, String> {
90 let turn_id = Uuid::new_v4();
91 let timestamp = chrono::Utc::now().timestamp();
92
93 let node = AinlMemoryNode::new_episode(
94 turn_id,
95 timestamp,
96 tool_calls,
97 delegation_to,
98 trace_event,
99 );
100
101 let node_id = node.id;
102 self.store.write_node(&node)?;
103 Ok(node_id)
104 }
105
106 /// Write a semantic fact (learned information with confidence).
107 ///
108 /// # Arguments
109 /// * `fact` - The fact in natural language
110 /// * `confidence` - Confidence score (0.0-1.0)
111 /// * `source_turn_id` - Turn ID that generated this fact
112 ///
113 /// # Returns
114 /// The ID of the created semantic node
115 pub fn write_fact(
116 &self,
117 fact: String,
118 confidence: f32,
119 source_turn_id: Uuid,
120 ) -> Result<Uuid, String> {
121 let node = AinlMemoryNode::new_fact(fact, confidence, source_turn_id);
122 let node_id = node.id;
123 self.store.write_node(&node)?;
124 Ok(node_id)
125 }
126
127 /// Store a procedural pattern (compiled workflow).
128 ///
129 /// # Arguments
130 /// * `pattern_name` - Name/identifier for the pattern
131 /// * `compiled_graph` - Binary representation of the compiled graph
132 ///
133 /// # Returns
134 /// The ID of the created procedural node
135 pub fn store_pattern(
136 &self,
137 pattern_name: String,
138 compiled_graph: Vec<u8>,
139 ) -> Result<Uuid, String> {
140 let node = AinlMemoryNode::new_pattern(pattern_name, compiled_graph);
141 let node_id = node.id;
142 self.store.write_node(&node)?;
143 Ok(node_id)
144 }
145
146 /// Recall recent episodes (within the last N seconds).
147 ///
148 /// # Arguments
149 /// * `seconds_ago` - Only return episodes from the last N seconds
150 ///
151 /// # Returns
152 /// Vector of episode nodes, most recent first
153 pub fn recall_recent(&self, seconds_ago: i64) -> Result<Vec<AinlMemoryNode>, String> {
154 let since = chrono::Utc::now().timestamp() - seconds_ago;
155 self.store.query_episodes_since(since, 100)
156 }
157
158 /// Get direct access to the underlying store for advanced queries
159 pub fn store(&self) -> &dyn GraphStore {
160 &self.store
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_graph_memory_api() {
170 let temp_dir = std::env::temp_dir();
171 let db_path = temp_dir.join("ainl_lib_test.db");
172 let _ = std::fs::remove_file(&db_path);
173
174 let memory = GraphMemory::new(&db_path).expect("Failed to create memory");
175
176 // Write an episode
177 let episode_id = memory
178 .write_episode(
179 vec!["file_read".to_string(), "agent_delegate".to_string()],
180 Some("agent-B".to_string()),
181 None,
182 )
183 .expect("Failed to write episode");
184
185 assert_ne!(episode_id, Uuid::nil());
186
187 // Write a fact
188 let fact_id = memory
189 .write_fact(
190 "User prefers concise responses".to_string(),
191 0.85,
192 episode_id,
193 )
194 .expect("Failed to write fact");
195
196 assert_ne!(fact_id, Uuid::nil());
197
198 // Recall recent episodes
199 let recent = memory.recall_recent(60).expect("Failed to recall");
200 assert_eq!(recent.len(), 1);
201
202 // Verify the episode content
203 if let AinlNodeType::Episode {
204 delegation_to,
205 tool_calls,
206 ..
207 } = &recent[0].node_type
208 {
209 assert_eq!(delegation_to, &Some("agent-B".to_string()));
210 assert_eq!(tool_calls.len(), 2);
211 } else {
212 panic!("Wrong node type");
213 }
214 }
215
216 #[test]
217 fn test_store_pattern() {
218 let temp_dir = std::env::temp_dir();
219 let db_path = temp_dir.join("ainl_lib_test_pattern.db");
220 let _ = std::fs::remove_file(&db_path);
221
222 let memory = GraphMemory::new(&db_path).expect("Failed to create memory");
223
224 let pattern_id = memory
225 .store_pattern("research_workflow".to_string(), vec![1, 2, 3, 4])
226 .expect("Failed to store pattern");
227
228 assert_ne!(pattern_id, Uuid::nil());
229
230 // Query it back
231 let patterns = find_patterns(memory.store(), "research").expect("Query failed");
232 assert_eq!(patterns.len(), 1);
233 }
234}