Skip to main content

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}