1use async_trait::async_trait;
2use serde_json::{Value, json};
3use std::sync::{Arc, Mutex};
4
5use crate::error::AgentError;
6use crate::knowledge::memory::MemoryEntry;
7use crate::knowledge::memory::MemoryStore;
8use crate::tools::api::{PermissionDecision, Tool, ToolContext, ToolDefinition, ToolOutput};
9
10mod maintenance;
11pub use maintenance::MemoryMaintenanceTool;
12
13pub struct MemoryReadTool {
16 store: Arc<Mutex<MemoryStore>>,
17}
18
19impl MemoryReadTool {
20 pub fn new(store: Arc<Mutex<MemoryStore>>) -> Self {
21 Self { store }
22 }
23}
24
25#[async_trait]
26impl Tool for MemoryReadTool {
27 fn definition(&self) -> ToolDefinition {
28 ToolDefinition {
29 name: "MemoryRead".into(),
30 description: "Read a memory entry by name. Returns the full content including body."
31 .into(),
32 input_schema: json!({"type":"object","properties":{"name":{"type":"string","description":"Name of the memory to read"}},"required":["name"]}),
33 }
34 }
35 async fn check_permission(
36 &self,
37 _: &Value,
38 _: &ToolContext,
39 ) -> Result<PermissionDecision, AgentError> {
40 Ok(PermissionDecision::Allow)
41 }
42 fn is_concurrency_safe(&self, _: &Value) -> bool {
43 true
44 }
45
46 async fn invoke(&self, args: Value, _: ToolContext) -> Result<ToolOutput, AgentError> {
47 let name = args
48 .get("name")
49 .and_then(|v| v.as_str())
50 .ok_or_else(|| AgentError::Validation("missing `name`".into()))?
51 .to_string();
52 let store = self.store.clone();
53 tokio::task::spawn_blocking(move || {
54 let mut store = store.lock().map_err(|e| AgentError::ToolExecution {
55 tool: "MemoryRead".into(),
56 message: format!("memory store poisoned: {e}"),
57 })?;
58 match store.read(&name) {
59 Some(entry) => {
60 store.record_use(&name).map_err(|e| AgentError::ToolExecution {
61 tool: "MemoryRead".into(),
62 message: e.to_string(),
63 })?;
64 let mut value = serde_json::to_value(&entry)
65 .unwrap_or(json!({"error":"serialization failed"}));
66 value["body"] = json!(entry.body);
67 Ok(ToolOutput::json(value))
68 }
69 None => {
70 Ok(ToolOutput::json(json!({"error": format!("memory '{}' not found", name)})))
71 }
72 }
73 })
74 .await
75 .map_err(|e| AgentError::ToolExecution {
76 tool: "MemoryRead".into(),
77 message: format!("memory task panicked: {e}"),
78 })?
79 }
80}
81
82pub struct MemoryWriteTool {
85 store: Arc<Mutex<MemoryStore>>,
86}
87
88impl MemoryWriteTool {
89 pub fn new(store: Arc<Mutex<MemoryStore>>) -> Self {
90 Self { store }
91 }
92}
93
94#[async_trait]
95impl Tool for MemoryWriteTool {
96 fn definition(&self) -> ToolDefinition {
97 ToolDefinition {
98 name: "MemoryWrite".into(),
99 description:
100 "Write a new memory entry. Categories: script, command, pattern, fact, workflow."
101 .into(),
102 input_schema: json!({"type":"object","properties":{
103 "name":{"type":"string"},
104 "description":{"type":"string"},
105 "category":{"type":"string","enum":["script","command","pattern","fact","workflow"]},
106 "body":{"type":"string","description":"Markdown body content"},
107 "status":{"type":"string","enum":["working","needs_fix","deprecated"],"default":"working"},
108 "confidence":{"type":"string"},
109 "tags":{"type":"array","items":{"type":"string"},"default":[]},
110 "related":{"type":"array","items":{"type":"string"},"default":[]}
111 },"required":["name","description","category","body"]}),
112 }
113 }
114 async fn check_permission(
115 &self,
116 _: &Value,
117 _: &ToolContext,
118 ) -> Result<PermissionDecision, AgentError> {
119 Ok(PermissionDecision::Allow)
120 }
121
122 async fn invoke(&self, args: Value, _: ToolContext) -> Result<ToolOutput, AgentError> {
123 let name = args
124 .get("name")
125 .and_then(|v| v.as_str())
126 .ok_or(AgentError::Validation("missing name".into()))?
127 .to_string();
128 let desc = args.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
129 let cat_str = args
130 .get("category")
131 .and_then(|v| v.as_str())
132 .ok_or(AgentError::Validation("missing category".into()))?
133 .to_string();
134 let body = args.get("body").and_then(|v| v.as_str()).unwrap_or("").to_string();
135 let status_str = args.get("status").and_then(|v| v.as_str()).unwrap_or("working");
136 let status = match status_str {
137 "working" => crate::knowledge::memory::MemoryStatus::Working,
138 "needs_fix" => crate::knowledge::memory::MemoryStatus::NeedsFix,
139 "deprecated" => crate::knowledge::memory::MemoryStatus::Deprecated,
140 _ => return Err(AgentError::Validation(format!("unknown status: {status_str}"))),
141 };
142 let confidence = args.get("confidence").and_then(|v| v.as_str()).map(String::from);
143 let category = match cat_str.as_str() {
144 "script" => crate::knowledge::memory::MemoryCategory::Script,
145 "command" => crate::knowledge::memory::MemoryCategory::Command,
146 "pattern" => crate::knowledge::memory::MemoryCategory::Pattern,
147 "fact" => crate::knowledge::memory::MemoryCategory::Fact,
148 "workflow" => crate::knowledge::memory::MemoryCategory::Workflow,
149 _ => return Err(AgentError::Validation(format!("unknown category: {cat_str}"))),
150 };
151 let tags: Vec<String> = args
152 .get("tags")
153 .and_then(|v| v.as_array())
154 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
155 .unwrap_or_default();
156 let related: Vec<String> = args
157 .get("related")
158 .and_then(|v| v.as_array())
159 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
160 .unwrap_or_default();
161 let now = chrono_now();
162
163 let entry = MemoryEntry {
164 name,
165 description: desc,
166 category,
167 tags,
168 created: now.clone(),
169 updated: now,
170 status,
171 times_used: 0,
172 confidence,
173 related,
174 source_session: None,
175 body,
176 };
177
178 let store = self.store.clone();
179 tokio::task::spawn_blocking(move || {
180 let entry_name = entry.name.clone();
181 let mut store = store.lock().map_err(|e| AgentError::ToolExecution {
182 tool: "MemoryWrite".into(),
183 message: format!("memory store poisoned: {e}"),
184 })?;
185 store.upsert(entry).map_err(|e| AgentError::ToolExecution {
186 tool: "MemoryWrite".into(),
187 message: e.to_string(),
188 })?;
189 Ok(ToolOutput::json(json!({"status": "written", "name": entry_name})))
190 })
191 .await
192 .map_err(|e| AgentError::ToolExecution {
193 tool: "MemoryWrite".into(),
194 message: format!("memory task panicked: {e}"),
195 })?
196 }
197}
198
199pub struct MemoryGrepTool {
202 store: Arc<Mutex<MemoryStore>>,
203}
204
205impl MemoryGrepTool {
206 pub fn new(store: Arc<Mutex<MemoryStore>>) -> Self {
207 Self { store }
208 }
209}
210
211#[async_trait]
212impl Tool for MemoryGrepTool {
213 fn definition(&self) -> ToolDefinition {
214 ToolDefinition {
215 name: "MemoryGrep".into(),
216 description:
217 "Search memories by keyword. Matches against name, description, tags, and body."
218 .into(),
219 input_schema: json!({"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}),
220 }
221 }
222 async fn check_permission(
223 &self,
224 _: &Value,
225 _: &ToolContext,
226 ) -> Result<PermissionDecision, AgentError> {
227 Ok(PermissionDecision::Allow)
228 }
229 fn is_concurrency_safe(&self, _: &Value) -> bool {
230 true
231 }
232
233 async fn invoke(&self, args: Value, _: ToolContext) -> Result<ToolOutput, AgentError> {
234 let query = args
235 .get("query")
236 .and_then(|v| v.as_str())
237 .ok_or(AgentError::Validation("missing query".into()))?
238 .to_string();
239 let store = self.store.clone();
240 tokio::task::spawn_blocking(move || {
241 let store = store.lock().map_err(|e| AgentError::ToolExecution {
242 tool: "MemoryGrep".into(),
243 message: format!("memory store poisoned: {e}"),
244 })?;
245 let results = store.search(&query);
246 let summary: Vec<Value> = results.iter().map(|e| json!({"name": e.name, "description": e.description, "category": format!("{:?}", e.category), "tags": e.tags})).collect();
247 Ok(ToolOutput::json(json!({"results": summary, "count": summary.len()})))
248 })
249 .await
250 .map_err(|e| AgentError::ToolExecution {
251 tool: "MemoryGrep".into(),
252 message: format!("memory task panicked: {e}"),
253 })?
254 }
255}
256
257pub struct MemoryEditTool {
260 store: Arc<Mutex<MemoryStore>>,
261}
262
263impl MemoryEditTool {
264 pub fn new(store: Arc<Mutex<MemoryStore>>) -> Self {
265 Self { store }
266 }
267}
268
269#[async_trait]
270impl Tool for MemoryEditTool {
271 fn definition(&self) -> ToolDefinition {
272 ToolDefinition {
273 name: "MemoryEdit".into(),
274 description: "Edit a memory entry's body content. Replaces the full body.".into(),
275 input_schema: json!({"type":"object","properties":{"name":{"type":"string"},"body":{"type":"string","description":"New body content"}},"required":["name","body"]}),
276 }
277 }
278 async fn check_permission(
279 &self,
280 _: &Value,
281 _: &ToolContext,
282 ) -> Result<PermissionDecision, AgentError> {
283 Ok(PermissionDecision::Allow)
284 }
285
286 async fn invoke(&self, args: Value, _: ToolContext) -> Result<ToolOutput, AgentError> {
287 let name = args
288 .get("name")
289 .and_then(|v| v.as_str())
290 .ok_or(AgentError::Validation("missing name".into()))?
291 .to_string();
292 let body = args.get("body").and_then(|v| v.as_str()).unwrap_or("").to_string();
293 let store = self.store.clone();
294 tokio::task::spawn_blocking(move || {
295 let mut store = store.lock().map_err(|e| AgentError::ToolExecution {
296 tool: "MemoryEdit".into(),
297 message: format!("memory store poisoned: {e}"),
298 })?;
299 if let Some(mut entry) = store.read(&name) {
300 entry.body = body;
301 entry.updated = chrono_now();
302 store.write(entry).map_err(|e| AgentError::ToolExecution {
303 tool: "MemoryEdit".into(),
304 message: e.to_string(),
305 })?;
306 Ok(ToolOutput::json(json!({"status": "updated", "name": name})))
307 } else {
308 Ok(ToolOutput::json(json!({"error": format!("memory '{}' not found", name)})))
309 }
310 })
311 .await
312 .map_err(|e| AgentError::ToolExecution {
313 tool: "MemoryEdit".into(),
314 message: format!("memory task panicked: {e}"),
315 })?
316 }
317}
318
319pub struct MemoryStatusTool {
322 store: Arc<Mutex<MemoryStore>>,
323}
324
325impl MemoryStatusTool {
326 pub fn new(store: Arc<Mutex<MemoryStore>>) -> Self {
327 Self { store }
328 }
329}
330
331#[async_trait]
332impl Tool for MemoryStatusTool {
333 fn definition(&self) -> ToolDefinition {
334 ToolDefinition {
335 name: "MemoryStatus".into(),
336 description: "Update the status of a memory entry: working, needs_fix, or deprecated."
337 .into(),
338 input_schema: json!({"type":"object","properties":{"name":{"type":"string"},"status":{"type":"string","enum":["working","needs_fix","deprecated"]}},"required":["name","status"]}),
339 }
340 }
341 async fn check_permission(
342 &self,
343 _: &Value,
344 _: &ToolContext,
345 ) -> Result<PermissionDecision, AgentError> {
346 Ok(PermissionDecision::Allow)
347 }
348
349 async fn invoke(&self, args: Value, _: ToolContext) -> Result<ToolOutput, AgentError> {
350 let name = args
351 .get("name")
352 .and_then(|v| v.as_str())
353 .ok_or(AgentError::Validation("missing name".into()))?
354 .to_string();
355 let status_str = args
356 .get("status")
357 .and_then(|v| v.as_str())
358 .ok_or(AgentError::Validation("missing status".into()))?
359 .to_string();
360 let status = match status_str.as_str() {
361 "working" => crate::knowledge::memory::MemoryStatus::Working,
362 "needs_fix" => crate::knowledge::memory::MemoryStatus::NeedsFix,
363 "deprecated" => crate::knowledge::memory::MemoryStatus::Deprecated,
364 _ => return Err(AgentError::Validation(format!("unknown status: {status_str}"))),
365 };
366 let store = self.store.clone();
367 tokio::task::spawn_blocking(move || {
368 let mut store = store.lock().map_err(|e| AgentError::ToolExecution {
369 tool: "MemoryStatus".into(),
370 message: format!("memory store poisoned: {e}"),
371 })?;
372 store.update_status(&name, status).map_err(|e| AgentError::ToolExecution {
373 tool: "MemoryStatus".into(),
374 message: e.to_string(),
375 })?;
376 Ok(ToolOutput::json(
377 json!({"status": "updated", "name": name, "new_status": status_str}),
378 ))
379 })
380 .await
381 .map_err(|e| AgentError::ToolExecution {
382 tool: "MemoryStatus".into(),
383 message: format!("memory task panicked: {e}"),
384 })?
385 }
386}
387
388fn is_leap_year(y: u64) -> bool {
389 (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
390}
391
392fn chrono_now() -> String {
393 let now =
396 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
397 let mut days = now.as_secs() / 86400;
398 let mut year: u64 = 1970;
399 loop {
400 let days_in_year: u64 = if is_leap_year(year) { 366 } else { 365 };
401 if days < days_in_year {
402 break;
403 }
404 days -= days_in_year;
405 year += 1;
406 }
407 let ml: [u64; 12] = if is_leap_year(year) {
408 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
409 } else {
410 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
411 };
412 let (mut rem, mut m) = (days, 1usize);
413 for &l in &ml {
414 if rem < l {
415 break;
416 }
417 rem -= l;
418 m += 1;
419 }
420 let d = rem + 1;
421 format!("{year}-{m:02}-{d:02}")
422}