diff --git a/Cargo.lock b/Cargo.lock index 179f381..0a0b6e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,7 @@ dependencies = [ [[package]] name = "aingle_ai" -version = "0.4.0" +version = "0.4.1" dependencies = [ "blake2", "candle-core 0.9.2", @@ -108,7 +108,7 @@ dependencies = [ [[package]] name = "aingle_contracts" -version = "0.4.0" +version = "0.4.1" dependencies = [ "blake3", "dashmap 6.1.0", @@ -127,7 +127,7 @@ dependencies = [ [[package]] name = "aingle_cortex" -version = "0.4.0" +version = "0.4.1" dependencies = [ "aingle_graph", "aingle_logic", @@ -173,7 +173,7 @@ dependencies = [ [[package]] name = "aingle_graph" -version = "0.4.0" +version = "0.4.1" dependencies = [ "bincode", "blake3", @@ -194,7 +194,7 @@ dependencies = [ [[package]] name = "aingle_logic" -version = "0.4.0" +version = "0.4.1" dependencies = [ "aingle_graph", "chrono", @@ -210,7 +210,7 @@ dependencies = [ [[package]] name = "aingle_minimal" -version = "0.4.0" +version = "0.4.1" dependencies = [ "async-io", "async-tungstenite", @@ -252,7 +252,7 @@ dependencies = [ [[package]] name = "aingle_viz" -version = "0.4.0" +version = "0.4.1" dependencies = [ "aingle_graph", "aingle_minimal", @@ -274,7 +274,7 @@ dependencies = [ [[package]] name = "aingle_zk" -version = "0.4.0" +version = "0.4.1" dependencies = [ "blake3", "bulletproofs", @@ -3977,8 +3977,9 @@ dependencies = [ [[package]] name = "ineru" -version = "0.4.0" +version = "0.4.1" dependencies = [ + "bincode", "blake3", "chrono", "criterion", @@ -4235,7 +4236,7 @@ dependencies = [ [[package]] name = "kaneru" -version = "0.4.0" +version = "0.4.1" dependencies = [ "chrono", "criterion", diff --git a/crates/ai_hash/Cargo.toml b/crates/ai_hash/Cargo.toml index 3577cd9..62d9091 100644 --- a/crates/ai_hash/Cargo.toml +++ b/crates/ai_hash/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ai_hash" -version = "0.4.0" +version = "0.4.1" authors = ["Apilium Technologies "] keywords = [ "aingle", "ai", "hash", "blake", "blake2b" ] categories = [ "cryptography" ] diff --git a/crates/aingle/Cargo.toml b/crates/aingle/Cargo.toml index 8224fd8..6b15d28 100644 --- a/crates/aingle/Cargo.toml +++ b/crates/aingle/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle" -version = "0.4.0" +version = "0.4.1" description = "AIngle, a framework for distributed applications" license = "Apache-2.0 OR LicenseRef-Commercial" homepage = "https://apilium.com" diff --git a/crates/aingle_ai/Cargo.toml b/crates/aingle_ai/Cargo.toml index d8a10f5..feeae23 100644 --- a/crates/aingle_ai/Cargo.toml +++ b/crates/aingle_ai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_ai" -version = "0.4.0" +version = "0.4.1" description = "AI integration layer for AIngle - Ineru, Nested Learning, Kaneru" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_contracts/Cargo.toml b/crates/aingle_contracts/Cargo.toml index e3f07cc..015e475 100644 --- a/crates/aingle_contracts/Cargo.toml +++ b/crates/aingle_contracts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_contracts" -version = "0.4.0" +version = "0.4.1" description = "Smart Contracts DSL and WASM Runtime for AIngle" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_cortex/Cargo.toml b/crates/aingle_cortex/Cargo.toml index 03ecbee..fa2f8a2 100644 --- a/crates/aingle_cortex/Cargo.toml +++ b/crates/aingle_cortex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_cortex" -version = "0.4.0" +version = "0.4.1" description = "Córtex API - REST/GraphQL/SPARQL interface for AIngle semantic graphs" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -18,7 +18,7 @@ rest = [] graphql = ["dep:async-graphql", "dep:async-graphql-axum"] sparql = ["dep:spargebra"] auth = ["dep:jsonwebtoken", "dep:argon2"] -p2p = ["dep:quinn", "dep:rustls", "dep:rcgen", "dep:ed25519-dalek", "dep:hex", "dep:dirs"] +p2p = ["dep:quinn", "dep:rustls", "dep:rcgen", "dep:ed25519-dalek", "dep:hex"] p2p-mdns = ["p2p", "dep:mdns-sd", "dep:if-addrs"] full = ["rest", "graphql", "sparql", "auth"] @@ -28,7 +28,7 @@ path = "src/main.rs" [dependencies] # Core AIngle crates -aingle_graph = { version = "0.4", path = "../aingle_graph" } +aingle_graph = { version = "0.4", path = "../aingle_graph", features = ["sled-backend"] } aingle_logic = { version = "0.4", path = "../aingle_logic" } aingle_zk = { version = "0.4", path = "../aingle_zk" } ineru = { version = "0.4", path = "../ineru" } @@ -92,7 +92,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std" rcgen = { version = "0.13", optional = true } ed25519-dalek = { version = "2", features = ["rand_core"], optional = true } hex = { version = "0.4", optional = true } -dirs = { version = "6", optional = true } +dirs = "6" mdns-sd = { version = "0.18", optional = true } if-addrs = { version = "0.13", optional = true } diff --git a/crates/aingle_cortex/src/main.rs b/crates/aingle_cortex/src/main.rs index 1d5b1fe..a0875e2 100644 --- a/crates/aingle_cortex/src/main.rs +++ b/crates/aingle_cortex/src/main.rs @@ -52,6 +52,15 @@ async fn main() -> Result<(), Box> { "--public" => { config.host = "0.0.0.0".to_string(); } + "--db" => { + if i + 1 < args.len() { + config.db_path = Some(args[i + 1].clone()); + i += 1; + } + } + "--memory" => { + config.db_path = Some(":memory:".to_string()); + } "--help" => { print_help(); return Ok(()); @@ -72,10 +81,24 @@ async fn main() -> Result<(), Box> { p2p }; + // Resolve the snapshot directory for Ineru persistence + let snapshot_dir = match &config.db_path { + Some(p) if p == ":memory:" => None, + Some(p) => std::path::Path::new(p).parent().map(|p| p.to_path_buf()), + None => { + let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from(".")); + Some(home.join(".aingle").join("cortex")) + } + }; + // Create and run server #[allow(unused_mut)] let mut server = CortexServer::new(config)?; + // Keep a reference to the state for shutdown flush + let state_for_shutdown = server.state().clone(); + let snapshot_dir_for_shutdown = snapshot_dir.clone(); + // Start P2P manager if enabled. #[cfg(feature = "p2p")] if p2p_config.enabled { @@ -96,12 +119,39 @@ async fn main() -> Result<(), Box> { } } - // Set up graceful shutdown - let shutdown_signal = async { - tokio::signal::ctrl_c() + // Set up graceful shutdown with data flush (handles both SIGINT and SIGTERM) + let shutdown_signal = async move { + let ctrl_c = tokio::signal::ctrl_c(); + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Failed to install SIGTERM handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => { + tracing::info!("SIGINT received — flushing data..."); + } + _ = terminate => { + tracing::info!("SIGTERM received — flushing data..."); + } + } + + // Flush graph database and save Ineru snapshot + if let Err(e) = state_for_shutdown + .flush(snapshot_dir_for_shutdown.as_deref()) .await - .expect("Failed to install CTRL+C handler"); - tracing::info!("Shutdown signal received"); + { + tracing::error!("Failed to flush data on shutdown: {}", e); + } else { + tracing::info!("Data flushed successfully"); + } }; server.run_with_shutdown(shutdown_signal).await?; @@ -119,6 +169,8 @@ fn print_help() { println!(" -h, --host Host to bind to (default: 127.0.0.1)"); println!(" -p, --port Port to listen on (default: 8080)"); println!(" --public Bind to all interfaces (0.0.0.0)"); + println!(" --db Path to graph database (default: ~/.aingle/cortex/graph.sled)"); + println!(" --memory Use volatile in-memory storage (no persistence)"); println!(" -V, --version Print version and exit"); println!(" --help Print this help message"); println!(); diff --git a/crates/aingle_cortex/src/rest/mod.rs b/crates/aingle_cortex/src/rest/mod.rs index 1876f00..bdbba03 100644 --- a/crates/aingle_cortex/src/rest/mod.rs +++ b/crates/aingle_cortex/src/rest/mod.rs @@ -78,9 +78,10 @@ pub fn router() -> Router { .route("/api/v1/query", post(query::query_pattern)) .route("/api/v1/query/subjects", get(query::list_subjects)) .route("/api/v1/query/predicates", get(query::list_predicates)) - // Stats + // Stats & Management .route("/api/v1/stats", get(stats::get_stats)) .route("/api/v1/health", get(stats::health_check)) + .route("/api/v1/flush", post(stats::flush_data)) // Validation/Proofs (legacy) .route("/api/v1/validate", post(proof::validate_triples)) .route("/api/v1/proof/{hash}", get(proof::get_proof)) diff --git a/crates/aingle_cortex/src/rest/stats.rs b/crates/aingle_cortex/src/rest/stats.rs index ef9475a..08074dd 100644 --- a/crates/aingle_cortex/src/rest/stats.rs +++ b/crates/aingle_cortex/src/rest/stats.rs @@ -91,6 +91,26 @@ pub struct ComponentStatus { pub message: Option, } +/// Flush response +#[derive(Debug, Serialize)] +pub struct FlushResponse { + /// Whether the flush was successful + pub ok: bool, +} + +/// Flush graph database and Ineru memory to disk. +/// +/// POST /api/v1/flush +pub async fn flush_data(State(state): State) -> Result> { + // Flush graph + { + let graph = state.graph.read().await; + graph.flush()?; + } + + Ok(Json(FlushResponse { ok: true })) +} + /// Health check endpoint /// /// GET /api/v1/health diff --git a/crates/aingle_cortex/src/server.rs b/crates/aingle_cortex/src/server.rs index 2c6f6c5..f4396c1 100644 --- a/crates/aingle_cortex/src/server.rs +++ b/crates/aingle_cortex/src/server.rs @@ -37,6 +37,12 @@ pub struct CortexConfig { pub audit_log_path: Option, /// Maximum request body size in bytes (default: 1MB). pub max_body_size: usize, + /// Path to the graph database directory. + /// + /// - `Some(":memory:")` — volatile in-memory storage (no persistence). + /// - `Some(path)` — persist to the given directory. + /// - `None` — persist to the default `~/.aingle/cortex/graph.sled`. + pub db_path: Option, } impl Default for CortexConfig { @@ -52,6 +58,7 @@ impl Default for CortexConfig { rate_limit_rpm: 100, audit_log_path: None, max_body_size: 1024 * 1024, // 1MB + db_path: None, } } } @@ -88,13 +95,16 @@ pub struct CortexServer { } impl CortexServer { - /// Creates a new `CortexServer` with a given configuration and a default, in-memory `AppState`. + /// Creates a new `CortexServer` with a given configuration. + /// + /// The graph database backend is selected based on `config.db_path`: + /// - `Some(":memory:")` — volatile in-memory storage. + /// - `Some(path)` — Sled-backed persistent storage at the given path. + /// - `None` — Sled-backed persistent storage at `~/.aingle/cortex/graph.sled`. pub fn new(config: CortexConfig) -> Result { - let state = if let Some(ref path) = config.audit_log_path { - AppState::with_audit_path(path.clone()) - } else { - AppState::new() - }; + let db_path = resolve_db_path(&config.db_path); + let state = AppState::with_db_path(&db_path, config.audit_log_path.clone())?; + info!("Graph database: {}", db_path); Ok(Self { config, state }) } @@ -248,6 +258,24 @@ impl CortexServer { } } +/// Resolves the graph database path from the configuration. +/// +/// - `":memory:"` → returns `":memory:"` (volatile in-memory storage). +/// - An explicit path → returns it as-is. +/// - `None` → returns the default `~/.aingle/cortex/graph.sled`. +fn resolve_db_path(db_path: &Option) -> String { + match db_path { + Some(p) if p == ":memory:" => ":memory:".to_string(), + Some(p) => p.clone(), + None => { + let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + let default_dir = home.join(".aingle").join("cortex"); + std::fs::create_dir_all(&default_dir).ok(); + default_dir.join("graph.sled").to_string_lossy().to_string() + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/aingle_cortex/src/state.rs b/crates/aingle_cortex/src/state.rs index aaead31..0147abb 100644 --- a/crates/aingle_cortex/src/state.rs +++ b/crates/aingle_cortex/src/state.rs @@ -5,6 +5,7 @@ use aingle_graph::GraphDB; use aingle_logic::RuleEngine; +use std::path::Path; use std::sync::Arc; use ineru::IneruMemory; use tokio::sync::RwLock; @@ -131,6 +132,103 @@ impl AppState { } } + /// Creates a new `AppState` with a configurable database path and optional audit log. + /// + /// - `":memory:"` — volatile in-memory storage. + /// - Any other path — Sled-backed persistent storage. + pub fn with_db_path( + db_path: &str, + audit_log_path: Option, + ) -> crate::error::Result { + let graph = if db_path == ":memory:" { + GraphDB::memory()? + } else { + // Ensure the parent directory exists + if let Some(parent) = Path::new(db_path).parent() { + std::fs::create_dir_all(parent).ok(); + } + GraphDB::sled(db_path)? + }; + + let logic = RuleEngine::new(); + + // Load Ineru snapshot if available next to the graph database + let memory = if db_path != ":memory:" { + let snapshot_path = Path::new(db_path) + .parent() + .unwrap_or(Path::new(".")) + .join("ineru.snapshot"); + if snapshot_path.exists() { + match IneruMemory::load_from_file(&snapshot_path) { + Ok(mem) => { + log::info!("Loaded Ineru snapshot from {}", snapshot_path.display()); + mem + } + Err(e) => { + log::warn!("Failed to load Ineru snapshot: {}. Starting fresh.", e); + IneruMemory::agent_mode() + } + } + } else { + IneruMemory::agent_mode() + } + } else { + IneruMemory::agent_mode() + }; + + let audit_log = if let Some(path) = audit_log_path { + AuditLog::with_path(10_000, path) + } else { + AuditLog::default() + }; + + #[cfg(feature = "auth")] + let user_store = { + let store = Arc::new(UserStore::new()); + let _ = store.init_default_admin(); + store + }; + + Ok(Self { + graph: Arc::new(RwLock::new(graph)), + logic: Arc::new(RwLock::new(logic)), + memory: Arc::new(RwLock::new(memory)), + broadcaster: Arc::new(EventBroadcaster::new()), + proof_store: Arc::new(ProofStore::new()), + sandbox_manager: Arc::new(SandboxManager::new()), + audit_log: Arc::new(RwLock::new(audit_log)), + #[cfg(feature = "auth")] + user_store, + #[cfg(feature = "p2p")] + p2p: None, + }) + } + + /// Flushes the graph database and saves the Ineru memory snapshot to disk. + /// + /// This should be called before shutdown or binary updates to ensure + /// no data is lost. + pub async fn flush(&self, snapshot_dir: Option<&Path>) -> crate::error::Result<()> { + // Flush graph database + { + let graph = self.graph.read().await; + graph.flush()?; + } + + // Save Ineru memory snapshot + if let Some(dir) = snapshot_dir { + let snapshot_path = dir.join("ineru.snapshot"); + let memory = self.memory.read().await; + if let Err(e) = memory.save_to_file(&snapshot_path) { + log::warn!("Failed to save Ineru snapshot: {}", e); + } else { + log::info!("Ineru snapshot saved to {}", snapshot_path.display()); + } + } + + Ok(()) + } + /// Returns an internal Cortex client configured for same-process access. /// /// This client calls the Cortex REST API and can be used by host functions diff --git a/crates/aingle_graph/Cargo.toml b/crates/aingle_graph/Cargo.toml index 3cd8034..80988e6 100644 --- a/crates/aingle_graph/Cargo.toml +++ b/crates/aingle_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_graph" -version = "0.4.0" +version = "0.4.1" description = "Native GraphDB for AIngle - Semantic triple store with SPO indexes" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_graph/src/lib.rs b/crates/aingle_graph/src/lib.rs index 0acaea6..84c8e32 100644 --- a/crates/aingle_graph/src/lib.rs +++ b/crates/aingle_graph/src/lib.rs @@ -554,6 +554,28 @@ impl GraphDB { self.store.stats() } + /// Flushes any buffered writes to the underlying storage backend. + /// + /// For persistent backends (e.g., Sled), this ensures all data is + /// durably written to disk. For in-memory backends, this is a no-op. + /// + /// # Examples + /// + /// ```no_run + /// # #[cfg(feature = "sled-backend")] + /// # fn example() -> Result<(), aingle_graph::Error> { + /// use aingle_graph::GraphDB; + /// + /// let db = GraphDB::sled("./my_graph.db")?; + /// // ... insert some triples ... + /// db.flush()?; // Ensure data is persisted + /// # Ok(()) + /// # } + /// ``` + pub fn flush(&self) -> Result<()> { + self.store.flush() + } + /// Returns the total number of triples in the graph. /// /// # Examples diff --git a/crates/aingle_graph/src/store.rs b/crates/aingle_graph/src/store.rs index 29d7463..61854f1 100644 --- a/crates/aingle_graph/src/store.rs +++ b/crates/aingle_graph/src/store.rs @@ -233,6 +233,14 @@ impl GraphStore { self.backend.count() } + /// Flushes any buffered writes to the underlying storage backend. + /// + /// For persistent backends (e.g., Sled), this ensures all data is + /// written to disk. For in-memory backends, this is a no-op. + pub fn flush(&self) -> Result<()> { + self.backend.flush() + } + /// Returns statistics about the graph, such as triple and node counts. pub fn stats(&self) -> GraphStats { let index = self.index.read().ok(); diff --git a/crates/aingle_logic/Cargo.toml b/crates/aingle_logic/Cargo.toml index 675bb57..2833965 100644 --- a/crates/aingle_logic/Cargo.toml +++ b/crates/aingle_logic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_logic" -version = "0.4.0" +version = "0.4.1" description = "Proof-of-Logic validation engine for AIngle semantic graphs" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_minimal/Cargo.toml b/crates/aingle_minimal/Cargo.toml index 61167e3..a186645 100644 --- a/crates/aingle_minimal/Cargo.toml +++ b/crates/aingle_minimal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_minimal" -version = "0.4.0" +version = "0.4.1" description = "Ultra-light AIngle node for IoT devices (<1MB RAM)" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_viz/Cargo.toml b/crates/aingle_viz/Cargo.toml index 27205ec..fd5b506 100644 --- a/crates/aingle_viz/Cargo.toml +++ b/crates/aingle_viz/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_viz" -version = "0.4.0" +version = "0.4.1" description = "DAG Visualization for AIngle - Web-based graph explorer" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_zk/Cargo.toml b/crates/aingle_zk/Cargo.toml index 0cd88e6..12e1d9a 100644 --- a/crates/aingle_zk/Cargo.toml +++ b/crates/aingle_zk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_zk" -version = "0.4.0" +version = "0.4.1" description = "Zero-Knowledge Proofs for AIngle - privacy-preserving cryptographic primitives" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" diff --git a/crates/aingle_zome_types/Cargo.toml b/crates/aingle_zome_types/Cargo.toml index 9842680..8b2ed7a 100644 --- a/crates/aingle_zome_types/Cargo.toml +++ b/crates/aingle_zome_types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_zome_types" -version = "0.4.0" +version = "0.4.1" description = "AIngle zome types" license = "Apache-2.0 OR LicenseRef-Commercial" homepage = "https://apilium.com" diff --git a/crates/ineru/Cargo.toml b/crates/ineru/Cargo.toml index 6c89e43..21c82c1 100644 --- a/crates/ineru/Cargo.toml +++ b/crates/ineru/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ineru" -version = "0.4.0" +version = "0.4.1" description = "Ineru: Neural-inspired memory system for AIngle AI agents" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -26,6 +26,7 @@ compression = [] # Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +bincode = "2.0" # Crypto for hashing blake3 = { version = "1.8", default-features = false, features = ["std"] } diff --git a/crates/ineru/src/error.rs b/crates/ineru/src/error.rs index 1b5946f..b7e4e01 100644 --- a/crates/ineru/src/error.rs +++ b/crates/ineru/src/error.rs @@ -89,6 +89,11 @@ impl Error { pub fn not_found(id: &str) -> Self { Error::NotFound(id.to_string()) } + + /// Helper to create an `Internal` error. + pub fn internal(msg: impl Into) -> Self { + Error::Internal(msg.into()) + } } #[cfg(test)] diff --git a/crates/ineru/src/lib.rs b/crates/ineru/src/lib.rs index 9c94c43..f13f1ec 100644 --- a/crates/ineru/src/lib.rs +++ b/crates/ineru/src/lib.rs @@ -337,6 +337,70 @@ pub struct MemoryStats { pub total_memory_bytes: usize, } +// --------------------------------------------------------------------------- +// Snapshot persistence +// --------------------------------------------------------------------------- + +/// A serializable snapshot of the Ineru memory state. +/// +/// Used to persist STM + LTM contents across process restarts. +#[derive(serde::Serialize, serde::Deserialize)] +struct IneruSnapshot { + stm_entries: Vec, + ltm_entries: Vec, + config: MemoryConfig, +} + +impl IneruMemory { + /// Exports the current memory state as a JSON byte vector. + pub fn export_snapshot(&self) -> Result> { + let stm_entries = self.stm.all_entries(); + let ltm_entries = self.ltm.all_entries(); + + let snapshot = IneruSnapshot { + stm_entries, + ltm_entries, + config: self.config.clone(), + }; + + serde_json::to_vec(&snapshot).map_err(|e| Error::internal(format!("snapshot export: {}", e))) + } + + /// Imports a memory state from a JSON byte slice. + pub fn import_snapshot(data: &[u8]) -> Result { + let snapshot: IneruSnapshot = serde_json::from_slice(data) + .map_err(|e| Error::internal(format!("snapshot import: {}", e)))?; + + let mut memory = Self::new(snapshot.config); + + // Restore STM entries + for entry in snapshot.stm_entries { + let _ = memory.stm.store(entry); + } + + // Restore LTM entries + for entry in snapshot.ltm_entries { + let _ = memory.ltm.store(entry); + } + + Ok(memory) + } + + /// Saves the current memory state to a file. + pub fn save_to_file(&self, path: &std::path::Path) -> Result<()> { + let data = self.export_snapshot()?; + std::fs::write(path, data) + .map_err(|e| Error::internal(format!("snapshot write: {}", e))) + } + + /// Loads a memory state from a file. + pub fn load_from_file(path: &std::path::Path) -> Result { + let data = std::fs::read(path) + .map_err(|e| Error::internal(format!("snapshot read: {}", e)))?; + Self::import_snapshot(&data) + } +} + impl Default for IneruMemory { fn default() -> Self { Self::new(MemoryConfig::default()) diff --git a/crates/ineru/src/ltm.rs b/crates/ineru/src/ltm.rs index 0f5eeb9..cca65dd 100644 --- a/crates/ineru/src/ltm.rs +++ b/crates/ineru/src/ltm.rs @@ -341,6 +341,11 @@ impl LongTermMemory { self.memories.len() } + /// Returns all memory entries stored in the LTM. + pub fn all_entries(&self) -> Vec { + self.memories.values().cloned().collect() + } + /// Returns the number of entities in the knowledge graph. pub fn entity_count(&self) -> usize { self.entities.len() diff --git a/crates/ineru/src/stm.rs b/crates/ineru/src/stm.rs index 5e6e67e..435a389 100644 --- a/crates/ineru/src/stm.rs +++ b/crates/ineru/src/stm.rs @@ -255,6 +255,11 @@ impl ShortTermMemory { Ok(()) } + /// Returns all entries currently in the STM. + pub fn all_entries(&self) -> Vec { + self.entries.values().cloned().collect() + } + /// Returns the number of entries currently in the STM. pub fn len(&self) -> usize { self.entries.len() diff --git a/crates/kaneru/Cargo.toml b/crates/kaneru/Cargo.toml index e4187c4..98c67e0 100644 --- a/crates/kaneru/Cargo.toml +++ b/crates/kaneru/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kaneru" -version = "0.4.0" +version = "0.4.1" description = "Kaneru: Unified Multi-Agent Execution System for AIngle AI agents" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle"