dejadb_store/
memory_tool.rs1use dejadb_core::error::{Hash, DejaDbError, Result};
17use dejadb_core::types::Fact;
18use serde_json::{json, Value};
19
20use crate::DejaDB;
21
22pub const MEMORY_FILE_RELATION: &str = "memory_file";
23const ROOT: &str = "/memories";
24
25pub struct MemoryTool<'a> {
26 m: &'a mut DejaDB,
27 ns: String,
28}
29
30impl<'a> MemoryTool<'a> {
31 pub fn new(m: &'a mut DejaDB, ns: &str) -> Self {
32 MemoryTool { m, ns: ns.to_string() }
33 }
34
35 pub fn execute(&mut self, cmd: &Value) -> Result<String> {
38 let command = cmd.get("command").and_then(|v| v.as_str()).unwrap_or("");
39 let path = cmd.get("path").and_then(|v| v.as_str()).unwrap_or("");
40 match command {
41 "view" => self.view(path),
42 "create" => self.create(
43 path,
44 cmd.get("file_text").and_then(|v| v.as_str()).unwrap_or(""),
45 ),
46 "str_replace" => self.str_replace(
47 path,
48 cmd.get("old_str").and_then(|v| v.as_str()).unwrap_or(""),
49 cmd.get("new_str").and_then(|v| v.as_str()).unwrap_or(""),
50 ),
51 "insert" => self.insert(
52 path,
53 cmd.get("insert_line").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
54 cmd.get("insert_text").and_then(|v| v.as_str()).unwrap_or(""),
55 ),
56 "delete" => self.delete(path),
57 "rename" => self.rename(
58 cmd.get("old_path").and_then(|v| v.as_str()).unwrap_or(path),
59 cmd.get("new_path").and_then(|v| v.as_str()).unwrap_or(""),
60 ),
61 other => Err(DejaDbError::Validation(format!(
62 "unknown memory command: {other}"
63 ))),
64 }
65 }
66
67 fn check_path(path: &str) -> Result<()> {
68 if !path.starts_with(ROOT) || path.contains("..") || path.contains("//") {
69 return Err(DejaDbError::Validation(format!(
70 "path must live under {ROOT} (got '{path}')"
71 )));
72 }
73 Ok(())
74 }
75
76 fn head(&mut self, path: &str) -> Result<Option<(Hash, String)>> {
77 match self.m.latest(&self.ns, path, MEMORY_FILE_RELATION)? {
78 Some(g) => {
79 let content = g
80 .fields
81 .get("context")
82 .and_then(|c| c.get("content"))
83 .and_then(|c| c.as_str())
84 .unwrap_or("")
85 .to_string();
86 Ok(Some((g.hash, content)))
87 }
88 None => Ok(None),
89 }
90 }
91
92 fn write_version(&mut self, path: &str, content: &str, prev: Option<Hash>) -> Result<Hash> {
93 use sha2::{Digest, Sha256};
94 let digest = hex::encode(&Sha256::digest(content.as_bytes())[..6]);
95 let mut f = Fact::new(path, MEMORY_FILE_RELATION, &format!("v:{digest}"));
96 f.common.namespace = Some(self.ns.clone());
97 f.common.context = Some(json!({ "content": content }));
98 f.common.embedding_text = Some(crate::migrate::clip_et(content));
101 f.common.source_type = Some("agent".to_string());
102 match prev {
103 Some(old) => self.m.supersede(&old, &mut f),
104 None => self.m.add(&f),
105 }
106 }
107
108 fn view(&mut self, path: &str) -> Result<String> {
109 Self::check_path(path)?;
110 let dirish = path == ROOT || path.ends_with('/');
111 if dirish {
112 let prefix = if path.ends_with('/') { path.to_string() } else { format!("{path}/") };
113 let files = self.m.subjects_with_relation(&self.ns, MEMORY_FILE_RELATION)?;
114 let listing: Vec<String> = files
115 .into_iter()
116 .filter(|f| f.starts_with(&prefix))
117 .collect();
118 if listing.is_empty() {
119 return Ok(format!("Directory: {path}\n(empty)"));
120 }
121 return Ok(format!("Directory: {path}\n{}", listing.join("\n")));
122 }
123 match self.head(path)? {
124 Some((_, content)) => {
125 let numbered: Vec<String> = content
126 .lines()
127 .enumerate()
128 .map(|(i, l)| format!("{:>4}: {l}", i + 1))
129 .collect();
130 Ok(numbered.join("\n"))
131 }
132 None => Err(DejaDbError::Validation(format!("file not found: {path}"))),
133 }
134 }
135
136 fn create(&mut self, path: &str, file_text: &str) -> Result<String> {
137 Self::check_path(path)?;
138 if path == ROOT || path.ends_with('/') {
139 return Err(DejaDbError::Validation("create requires a file path".into()));
140 }
141 let prev = self.head(path)?.map(|(h, _)| h);
142 let existed = prev.is_some();
143 self.write_version(path, file_text, prev)?;
144 Ok(format!(
145 "{} {path}",
146 if existed { "Overwrote (new version of)" } else { "Created" }
147 ))
148 }
149
150 fn str_replace(&mut self, path: &str, old_str: &str, new_str: &str) -> Result<String> {
151 Self::check_path(path)?;
152 let (head, content) = self
153 .head(path)?
154 .ok_or_else(|| DejaDbError::Validation(format!("file not found: {path}")))?;
155 let occurrences = content.matches(old_str).count();
156 if old_str.is_empty() || occurrences == 0 {
157 return Err(DejaDbError::Validation("old_str not found in file".into()));
158 }
159 if occurrences > 1 {
160 return Err(DejaDbError::Validation(format!(
161 "old_str appears {occurrences} times — must be unique"
162 )));
163 }
164 let updated = content.replacen(old_str, new_str, 1);
165 self.write_version(path, &updated, Some(head))?;
166 Ok(format!("Replaced text in {path}"))
167 }
168
169 fn insert(&mut self, path: &str, insert_line: usize, insert_text: &str) -> Result<String> {
170 Self::check_path(path)?;
171 let (head, content) = self
172 .head(path)?
173 .ok_or_else(|| DejaDbError::Validation(format!("file not found: {path}")))?;
174 let mut lines: Vec<&str> = content.lines().collect();
175 let at = insert_line.min(lines.len());
176 lines.insert(at, insert_text);
177 let updated = lines.join("\n");
178 self.write_version(path, &updated, Some(head))?;
179 Ok(format!("Inserted at line {at} in {path}"))
180 }
181
182 fn delete(&mut self, path: &str) -> Result<String> {
183 Self::check_path(path)?;
184 if path == ROOT {
185 return Err(DejaDbError::Validation("refusing to delete the root".into()));
186 }
187 let versions = self.m.history(&self.ns, path, MEMORY_FILE_RELATION)?;
189 if versions.is_empty() {
190 return Err(DejaDbError::Validation(format!("file not found: {path}")));
191 }
192 let n = versions.len();
193 for v in versions {
194 self.m.forget(&v.hash)?;
195 }
196 Ok(format!("Deleted {path} ({n} versions erased)"))
197 }
198
199 fn rename(&mut self, old_path: &str, new_path: &str) -> Result<String> {
200 Self::check_path(old_path)?;
201 Self::check_path(new_path)?;
202 let (old_head, content) = self
203 .head(old_path)?
204 .ok_or_else(|| DejaDbError::Validation(format!("file not found: {old_path}")))?;
205 if self.head(new_path)?.is_some() {
206 return Err(DejaDbError::Validation(format!("target exists: {new_path}")));
207 }
208 let mut f = {
212 use sha2::{Digest, Sha256};
213 let digest = hex::encode(&Sha256::digest(content.as_bytes())[..6]);
214 let mut f = Fact::new(new_path, MEMORY_FILE_RELATION, &format!("v:{digest}"));
215 f.common.namespace = Some(self.ns.clone());
216 f.common.context = Some(json!({ "content": content }));
217 f.common.embedding_text = Some(crate::migrate::clip_et(&content));
218 f.common.derived_from = Some(old_head.to_hex());
219 f
220 };
221 self.m.add(&f)?;
222 let versions = self.m.history(&self.ns, old_path, MEMORY_FILE_RELATION)?;
223 for v in versions {
224 self.m.forget(&v.hash)?;
225 }
226 let _ = &mut f;
227 Ok(format!("Renamed {old_path} → {new_path}"))
228 }
229}