mermaid_cli/providers/tool/
memory.rs1use async_trait::async_trait;
17
18use crate::app::memory::{self, MemoryScope};
19use crate::domain::{ToolDefinition, ToolMetadata, ToolOutcome, ToolRunMetadata};
20
21use super::super::ctx::ExecContext;
22use super::ToolExecutor;
23
24pub struct MemoryTool;
25
26fn scope_from_args(args: &serde_json::Value) -> MemoryScope {
27 if args
28 .get("global")
29 .and_then(|v| v.as_bool())
30 .unwrap_or(false)
31 {
32 MemoryScope::Global
33 } else if args
34 .get("shared")
35 .and_then(|v| v.as_bool())
36 .unwrap_or(false)
37 {
38 MemoryScope::ProjectShared
39 } else {
40 MemoryScope::ProjectPrivate
41 }
42}
43
44fn str_arg(args: &serde_json::Value, key: &str) -> Option<String> {
45 args.get(key)
46 .and_then(|v| v.as_str())
47 .map(str::trim)
48 .filter(|s| !s.is_empty())
49 .map(str::to_string)
50}
51
52fn tags_arg(args: &serde_json::Value) -> Vec<String> {
53 args.get("tags")
54 .and_then(|v| v.as_array())
55 .map(|a| {
56 a.iter()
57 .filter_map(|t| t.as_str())
58 .map(str::to_string)
59 .collect()
60 })
61 .unwrap_or_default()
62}
63
64#[async_trait]
65impl ToolExecutor for MemoryTool {
66 fn name(&self) -> &'static str {
67 "memory"
68 }
69
70 fn schema(&self) -> ToolDefinition {
71 ToolDefinition {
72 name: "memory".to_string(),
73 description: "Manage your durable, cross-session memory of semantic facts: preferences, project conventions, decisions and their rationale, and hard-won gotchas. The memory index is always in your context; use this tool to change it. \
74 `action=remember` saves a new fact; `action=update` replaces one fact's body (pass its `id`); `action=forget` deletes a fact (pass its `id`). \
75 Keep each fact atomic — one idea per memory — and never merge or re-summarize the whole corpus. \
76 Default scope is project-private (machine-local, not committed); set `shared=true` for team facts committed to the repo's .mermaid/memory, or `global=true` for facts that apply across all projects. \
77 Save durable knowledge, not transient task state, and never store secrets, tokens, or PII."
78 .to_string(),
79 input_schema: serde_json::json!({
80 "type": "object",
81 "properties": {
82 "action": {
83 "type": "string",
84 "enum": ["remember", "update", "forget"],
85 "description": "What to do."
86 },
87 "name": {
88 "type": "string",
89 "description": "Short title for the fact; also the filename. Required for remember."
90 },
91 "content": {
92 "type": "string",
93 "description": "The fact itself (Markdown). Required for remember and update."
94 },
95 "description": {
96 "type": "string",
97 "description": "One-line summary shown in the always-loaded index. Defaults to the first line of content."
98 },
99 "tags": {
100 "type": "array",
101 "items": { "type": "string" },
102 "description": "Optional keywords."
103 },
104 "shared": {
105 "type": "boolean",
106 "description": "Store in the committed project-shared scope (.mermaid/memory) instead of private."
107 },
108 "global": {
109 "type": "boolean",
110 "description": "Store in the global, cross-project scope."
111 },
112 "id": {
113 "type": "string",
114 "description": "Name/id of the fact to update or forget (as shown in the index)."
115 }
116 },
117 "required": ["action"]
118 }),
119 }
120 }
121
122 async fn execute(&self, args: serde_json::Value, ctx: ExecContext) -> ToolOutcome {
123 let start = std::time::Instant::now();
124 let action = str_arg(&args, "action").unwrap_or_default();
125
126 if let Some(blocked) = super::policy_gate::gate_external(
129 &ctx,
130 "memory",
131 crate::runtime::ToolCategory::Memory,
132 format!("memory {action}"),
133 &args,
134 )
135 .await
136 {
137 return blocked;
138 }
139
140 let secs = || start.elapsed().as_secs_f64();
141 match action.as_str() {
142 "remember" => {
143 let Some(name) = str_arg(&args, "name") else {
144 return ToolOutcome::error("memory remember requires 'name'", secs());
145 };
146 let Some(content) = args.get("content").and_then(|v| v.as_str()) else {
147 return ToolOutcome::error("memory remember requires 'content'", secs());
148 };
149 let scope = scope_from_args(&args);
150 let description = str_arg(&args, "description").unwrap_or_else(|| {
151 content
152 .lines()
153 .find(|l| !l.trim().is_empty())
154 .unwrap_or("")
155 .to_string()
156 });
157 let tags = tags_arg(&args);
158 match memory::write_memory(&ctx.workdir, scope, &name, &description, &tags, content)
159 {
160 Ok(path) => finish(start, "remember", &name, scope, &path),
161 Err(e) => ToolOutcome::error(format!("memory remember failed: {e}"), secs()),
162 }
163 },
164 "update" => {
165 let Some(id) = str_arg(&args, "id").or_else(|| str_arg(&args, "name")) else {
166 return ToolOutcome::error("memory update requires 'id'", secs());
167 };
168 let Some(content) = args.get("content").and_then(|v| v.as_str()) else {
169 return ToolOutcome::error("memory update requires 'content'", secs());
170 };
171 let Some(existing) = memory::find(&ctx.workdir, &id) else {
172 return ToolOutcome::error(
173 format!("memory update: no memory named '{id}'"),
174 secs(),
175 );
176 };
177 let description =
178 str_arg(&args, "description").unwrap_or_else(|| existing.description.clone());
179 let tags = tags_arg(&args);
180 match memory::write_memory(
181 &ctx.workdir,
182 existing.scope,
183 &existing.name,
184 &description,
185 &tags,
186 content,
187 ) {
188 Ok(path) => {
189 if path != existing.path {
193 let _ = std::fs::remove_file(&existing.path);
194 }
195 finish(start, "update", &existing.name, existing.scope, &path)
196 },
197 Err(e) => ToolOutcome::error(format!("memory update failed: {e}"), secs()),
198 }
199 },
200 "forget" => {
201 let Some(id) = str_arg(&args, "id").or_else(|| str_arg(&args, "name")) else {
202 return ToolOutcome::error("memory forget requires 'id'", secs());
203 };
204 match memory::delete_memory(&ctx.workdir, &id) {
205 Ok(Some(path)) => ToolOutcome::success(
206 format!("Forgot memory '{id}' ({})", path.display()),
207 format!("forgot {id}"),
208 secs(),
209 )
210 .with_metadata(ToolRunMetadata {
211 detail: ToolMetadata::Custom {
212 name: "memory".to_string(),
213 data: serde_json::json!({
214 "action": "forget",
215 "id": id,
216 "path": path.display().to_string(),
217 }),
218 },
219 ..ToolRunMetadata::default()
220 }),
221 Ok(None) => {
222 ToolOutcome::error(format!("memory forget: no memory named '{id}'"), secs())
223 },
224 Err(e) => ToolOutcome::error(format!("memory forget failed: {e}"), secs()),
225 }
226 },
227 other => ToolOutcome::error(
228 format!("memory: unknown action '{other}' (expected remember, update, or forget)"),
229 secs(),
230 ),
231 }
232 }
233}
234
235fn finish(
237 start: std::time::Instant,
238 action: &str,
239 name: &str,
240 scope: MemoryScope,
241 path: &std::path::Path,
242) -> ToolOutcome {
243 let verb = if action == "remember" {
244 "Remembered"
245 } else {
246 "Updated"
247 };
248 ToolOutcome::success(
249 format!("{verb} '{name}' [{}] → {}", scope.as_str(), path.display()),
250 format!("{action} {name} [{}]", scope.as_str()),
251 start.elapsed().as_secs_f64(),
252 )
253 .with_metadata(ToolRunMetadata {
254 detail: ToolMetadata::Custom {
255 name: "memory".to_string(),
256 data: serde_json::json!({
257 "action": action,
258 "name": name,
259 "scope": scope.as_str(),
260 "path": path.display().to_string(),
261 }),
262 },
263 ..ToolRunMetadata::default()
264 })
265}