garudust-memory 0.2.1

SQLite-backed persistent memory and user profiles for Garudust AI agents
Documentation
use std::path::{Path, PathBuf};

use async_trait::async_trait;
use garudust_core::{
    config::MemoryExpiryConfig,
    error::AgentError,
    memory::{MemoryContent, MemoryStore},
};

pub struct FileMemoryStore {
    memory_path: PathBuf,
    profile_path: PathBuf,
}

impl FileMemoryStore {
    pub fn new(home_dir: &Path) -> Self {
        let memories = home_dir.join("memories");
        Self {
            memory_path: memories.join("MEMORY.md"),
            profile_path: memories.join("USER.md"),
        }
    }

    async fn read_file(&self, path: &PathBuf) -> Result<String, AgentError> {
        match tokio::fs::read_to_string(path).await {
            Ok(s) => Ok(s),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
            Err(e) => Err(AgentError::Other(anyhow::anyhow!("{e}"))),
        }
    }

    async fn write_file(&self, path: &PathBuf, content: &str) -> Result<(), AgentError> {
        if let Some(parent) = path.parent() {
            tokio::fs::create_dir_all(parent)
                .await
                .map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))?;
        }
        tokio::fs::write(path, content)
            .await
            .map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))
    }

    /// Expire old entries from MEMORY.md according to `config`.
    /// Returns the number of entries removed (0 means no file write occurred).
    pub async fn expire_entries(&self, config: &MemoryExpiryConfig) -> Result<usize, AgentError> {
        let mut mem = self.read_memory().await?;
        let removed = mem.expire(config);
        if removed > 0 {
            self.write_memory(&mem).await?;
        }
        Ok(removed)
    }
}

#[async_trait]
impl MemoryStore for FileMemoryStore {
    async fn read_memory(&self) -> Result<MemoryContent, AgentError> {
        let raw = self.read_file(&self.memory_path).await?;
        Ok(MemoryContent::parse(&raw))
    }

    async fn write_memory(&self, content: &MemoryContent) -> Result<(), AgentError> {
        self.write_file(&self.memory_path, &content.serialize())
            .await
    }

    async fn read_user_profile(&self) -> Result<String, AgentError> {
        self.read_file(&self.profile_path).await
    }

    async fn write_user_profile(&self, content: &str) -> Result<(), AgentError> {
        self.write_file(&self.profile_path, content).await
    }
}