use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MemoryFile {
pub file_name: String,
pub content: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind")]
pub enum MemoryNode {
#[serde(rename = "folder")]
Folder { name: String, children: Vec<MemoryNode> },
#[serde(rename = "file")]
File { name: String, path: String, content: String },
}
fn build_tree(files: &HashMap<String, String>) -> Vec<MemoryNode> {
let mut entries: Vec<(&str, &str)> = files.iter().map(|(p, c)| (p.as_str(), c.as_str())).collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
build_level(&entries, "")
}
fn build_level(entries: &[(&str, &str)], prefix: &str) -> Vec<MemoryNode> {
let mut folder_order: Vec<String> = Vec::new();
let mut folder_entries: HashMap<String, Vec<(&str, &str)>> = HashMap::new();
let mut files_here: Vec<(&str, &str)> = Vec::new();
for &(path, content) in entries {
let rel = path.strip_prefix(prefix).unwrap_or(path).trim_start_matches('/');
if rel.is_empty() {
continue;
}
match rel.split_once('/') {
Some((folder, _rest)) => {
folder_entries.entry(folder.to_string()).or_insert_with(|| {
folder_order.push(folder.to_string());
Vec::new()
}).push((path, content));
}
None => files_here.push((path, content)),
}
}
let mut nodes = Vec::new();
folder_order.sort();
for folder in folder_order {
let child_entries = folder_entries.remove(&folder).unwrap_or_default();
let next_prefix = if prefix.is_empty() { folder.clone() } else { format!("{}/{}", prefix, folder) };
nodes.push(MemoryNode::Folder { children: build_level(&child_entries, &next_prefix), name: folder });
}
files_here.sort_by(|a, b| a.0.cmp(b.0));
for (path, content) in files_here {
let name = path.rsplit('/').next().unwrap_or(path).to_string();
nodes.push(MemoryNode::File { name, path: path.to_string(), content: content.to_string() });
}
nodes
}
pub struct MemoryManager {
base_dir: PathBuf,
memories: Mutex<HashMap<String, HashMap<String, String>>>,
shared: Mutex<HashMap<String, String>>,
}
impl MemoryManager {
pub fn new(base_dir: impl AsRef<Path>) -> std::io::Result<Self> {
let base_dir = base_dir.as_ref().to_path_buf();
#[cfg(not(target_arch = "wasm32"))]
{
std::fs::create_dir_all(&base_dir)?;
}
Ok(Self { base_dir, memories: Mutex::new(HashMap::new()), shared: Mutex::new(HashMap::new()) })
}
pub fn setup_agent_dir(&self, agent_id: &str) -> std::io::Result<()> {
#[cfg(not(target_arch = "wasm32"))]
{
let agent_dir = self.base_dir.join(agent_id);
let memories_dir = agent_dir.join("memories");
std::fs::create_dir_all(&memories_dir)?;
let index_path = agent_dir.join("index.md");
if !index_path.exists() {
std::fs::write(index_path, format!("# Index for Agent {}\n", agent_id))?;
}
}
Ok(())
}
pub fn read_memory(&self, agent_id: &str, file_name: &str) -> Result<String, String> {
let memories = self.memories.lock().unwrap();
if let Some(agent_memories) = memories.get(agent_id) {
if let Some(content) = agent_memories.get(file_name) {
return Ok(content.clone());
}
}
Err(format!("Memory file {} not found", file_name))
}
pub fn write_memory(&self, agent_id: &str, file_name: &str, content: &str) -> std::io::Result<()> {
#[cfg(not(target_arch = "wasm32"))]
{
let memories_dir = self.base_dir.join(agent_id).join("memories");
if let Some(parent) = Path::new(file_name).parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(memories_dir.join(parent))?;
}
std::fs::write(memories_dir.join(file_name), content)?;
}
self.memories
.lock()
.unwrap()
.entry(agent_id.to_string())
.or_default()
.insert(file_name.to_string(), content.to_string());
Ok(())
}
pub fn read_memories(&self, agent_id: &str) -> Vec<MemoryFile> {
self.memories
.lock()
.unwrap()
.get(agent_id)
.map(|files| files.iter().map(|(file_name, content)| MemoryFile { file_name: file_name.clone(), content: content.clone() }).collect())
.unwrap_or_default()
}
pub fn read_memory_tree(&self, agent_id: &str) -> Vec<MemoryNode> {
let memories = self.memories.lock().unwrap();
match memories.get(agent_id) {
Some(files) => build_tree(files),
None => Vec::new(),
}
}
pub fn write_shared(&self, path: &str, content: &str) -> std::io::Result<()> {
#[cfg(not(target_arch = "wasm32"))]
{
let shared_dir = self.base_dir.join("_shared");
if let Some(parent) = Path::new(path).parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(shared_dir.join(parent))?;
} else {
std::fs::create_dir_all(&shared_dir)?;
}
std::fs::write(shared_dir.join(path), content)?;
}
self.shared.lock().unwrap().insert(path.to_string(), content.to_string());
Ok(())
}
pub fn read_shared(&self, path: &str) -> Result<String, String> {
self.shared
.lock()
.unwrap()
.get(path)
.cloned()
.ok_or_else(|| format!("Shared file {} not found", path))
}
pub fn read_shared_tree(&self) -> Vec<MemoryNode> {
build_tree(&self.shared.lock().unwrap())
}
}