clawgarden-agent 0.22.0

Agent runtime with persona/memory loader, judge, and pi RPC for ClawGarden
Documentation
//! Memory loader — loads and manages agent memory.
//!
//! Uses the `Memory` trait from `clawgarden-proto` to provide
//! structured memory with categories (Core, Daily, Ephemeral)
//! and recall-by-relevance.

use anyhow::Result;
use clawgarden_proto::{FileMemory, Memory, MemoryCategory, MemoryEntry};
use std::path::PathBuf;

/// Create a FileMemory instance for an agent.
///
/// Memory file location: `<workspace>/agents/<name>/memory.md`
/// The file is created on first store if it doesn't exist.
pub fn create_memory(agent_name: &str) -> Result<Box<dyn Memory>> {
    let config = clawgarden_proto::AppConfig::load();
    let path = PathBuf::from(config.agents_dir())
        .join(agent_name)
        .join("memory.md");

    Ok(Box::new(FileMemory::new(path)))
}

/// Load full memory content as a string (for system prompt injection).
///
/// Returns the raw Markdown content, or empty string if no memory file exists.
pub async fn load_memory(agent_name: &str) -> Result<String> {
    let config = clawgarden_proto::AppConfig::load();
    let path = PathBuf::from(config.agents_dir())
        .join(agent_name)
        .join("memory.md");

    match tokio::fs::read_to_string(&path).await {
        Ok(content) => {
            log::info!(
                "Loaded memory for agent '{}' from {}",
                agent_name,
                path.display()
            );
            Ok(content)
        }
        Err(e) => {
            log::debug!(
                "Memory file not found for agent '{}' at {} (optional): {}",
                agent_name,
                path.display(),
                e
            );
            Ok(String::new())
        }
    }
}

/// Recall relevant memories for a query.
///
/// Returns a formatted string suitable for prompt injection.
/// Returns empty string if no relevant memories found.
pub fn recall_for_prompt(agent_name: &str, query: &str, limit: usize) -> String {
    let mem = match create_memory(agent_name) {
        Ok(m) => m,
        Err(e) => {
            log::debug!("Failed to create memory for recall: {}", e);
            return String::new();
        }
    };

    match mem.recall(query, limit) {
        Ok(entries) if entries.is_empty() => String::new(),
        Ok(entries) => {
            let mut out = String::from("[Things you remember]\n");
            for entry in &entries {
                out.push_str(&format!("- {}: {}\n", entry.key, entry.content));
            }
            out
        }
        Err(e) => {
            log::debug!("Memory recall failed: {}", e);
            String::new()
        }
    }
}

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

    #[tokio::test]
    async fn test_load_memory_missing_file() {
        let result = load_memory("nonexistent_agent").await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }

    #[test]
    fn test_create_memory() {
        // Should not fail even for nonexistent agent
        let result = create_memory("test_agent");
        assert!(result.is_ok());
    }

    #[test]
    fn test_recall_for_prompt_no_file() {
        let result = recall_for_prompt("nonexistent_agent_12345", "Rust", 5);
        assert!(result.is_empty());
    }
}