otherone-memory 0.5.0

Long-term and short-term memory primitives for the Otherone agent framework
Documentation
use std::fs;
use std::path::PathBuf;
use std::sync::{OnceLock, RwLock};

use serde::{Deserialize, Serialize};

use crate::error::MemoryError;
use crate::tree::MemoryTree;
use crate::types::MemoryPoint;

static MEMORY_STORAGE_ROOT: OnceLock<RwLock<Option<PathBuf>>> = OnceLock::new();

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryStorageFile {
    #[serde(default)]
    pub points: Vec<MemoryPoint>,
}

pub fn set_memory_storage_root(root: impl Into<PathBuf>) {
    let lock = MEMORY_STORAGE_ROOT.get_or_init(|| RwLock::new(None));
    if let Ok(mut configured_root) = lock.write() {
        *configured_root = Some(root.into());
    }
}

pub fn clear_memory_storage_root() {
    let lock = MEMORY_STORAGE_ROOT.get_or_init(|| RwLock::new(None));
    if let Ok(mut configured_root) = lock.write() {
        *configured_root = None;
    }
}

pub fn memory_storage_path() -> PathBuf {
    memory_storage_root()
        .join(".otherone")
        .join("memory")
        .join("long-term-memory.json")
}

pub fn read_memory_tree() -> Result<MemoryTree, MemoryError> {
    let path = memory_storage_path();

    if !path.exists() {
        let tree = MemoryTree::new();
        write_memory_tree(&tree)?;
        return Ok(tree);
    }

    let content =
        fs::read_to_string(&path).map_err(|error| MemoryError::IoError(error.to_string()))?;
    let storage_file: MemoryStorageFile = serde_json::from_str(&content)
        .map_err(|error| MemoryError::JsonError(error.to_string()))?;

    MemoryTree::from_points(storage_file.points)
}

pub fn write_memory_tree(tree: &MemoryTree) -> Result<(), MemoryError> {
    let path = memory_storage_path();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|error| MemoryError::IoError(error.to_string()))?;
    }

    let storage_file = MemoryStorageFile {
        points: tree.to_points(),
    };
    let content = serde_json::to_string_pretty(&storage_file)
        .map_err(|error| MemoryError::JsonError(error.to_string()))?;

    fs::write(&path, content).map_err(|error| MemoryError::IoError(error.to_string()))?;
    Ok(())
}

fn memory_storage_root() -> PathBuf {
    MEMORY_STORAGE_ROOT
        .get_or_init(|| RwLock::new(None))
        .read()
        .ok()
        .and_then(|configured_root| configured_root.clone())
        .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn reads_and_writes_memory_tree_file() {
        let root = std::env::temp_dir().join(format!(
            "otherone-memory-storage-{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        set_memory_storage_root(root.clone());

        let mut tree = MemoryTree::new();
        let root_id = tree
            .insert_root("likes noodles", "food preference")
            .unwrap();
        write_memory_tree(&tree).unwrap();

        let restored = read_memory_tree().unwrap();
        assert!(memory_storage_path().starts_with(&root));
        assert!(restored.contains(&root_id));

        clear_memory_storage_root();
    }
}