1use async_trait::async_trait;
2use serde_json::json;
3
4use super::{Tool, ToolContext, ToolOutput};
5use crate::error::Result;
6use crate::memory::MemoryStore;
7use crate::storage;
8
9const DEFAULT_MEMORY_LIMIT: usize = 2200;
10const DEFAULT_USER_LIMIT: usize = 1400;
11
12pub struct MemoryTool;
13
14#[async_trait]
15impl Tool for MemoryTool {
16 fn name(&self) -> &str {
17 "memory"
18 }
19
20 fn label(&self) -> &str {
21 "Memory"
22 }
23
24 fn description(&self) -> &str {
25 "Manage persistent memory across sessions. Use to save environment facts, \
26 user preferences, and lessons learned. Target 'memory' for agent notes, \
27 'user' for user profile."
28 }
29
30 fn parameters(&self) -> serde_json::Value {
31 json!({
32 "type": "object",
33 "required": ["action", "target"],
34 "properties": {
35 "action": {
36 "type": "string",
37 "enum": ["add", "replace", "remove"],
38 "description": "Action to perform"
39 },
40 "target": {
41 "type": "string",
42 "enum": ["memory", "user"],
43 "description": "Which store: 'memory' for agent notes, 'user' for user profile"
44 },
45 "content": {
46 "type": "string",
47 "description": "Content to add or replacement text"
48 },
49 "old_text": {
50 "type": "string",
51 "description": "Unique substring identifying the entry to replace or remove"
52 }
53 }
54 })
55 }
56
57 fn is_readonly(&self) -> bool {
58 false
59 }
60
61 async fn execute(
62 &self,
63 _call_id: &str,
64 params: serde_json::Value,
65 _ctx: ToolContext,
66 ) -> Result<ToolOutput> {
67 let action = params["action"].as_str().unwrap_or("");
68 let target = params["target"].as_str().unwrap_or("");
69
70 if action.is_empty() {
71 return Ok(ToolOutput::error("Missing required parameter: action"));
72 }
73 if target.is_empty() {
74 return Ok(ToolOutput::error("Missing required parameter: target"));
75 }
76
77 let (path, char_limit) = match target {
78 "memory" => (storage::global_memory_path(), DEFAULT_MEMORY_LIMIT),
79 "user" => (storage::global_user_path(), DEFAULT_USER_LIMIT),
80 other => {
81 return Ok(ToolOutput::error(format!(
82 "Unknown target \"{other}\". Use \"memory\" or \"user\"."
83 )));
84 }
85 };
86
87 let mut store = match MemoryStore::load(&path, char_limit) {
88 Ok(s) => s,
89 Err(e) => return Ok(ToolOutput::error(format!("Failed to load memory: {e}"))),
90 };
91
92 let result = match action {
93 "add" => {
94 let content = params["content"].as_str().unwrap_or("");
95 if content.is_empty() {
96 return Ok(ToolOutput::error(
97 "Missing required parameter: content (for 'add' action)",
98 ));
99 }
100 store.add(content)?
101 }
102 "replace" => {
103 let old_text = params["old_text"].as_str().unwrap_or("");
104 let content = params["content"].as_str().unwrap_or("");
105 if old_text.is_empty() {
106 return Ok(ToolOutput::error(
107 "Missing required parameter: old_text (for 'replace' action)",
108 ));
109 }
110 if content.is_empty() {
111 return Ok(ToolOutput::error(
112 "Missing required parameter: content (for 'replace' action)",
113 ));
114 }
115 store.replace(old_text, content)?
116 }
117 "remove" => {
118 let old_text = params["old_text"].as_str().unwrap_or("");
119 if old_text.is_empty() {
120 return Ok(ToolOutput::error(
121 "Missing required parameter: old_text (for 'remove' action)",
122 ));
123 }
124 store.remove(old_text)?
125 }
126 other => {
127 return Ok(ToolOutput::error(format!(
128 "Unknown action \"{other}\". Use \"add\", \"replace\", or \"remove\"."
129 )));
130 }
131 };
132
133 let json_text = serde_json::to_string_pretty(&result.to_json()).unwrap_or_default();
134 if result.success {
135 Ok(ToolOutput::text(json_text))
136 } else {
137 Ok(ToolOutput::error(json_text))
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::tools::ToolContext;
146 use std::sync::Arc;
147
148 fn test_ctx() -> ToolContext {
149 let (tx, _rx) = tokio::sync::mpsc::channel(16);
150 let (cmd_tx, _cmd_rx) = tokio::sync::mpsc::channel(16);
151 let dir = std::env::temp_dir();
152 ToolContext {
153 cwd: dir,
154 cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
155 update_tx: tx,
156 command_tx: cmd_tx,
157 ui: Arc::new(crate::ui::NullInterface),
158 file_cache: Arc::new(crate::tools::FileCache::new()),
159 checkpoint_state: Arc::new(crate::tools::CheckpointState::new()),
160 file_tracker: Arc::new(std::sync::Mutex::new(crate::tools::FileTracker::new())),
161 anchor_store: Arc::new(crate::tools::AnchorStore::new()),
162 lua_tool_loader: None,
163 mode: crate::config::AgentMode::Full,
164 read_max_lines: 500,
165 turn_mana_review: Arc::new(std::sync::Mutex::new(
166 crate::mana_review::TurnManaReviewAccumulator::default(),
167 )),
168 config: Arc::new(crate::config::Config::default()),
169 }
170 }
171
172 #[tokio::test]
173 async fn memory_tool_validates_params() {
174 let tool = MemoryTool;
175
176 let r = tool
178 .execute("c1", json!({"target": "memory"}), test_ctx())
179 .await
180 .unwrap();
181 assert!(r.is_error);
182
183 let r = tool
185 .execute("c2", json!({"action": "add"}), test_ctx())
186 .await
187 .unwrap();
188 assert!(r.is_error);
189
190 let r = tool
192 .execute(
193 "c3",
194 json!({"action": "add", "target": "memory"}),
195 test_ctx(),
196 )
197 .await
198 .unwrap();
199 assert!(r.is_error);
200 }
201}