cortex-agent 0.3.1

Self-learning AI agent with persistent memory, tools, plugins, and a beautiful terminal UI
use std::sync::Arc;

use crate::memory::store::MemoryStore;
use crate::tool::{param_integer, param_string, ToolSpec};

pub fn memory_tools(store: Option<Arc<std::sync::Mutex<MemoryStore>>>) -> Vec<ToolSpec> {
    vec![
        save_memory_tool(store.clone()),
        search_memory_tool(store.clone()),
        list_memories_tool(store.clone()),
        forget_memory_tool(store),
    ]
}

fn get_store(store: &Option<Arc<std::sync::Mutex<MemoryStore>>>) -> Result<std::sync::MutexGuard<'_, MemoryStore>, String> {
    store
        .as_ref()
        .and_then(|s| s.lock().ok())
        .ok_or_else(|| "Memory is not initialized. Enable memory in config.".into())
}

fn save_memory_tool(store: Option<Arc<std::sync::Mutex<MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[
        ("target", param_string("'user' for user-specific facts, 'memory' for general knowledge")),
        ("content", param_string("The fact to remember (be specific and complete)")),
        ("category", serde_json::json!({"type": "string", "description": "Category grouping", "default": "general"})),
        ("importance", serde_json::json!({"type": "integer", "description": "How important (1-5)", "default": 3})),
        ("tags", serde_json::json!({"type": "string", "description": "Comma-separated tags", "default": ""})),
    ]);

    ToolSpec::new("save_memory", "Save a fact to persistent memory so it can be recalled later", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let target = args.get("target").and_then(|v| v.as_str()).unwrap_or("memory");
            let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
            let category = args.get("category").and_then(|v| v.as_str()).unwrap_or("general");
            let importance = args.get("importance").and_then(|v| v.as_i64()).unwrap_or(3).clamp(1, 5) as i32;
            let tags_str = args.get("tags").and_then(|v| v.as_str()).unwrap_or("");
            let tags: Vec<String> = if tags_str.is_empty() { vec![] } else { tags_str.split(',').map(|t| t.trim().to_string()).filter(|t| !t.is_empty()).collect() };
            let mid = guard.save_memory(target, content, category, importance, &tags).map_err(|e| format!("Failed to save memory: {}", e))?;
            Ok(format!("Saved as memory #{} (target={}, importance={})", mid, target, importance))
        }),
    )
}

fn search_memory_tool(store: Option<Arc<std::sync::Mutex<MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[
        ("query", param_string("Search terms to match against memory content and tags")),
        ("limit", serde_json::json!({"type": "integer", "description": "Max results", "default": 5})),
    ]);
    ToolSpec::new("search_memory", "Search saved memories for relevant facts", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let query = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
            let limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(5).min(20);
            let results = guard.search_memories(query, limit).map_err(|e| format!("Search failed: {}", e))?;
            if results.is_empty() { return Ok("No matching memories found.".into()); }
            let mut lines = vec!["Matching memories:".to_string()];
            for m in &results { lines.push(format!("  #{} [{}/{}] (imp={}) [{}]: {}", m.id, m.target, m.category, m.importance, m.created_at, m.content)); }
            Ok(lines.join("\n"))
        }),
    )
}

fn list_memories_tool(store: Option<Arc<std::sync::Mutex<MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[
        ("target", serde_json::json!({"type": "string", "description": "'user', 'memory', or empty for all", "default": ""})),
        ("limit", serde_json::json!({"type": "integer", "description": "Max entries", "default": 10})),
    ]);
    ToolSpec::new("list_memories", "List recent memories, optionally filtered by target", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let target = args.get("target").and_then(|v| v.as_str()).unwrap_or("");
            let limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(10).min(50);
            let t = if target.is_empty() { None } else { Some(target) };
            if let Some(t) = t { if t != "user" && t != "memory" { return Err(format!("Invalid target '{}'. Use 'user', 'memory', or leave empty.", t)); } }
            let results = guard.list_memories(t, limit).map_err(|e| format!("List failed: {}", e))?;
            if results.is_empty() { return Ok("No memories saved yet.".into()); }
            let mut lines = vec![format!("Memories ({}):", if target.is_empty() { "all" } else { target })];
            for m in &results {
                let tag_str = if m.tags.is_empty() { String::new() } else { format!(" [{}]", m.tags.join(", ")) };
                lines.push(format!("  #{} [{}/imp={}] {}{}", m.id, m.category, m.importance, m.content, tag_str));
            }
            Ok(lines.join("\n"))
        }),
    )
}

fn forget_memory_tool(store: Option<Arc<std::sync::Mutex<MemoryStore>>>) -> ToolSpec {
    let (params, _) = crate::tool::required_params(&[("memory_id", param_integer("The ID of the memory to delete"))]);
    ToolSpec::new("forget_memory", "Delete a specific memory by its ID", params,
        Arc::new(move |args| {
            let guard = get_store(&store)?;
            let memory_id = args.get("memory_id").and_then(|v| v.as_i64()).unwrap_or(0);
            if guard.delete_memory(memory_id).map_err(|e| format!("Failed to delete: {}", e))? {
                Ok(format!("Memory #{} deleted.", memory_id))
            } else { Ok(format!("Memory #{} not found.", memory_id)) }
        }),
    )
}