lc_agents/executor/
file_memory_tool.rs1use std::path::PathBuf;
23use std::sync::Arc;
24
25use async_trait::async_trait;
26use serde_json::Value;
27
28use lc_core::tools::{BaseTool, ToolError};
29use lc_memory::file_memory::{FileMemoryError, FileMemoryStore};
30
31pub const FILE_MEMORY_TOOL_NAME: &str = "file_memory";
33
34const DESCRIPTION: &str = "\
36Persistent file-backed agent memory. Ops:
37- view: read a memory's text (name)
38- create: write a new named memory (name, content)
39- write: overwrite an existing memory (name, content)
40- append: append text to a memory (name, content)
41- delete: remove a memory (name)
42- list: list existing memory names
43Input JSON: {\"op\": \"view\", \"name\": \"user_profile\"}";
44
45pub struct FileMemoryTool {
48 store: FileMemoryStore,
49}
50
51impl FileMemoryTool {
52 pub fn new(root: impl Into<PathBuf>) -> Result<Self, FileMemoryError> {
54 Ok(Self {
55 store: FileMemoryStore::new(root)?,
56 })
57 }
58}
59
60#[async_trait]
61impl BaseTool for FileMemoryTool {
62 fn name(&self) -> &str {
63 FILE_MEMORY_TOOL_NAME
64 }
65
66 fn description(&self) -> &str {
67 DESCRIPTION
68 }
69
70 async fn run(&self, input: String) -> Result<String, ToolError> {
71 let parsed: Value = serde_json::from_str(&input)
72 .map_err(|e| ToolError::InvalidInput(format!("memory input not JSON: {e}")))?;
73 let op = parsed
74 .get("op")
75 .and_then(Value::as_str)
76 .ok_or_else(|| ToolError::InvalidInput("missing required field 'op'".into()))?;
77 let name = parsed.get("name").and_then(Value::as_str);
78 let content = parsed.get("content").and_then(Value::as_str).unwrap_or("");
79
80 match op {
81 "view" => {
82 let n = require_name(name)?;
83 self.store
84 .view(n)
85 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))
86 }
87 "list" => {
88 let entries = self
89 .store
90 .list()
91 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
92 if entries.is_empty() {
93 Ok("(no memories)".to_string())
94 } else {
95 let names: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
96 Ok(format!("memories: {}", names.join(", ")))
97 }
98 }
99 "create" => {
100 let n = require_name(name)?;
101 self.store
102 .create(n, content)
103 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
104 Ok(format!("created memory '{n}'"))
105 }
106 "write" => {
107 let n = require_name(name)?;
108 self.store
109 .write(n, content)
110 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
111 Ok(format!("wrote memory '{n}'"))
112 }
113 "append" => {
114 let n = require_name(name)?;
115 self.store
116 .append(n, content)
117 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
118 Ok(format!("appended to memory '{n}'"))
119 }
120 "delete" => {
121 let n = require_name(name)?;
122 self.store
123 .delete(n)
124 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
125 Ok(format!("deleted memory '{n}'"))
126 }
127 other => Err(ToolError::InvalidInput(format!(
128 "unknown op '{other}' (expected view|create|write|append|delete|list)"
129 ))),
130 }
131 }
132}
133
134fn require_name(name: Option<&str>) -> Result<&str, ToolError> {
135 name.filter(|s| !s.is_empty())
136 .ok_or_else(|| ToolError::InvalidInput("missing required field 'name'".into()))
137}
138
139impl std::fmt::Debug for FileMemoryTool {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 f.debug_struct("FileMemoryTool").finish()
142 }
143}
144
145pub fn mount(root: impl Into<PathBuf>) -> Result<Arc<dyn BaseTool>, FileMemoryError> {
147 Ok(Arc::new(FileMemoryTool::new(root)?))
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn temp_tool() -> (tempfile::TempDir, FileMemoryTool) {
155 let dir = tempfile::tempdir().unwrap();
156 let tool = FileMemoryTool::new(dir.path()).unwrap();
157 (dir, tool)
158 }
159
160 async fn run(tool: &FileMemoryTool, s: &str) -> Result<String, ToolError> {
161 tool.run(s.to_string()).await
162 }
163
164 #[tokio::test]
165 async fn create_view_write_roundtrip() {
166 let (_d, tool) = temp_tool();
167 let out = run(
168 &tool,
169 r#"{"op":"create","name":"profile","content":"Alice"}"#,
170 )
171 .await
172 .unwrap();
173 assert!(out.contains("created memory 'profile'"));
174 let out = run(&tool, r#"{"op":"view","name":"profile"}"#)
175 .await
176 .unwrap();
177 assert_eq!(out, "Alice");
178 let out = run(
179 &tool,
180 r#"{"op":"append","name":"profile","content":" / engineer"}"#,
181 )
182 .await
183 .unwrap();
184 assert!(out.contains("appended"));
185 let view = run(&tool, r#"{"op":"view","name":"profile"}"#)
186 .await
187 .unwrap();
188 assert!(view.contains("engineer"));
189 let out = run(&tool, r#"{"op":"write","name":"profile","content":"Bob"}"#)
190 .await
191 .unwrap();
192 assert!(out.contains("wrote memory 'profile'"));
193 let view = run(&tool, r#"{"op":"view","name":"profile"}"#)
194 .await
195 .unwrap();
196 assert_eq!(view, "Bob");
197 }
198
199 #[tokio::test]
200 async fn list_and_delete() {
201 let (_d, tool) = temp_tool();
202 run(&tool, r#"{"op":"create","name":"a","content":"1"}"#)
203 .await
204 .unwrap();
205 run(&tool, r#"{"op":"create","name":"b","content":"2"}"#)
206 .await
207 .unwrap();
208 let list = run(&tool, r#"{"op":"list"}"#).await.unwrap();
209 assert!(list.contains("a") && list.contains("b"));
210 run(&tool, r#"{"op":"delete","name":"a"}"#).await.unwrap();
211 let list = run(&tool, r#"{"op":"list"}"#).await.unwrap();
212 assert!(!list.contains("a"));
213 }
214
215 #[tokio::test]
216 async fn missing_fields_error() {
217 let (_d, tool) = temp_tool();
218 assert!(matches!(
219 tool.run(r#"{}"#.to_string()).await.unwrap_err(),
220 ToolError::InvalidInput(_)
221 ));
222 let err = tool.run(r#"{"op":"view"}"#.to_string()).await.unwrap_err();
223 assert!(matches!(err, ToolError::InvalidInput(_)));
224 let err = tool
225 .run(r#"{"op":"bogus","name":"x"}"#.to_string())
226 .await
227 .unwrap_err();
228 assert!(matches!(err, ToolError::InvalidInput(_)));
229 }
230
231 #[tokio::test]
232 async fn view_missing_memory_is_execution_error_not_invalid_input() {
233 let (_d, tool) = temp_tool();
234 let err = tool
236 .run(r#"{"op":"view","name":"nope"}"#.to_string())
237 .await
238 .unwrap_err();
239 assert!(matches!(err, ToolError::ExecutionFailed(_)));
240 }
241
242 #[test]
243 fn mount_returns_arc_base_tool() {
244 let dir = tempfile::tempdir().unwrap();
245 let arc = mount(dir.path()).unwrap();
246 assert_eq!(arc.name(), FILE_MEMORY_TOOL_NAME);
247 }
248}