Skip to main content

bamboo_server_tools/memory/
mod.rs

1use async_trait::async_trait;
2use serde_json::json;
3
4use bamboo_agent_core::tools::{Tool, ToolError, ToolExecutionContext, ToolResult};
5use bamboo_agent_core::Session;
6use bamboo_memory::memory_store::{
7    DurableMemoryStatus, MemoryQueryOptions, MemoryScope, MemoryStore, MAX_MAX_CHARS,
8    MAX_QUERY_LIMIT,
9};
10use bamboo_tools::tools::session_memory::{
11    execute_session_memory_action, SessionMemoryAction, MEMORY_SESSION_ACTION_NAMES,
12};
13
14mod args;
15mod parsing;
16
17#[cfg(test)]
18mod tests;
19
20use args::MemoryArgs;
21
22#[derive(Clone)]
23pub struct MemoryTool {
24    session_repo: bamboo_engine::SessionRepository,
25    memory_store: MemoryStore,
26}
27
28impl MemoryTool {
29    pub fn new(
30        session_repo: bamboo_engine::SessionRepository,
31        data_dir: impl Into<std::path::PathBuf>,
32    ) -> Self {
33        Self {
34            session_repo,
35            memory_store: MemoryStore::new(data_dir),
36        }
37    }
38
39    async fn session_for_context(&self, session_id: Option<&str>) -> Option<Session> {
40        self.session_repo.load(session_id?).await
41    }
42
43    async fn resolve_project_key(
44        &self,
45        explicit: Option<&str>,
46        session_id: Option<&str>,
47    ) -> Option<String> {
48        if let Some(explicit) = explicit
49            .map(str::trim)
50            .filter(|value| !value.is_empty())
51            .map(ToString::to_string)
52        {
53            return Some(explicit);
54        }
55
56        if let Some(project_key) = self.memory_store.project_key_for_session(session_id) {
57            return Some(project_key);
58        }
59
60        self.session_for_context(session_id)
61            .await
62            .and_then(|session| session.metadata.get("workspace_path").cloned())
63            .map(std::path::PathBuf::from)
64            .map(|path| bamboo_memory::memory_store::project_key_from_path(&path))
65    }
66}
67
68#[async_trait]
69impl Tool for MemoryTool {
70    fn name(&self) -> &str {
71        "memory"
72    }
73
74    fn description(&self) -> &str {
75        "Unified memory management tool for Bamboo. Use session_* actions for session continuity notes, and query/get/write/merge/split/consolidate/purge/inspect/rebuild for durable project/global memory backed by canonical topic files and derived indexes."
76    }
77
78    fn parameters_schema(&self) -> serde_json::Value {
79        json!({
80            "type": "object",
81            "properties": {
82                "action": {
83                    "type": "string",
84                    "enum": [
85                        "session_read",
86                        "session_append",
87                        "session_replace",
88                        "session_clear",
89                        "session_list_topics",
90                        "query",
91                        "get",
92                        "find_duplicates",
93                        "write",
94                        "merge",
95                        "split",
96                        "consolidate",
97                        "purge",
98                        "inspect",
99                        "rebuild",
100                        "scan_blobs",
101                        "scan_duplicates"
102                    ]
103                },
104                "scope": {"type": "string", "enum": ["session", "project", "global"]},
105                "granularity": {
106                    "type": "string",
107                    "enum": ["day", "week", "month", "quarter", "year"],
108                    "description": "Optional temporal granularity for `write`, orthogonal to scope: day (today's working context), week (sprint), month, quarter (direction), year (long-term goals). Omit if the memory has no time horizon. Coarser granularities are prefix-cache friendly and recalled ahead of finer ones at equal relevance."
109                },
110                "project_key": {"type": "string"},
111                "topic": {"type": "string"},
112                "id": {"type": "string"},
113                "query": {"type": "string"},
114                "type": {"type": "string", "enum": ["user", "feedback", "project", "reference"]},
115                "title": {"type": "string"},
116                "content": {"type": "string"},
117                "tags": {"type": "array", "items": {"type": "string"}},
118                "pieces": {"type": "array", "items": {"type": "object"}},
119                "ids": {"type": "array", "items": {"type": "string"}},
120                "min_score": {"type": "number"},
121                "filters": {"type": "object"},
122                "options": {"type": "object"},
123                "reason": {"type": "string"}
124            },
125            "required": ["action"]
126        })
127    }
128
129    fn call_mutability(&self, args: &serde_json::Value) -> bamboo_tools::ToolMutability {
130        let action = args
131            .get("action")
132            .and_then(|value| value.as_str())
133            .unwrap_or("")
134            .trim()
135            .to_ascii_lowercase();
136        match action.as_str() {
137            "session_read"
138            | "session_list_topics"
139            | "query"
140            | "get"
141            | "find_duplicates"
142            | "scan_blobs"
143            | "scan_duplicates"
144            | "inspect" => bamboo_tools::ToolMutability::ReadOnly,
145            _ => bamboo_tools::ToolMutability::Mutating,
146        }
147    }
148
149    fn call_concurrency_safe(&self, args: &serde_json::Value) -> bool {
150        matches!(
151            self.call_mutability(args),
152            bamboo_tools::ToolMutability::ReadOnly
153        )
154    }
155
156    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
157        self.execute_with_context(args, ToolExecutionContext::none("tool_call"))
158            .await
159    }
160
161    async fn execute_with_context(
162        &self,
163        args: serde_json::Value,
164        ctx: ToolExecutionContext<'_>,
165    ) -> Result<ToolResult, ToolError> {
166        let session_id = ctx.session_id.ok_or_else(|| {
167            ToolError::Execution("memory requires a session_id in tool context".to_string())
168        })?;
169
170        let parsed: MemoryArgs = serde_json::from_value(args).map_err(|error| {
171            ToolError::InvalidArguments(format!("Invalid memory args: {error}"))
172        })?;
173
174        match parsed {
175            MemoryArgs::SessionRead { topic, options } => {
176                let max_chars = options.and_then(|value| value.max_chars);
177                execute_session_memory_action(
178                    &self.memory_store,
179                    session_id,
180                    SessionMemoryAction::Read,
181                    topic.as_deref(),
182                    None,
183                    max_chars,
184                    MEMORY_SESSION_ACTION_NAMES,
185                )
186                .await
187            }
188            MemoryArgs::SessionAppend { topic, content } => {
189                execute_session_memory_action(
190                    &self.memory_store,
191                    session_id,
192                    SessionMemoryAction::Append,
193                    topic.as_deref(),
194                    Some(content.as_str()),
195                    None,
196                    MEMORY_SESSION_ACTION_NAMES,
197                )
198                .await
199            }
200            MemoryArgs::SessionReplace { topic, content } => {
201                execute_session_memory_action(
202                    &self.memory_store,
203                    session_id,
204                    SessionMemoryAction::Replace,
205                    topic.as_deref(),
206                    Some(content.as_str()),
207                    None,
208                    MEMORY_SESSION_ACTION_NAMES,
209                )
210                .await
211            }
212            MemoryArgs::SessionClear { topic } => {
213                execute_session_memory_action(
214                    &self.memory_store,
215                    session_id,
216                    SessionMemoryAction::Clear,
217                    topic.as_deref(),
218                    None,
219                    None,
220                    MEMORY_SESSION_ACTION_NAMES,
221                )
222                .await
223            }
224            MemoryArgs::SessionListTopics => {
225                execute_session_memory_action(
226                    &self.memory_store,
227                    session_id,
228                    SessionMemoryAction::ListTopics,
229                    None,
230                    None,
231                    None,
232                    MEMORY_SESSION_ACTION_NAMES,
233                )
234                .await
235            }
236            MemoryArgs::Query {
237                scope,
238                query,
239                filters,
240                project_key,
241                options,
242            } => {
243                let scope = Self::parse_scope(Some(&scope))?;
244                if scope == MemoryScope::Session {
245                    return Err(ToolError::InvalidArguments(
246                        "query supports durable scopes only; use session_read/session_list_topics for session scope"
247                            .to_string(),
248                    ));
249                }
250                let project_key = self
251                    .resolve_project_key(project_key.as_deref(), Some(session_id))
252                    .await;
253                let options = MemoryQueryOptions {
254                    limit: options
255                        .as_ref()
256                        .and_then(|value| value.limit)
257                        .map(|value| value.min(MAX_QUERY_LIMIT)),
258                    max_chars: options
259                        .as_ref()
260                        .and_then(|value| value.max_chars)
261                        .map(|value| value.min(MAX_MAX_CHARS)),
262                    cursor: options.as_ref().and_then(|value| value.cursor.clone()),
263                    include_related: options
264                        .as_ref()
265                        .and_then(|value| value.include_related)
266                        .unwrap_or(false),
267                };
268                let (filter_types, filter_statuses) = Self::parse_query_filters(filters.as_ref())?;
269                let result = self
270                    .memory_store
271                    .query_scope(
272                        scope,
273                        project_key.as_deref(),
274                        query.as_deref(),
275                        filter_types.as_ref(),
276                        filter_statuses.as_ref(),
277                        &options,
278                    )
279                    .await
280                    .map_err(|error| {
281                        ToolError::Execution(format!("Failed to query memory: {error}"))
282                    })?;
283                Ok(ToolResult {
284                    success: true,
285                    result: json!({
286                        "action": "query",
287                        "success": true,
288                        "data": result,
289                        "summary": bamboo_memory::memory_store::summary_json(result.returned_count, result.matched_count),
290                        "warnings": [],
291                    }).to_string(),
292                    display_preference: Some("json".to_string()),
293                    images: Vec::new(),
294                })
295            }
296            MemoryArgs::Get {
297                id,
298                project_key,
299                options,
300            } => {
301                let project_key = self
302                    .resolve_project_key(project_key.as_deref(), Some(session_id))
303                    .await;
304                let max_chars = options
305                    .and_then(|value| value.max_chars)
306                    .unwrap_or(MAX_MAX_CHARS)
307                    .min(MAX_MAX_CHARS);
308                let Some(mut doc) = self
309                    .memory_store
310                    .get_memory(id.trim(), project_key.as_deref())
311                    .await
312                    .map_err(|error| {
313                        ToolError::Execution(format!("Failed to get memory: {error}"))
314                    })?
315                else {
316                    return Err(ToolError::Execution(format!(
317                        "memory not found: {}",
318                        id.trim()
319                    )));
320                };
321                let (body, truncated) =
322                    bamboo_memory::memory_store::truncate_chars(&doc.body, max_chars);
323                doc.body = body;
324                Ok(ToolResult {
325                    success: true,
326                    result: json!({
327                        "action": "get",
328                        "id": doc.frontmatter.id,
329                        "memory": {
330                            "frontmatter": doc.frontmatter,
331                            "body": doc.body,
332                            "path": doc.path,
333                            "body_truncated": truncated,
334                        }
335                    })
336                    .to_string(),
337                    display_preference: Some("json".to_string()),
338                    images: Vec::new(),
339                })
340            }
341            MemoryArgs::Write {
342                scope,
343                r#type,
344                title,
345                content,
346                tags,
347                project_key,
348                granularity,
349                options,
350            } => {
351                let scope = Self::parse_scope(Some(&scope))?;
352                if scope == MemoryScope::Session {
353                    return Err(ToolError::InvalidArguments(
354                        "write supports durable scopes only; use session_replace/session_append for session scope"
355                            .to_string(),
356                    ));
357                }
358                let granularity = Self::parse_granularity(granularity.as_deref())?;
359                let project_key = self
360                    .resolve_project_key(project_key.as_deref(), Some(session_id))
361                    .await;
362                let doc = self
363                    .memory_store
364                    .write_memory(
365                        scope,
366                        project_key.as_deref(),
367                        Self::parse_type(&r#type)?,
368                        &title,
369                        &content,
370                        &tags,
371                        Some(session_id),
372                        "main-model",
373                        options
374                            .and_then(|value| value.allow_merge_if_similar)
375                            .unwrap_or(false),
376                        granularity,
377                    )
378                    .await
379                    .map_err(|error| {
380                        ToolError::Execution(format!("Failed to write memory: {error}"))
381                    })?;
382                Ok(ToolResult {
383                    success: true,
384                    result: json!({
385                        "action": "write",
386                        "memory": {
387                            "id": doc.frontmatter.id,
388                            "title": doc.frontmatter.title,
389                            "type": doc.frontmatter.r#type,
390                            "scope": doc.frontmatter.scope,
391                            "status": doc.frontmatter.status,
392                            "project_key": doc.frontmatter.project_key,
393                            "path": doc.path,
394                        }
395                    })
396                    .to_string(),
397                    display_preference: Some("json".to_string()),
398                    images: Vec::new(),
399                })
400            }
401            MemoryArgs::Merge {
402                id,
403                content,
404                tags,
405                project_key,
406                source_memory_ids,
407                mode,
408                reason,
409            } => {
410                let project_key = self
411                    .resolve_project_key(project_key.as_deref(), Some(session_id))
412                    .await;
413                let mode = Self::parse_merge_mode(mode.as_deref())?;
414                if matches!(mode.as_deref(), Some("contradict")) {
415                    let Some(result) = self
416                        .memory_store
417                        .mark_memory_contradicted(
418                            id.trim(),
419                            project_key.as_deref(),
420                            &source_memory_ids,
421                            reason.as_deref().or(Some(content.trim())),
422                            Some(session_id),
423                            "main-model",
424                        )
425                        .await
426                        .map_err(|error| {
427                            ToolError::Execution(format!("Failed to contradict memory: {error}"))
428                        })?
429                    else {
430                        return Err(ToolError::Execution(format!(
431                            "memory not found: {}",
432                            id.trim()
433                        )));
434                    };
435                    Ok(ToolResult {
436                        success: true,
437                        result: json!({
438                            "action": "merge",
439                            "mode": "contradict",
440                            "data": result,
441                        })
442                        .to_string(),
443                        display_preference: Some("json".to_string()),
444                        images: Vec::new(),
445                    })
446                } else {
447                    let Some(result) = self
448                        .memory_store
449                        .merge_memory(
450                            id.trim(),
451                            project_key.as_deref(),
452                            &content,
453                            &tags,
454                            Some(session_id),
455                            "main-model",
456                            &source_memory_ids,
457                        )
458                        .await
459                        .map_err(|error| {
460                            ToolError::Execution(format!("Failed to merge memory: {error}"))
461                        })?
462                    else {
463                        return Err(ToolError::Execution(format!(
464                            "memory not found: {}",
465                            id.trim()
466                        )));
467                    };
468                    Ok(ToolResult {
469                        success: true,
470                        result: json!({
471                            "action": "merge",
472                            "mode": mode.unwrap_or_else(|| "merge".to_string()),
473                            "data": result,
474                        })
475                        .to_string(),
476                        display_preference: Some("json".to_string()),
477                        images: Vec::new(),
478                    })
479                }
480            }
481            MemoryArgs::FindDuplicates {
482                scope,
483                title,
484                content,
485                r#type,
486                tags,
487                project_key,
488                options,
489            } => {
490                let scope = Self::parse_scope(Some(&scope))?;
491                if scope == MemoryScope::Session {
492                    return Err(ToolError::InvalidArguments(
493                        "find_duplicates supports durable scopes only".to_string(),
494                    ));
495                }
496                let r#type = match r#type.as_deref() {
497                    Some(value) => Some(Self::parse_type(value)?),
498                    None => None,
499                };
500                let project_key = self
501                    .resolve_project_key(project_key.as_deref(), Some(session_id))
502                    .await;
503                let limit = options
504                    .and_then(|value| value.limit)
505                    .unwrap_or(5)
506                    .clamp(1, MAX_QUERY_LIMIT);
507                let candidates = self
508                    .memory_store
509                    .find_duplicate_candidates(
510                        scope,
511                        project_key.as_deref(),
512                        r#type,
513                        &title,
514                        content.as_deref().unwrap_or(""),
515                        &tags,
516                        limit,
517                    )
518                    .await
519                    .map_err(|error| {
520                        ToolError::Execution(format!("Failed to find duplicates: {error}"))
521                    })?;
522                Ok(ToolResult {
523                    success: true,
524                    result: json!({
525                        "action": "find_duplicates",
526                        "candidates": candidates,
527                    })
528                    .to_string(),
529                    display_preference: Some("json".to_string()),
530                    images: Vec::new(),
531                })
532            }
533            MemoryArgs::Split {
534                id,
535                project_key,
536                pieces,
537            } => {
538                if pieces.is_empty() {
539                    return Err(ToolError::InvalidArguments(
540                        "split requires at least one piece".to_string(),
541                    ));
542                }
543                let project_key = self
544                    .resolve_project_key(project_key.as_deref(), Some(session_id))
545                    .await;
546                let mut split_pieces = Vec::with_capacity(pieces.len());
547                for piece in pieces {
548                    let r#type = match piece.r#type.as_deref() {
549                        Some(value) => Some(Self::parse_type(value)?),
550                        None => None,
551                    };
552                    split_pieces.push(bamboo_memory::memory_store::MemorySplitPiece {
553                        title: piece.title,
554                        r#type,
555                        content: piece.content,
556                        tags: piece.tags,
557                    });
558                }
559                let Some(result) = self
560                    .memory_store
561                    .split_memory(
562                        id.trim(),
563                        project_key.as_deref(),
564                        &split_pieces,
565                        Some(session_id),
566                        "main-model",
567                    )
568                    .await
569                    .map_err(|error| {
570                        ToolError::Execution(format!("Failed to split memory: {error}"))
571                    })?
572                else {
573                    return Err(ToolError::Execution(format!(
574                        "memory not found: {}",
575                        id.trim()
576                    )));
577                };
578                Ok(ToolResult {
579                    success: true,
580                    result: json!({
581                        "action": "split",
582                        "data": result,
583                    })
584                    .to_string(),
585                    display_preference: Some("json".to_string()),
586                    images: Vec::new(),
587                })
588            }
589            MemoryArgs::ScanBlobs {
590                scope,
591                project_key,
592                min_sections,
593                options,
594            } => {
595                let scope = Self::parse_scope(Some(&scope))?;
596                if scope == MemoryScope::Session {
597                    return Err(ToolError::InvalidArguments(
598                        "scan_blobs supports durable scopes only".to_string(),
599                    ));
600                }
601                let project_key = self
602                    .resolve_project_key(project_key.as_deref(), Some(session_id))
603                    .await;
604                let min_sections = min_sections.unwrap_or(3);
605                let limit = options
606                    .and_then(|value| value.limit)
607                    .unwrap_or(20)
608                    .clamp(1, 200);
609                let report = self
610                    .memory_store
611                    .scan_blob_candidates(scope, project_key.as_deref(), min_sections, limit)
612                    .await
613                    .map_err(|error| {
614                        ToolError::Execution(format!("Failed to scan blobs: {error}"))
615                    })?;
616                Ok(ToolResult {
617                    success: true,
618                    result: json!({
619                        "action": "scan_blobs",
620                        "report": report,
621                    })
622                    .to_string(),
623                    display_preference: Some("json".to_string()),
624                    images: Vec::new(),
625                })
626            }
627            MemoryArgs::ScanDuplicates {
628                scope,
629                project_key,
630                min_score,
631                options,
632            } => {
633                let scope = Self::parse_scope(Some(&scope))?;
634                if scope == MemoryScope::Session {
635                    return Err(ToolError::InvalidArguments(
636                        "scan_duplicates supports durable scopes only".to_string(),
637                    ));
638                }
639                let project_key = self
640                    .resolve_project_key(project_key.as_deref(), Some(session_id))
641                    .await;
642                let min_score = min_score.unwrap_or(0.6);
643                let limit = options
644                    .and_then(|value| value.limit)
645                    .unwrap_or(20)
646                    .clamp(1, 200);
647                let report = self
648                    .memory_store
649                    .scan_duplicate_clusters(scope, project_key.as_deref(), min_score, 5, limit)
650                    .await
651                    .map_err(|error| {
652                        ToolError::Execution(format!("Failed to scan duplicates: {error}"))
653                    })?;
654                Ok(ToolResult {
655                    success: true,
656                    result: json!({
657                        "action": "scan_duplicates",
658                        "report": report,
659                    })
660                    .to_string(),
661                    display_preference: Some("json".to_string()),
662                    images: Vec::new(),
663                })
664            }
665            MemoryArgs::Consolidate {
666                ids,
667                title,
668                content,
669                r#type,
670                tags,
671                project_key,
672            } => {
673                if ids.len() < 2 {
674                    return Err(ToolError::InvalidArguments(
675                        "consolidate requires at least two source memory ids".to_string(),
676                    ));
677                }
678                let r#type = match r#type.as_deref() {
679                    Some(value) => Some(Self::parse_type(value)?),
680                    None => None,
681                };
682                let project_key = self
683                    .resolve_project_key(project_key.as_deref(), Some(session_id))
684                    .await;
685                let merged = bamboo_memory::memory_store::MemorySplitPiece {
686                    title,
687                    r#type,
688                    content,
689                    tags,
690                };
691                let ids: Vec<String> = ids.iter().map(|id| id.trim().to_string()).collect();
692                let Some(result) = self
693                    .memory_store
694                    .consolidate_memories(
695                        &ids,
696                        project_key.as_deref(),
697                        &merged,
698                        Some(session_id),
699                        "main-model",
700                    )
701                    .await
702                    .map_err(|error| {
703                        ToolError::Execution(format!("Failed to consolidate memories: {error}"))
704                    })?
705                else {
706                    return Err(ToolError::Execution(
707                        "one or more source memories not found".to_string(),
708                    ));
709                };
710                Ok(ToolResult {
711                    success: true,
712                    result: json!({
713                        "action": "consolidate",
714                        "data": result,
715                    })
716                    .to_string(),
717                    display_preference: Some("json".to_string()),
718                    images: Vec::new(),
719                })
720            }
721            MemoryArgs::Purge {
722                id,
723                scope,
724                reason,
725                project_key,
726                filters,
727                mode,
728            } => {
729                let mode = match mode
730                    .as_deref()
731                    .map(str::trim)
732                    .filter(|value| !value.is_empty())
733                {
734                    Some(value) => Self::parse_status(value)?,
735                    None => DurableMemoryStatus::Archived,
736                };
737                let project_key = self
738                    .resolve_project_key(project_key.as_deref(), Some(session_id))
739                    .await;
740
741                if let Some(id) = id
742                    .as_deref()
743                    .map(str::trim)
744                    .filter(|value| !value.is_empty())
745                {
746                    let Some(doc) = self
747                        .memory_store
748                        .archive_memory(id, project_key.as_deref(), mode, reason.as_deref())
749                        .await
750                        .map_err(|error| {
751                            ToolError::Execution(format!("Failed to purge memory: {error}"))
752                        })?
753                    else {
754                        return Err(ToolError::Execution(format!("memory not found: {}", id)));
755                    };
756                    Ok(ToolResult {
757                        success: true,
758                        result: json!({
759                            "action": "purge",
760                            "id": doc.frontmatter.id,
761                            "status": doc.frontmatter.status,
762                        })
763                        .to_string(),
764                        display_preference: Some("json".to_string()),
765                        images: Vec::new(),
766                    })
767                } else {
768                    let scope = Self::parse_scope(scope.as_deref())?;
769                    if scope == MemoryScope::Session {
770                        return Err(ToolError::InvalidArguments(
771                            "purge supports durable scopes only in v1".to_string(),
772                        ));
773                    }
774                    let (filter_types, filter_statuses) =
775                        Self::parse_query_filters(filters.as_ref())?;
776                    let result = self
777                        .memory_store
778                        .purge_memories(
779                            scope,
780                            project_key.as_deref(),
781                            filter_types.as_ref(),
782                            filter_statuses.as_ref(),
783                            mode,
784                            reason.as_deref(),
785                        )
786                        .await
787                        .map_err(|error| {
788                            ToolError::Execution(format!("Failed to purge memory: {error}"))
789                        })?;
790                    Ok(ToolResult {
791                        success: true,
792                        result: json!({
793                            "action": "purge",
794                            "data": result,
795                        })
796                        .to_string(),
797                        display_preference: Some("json".to_string()),
798                        images: Vec::new(),
799                    })
800                }
801            }
802            MemoryArgs::Inspect { scope, project_key } => {
803                let scope = Self::parse_scope(Some(&scope))?;
804                if scope == MemoryScope::Session {
805                    return Err(ToolError::InvalidArguments(
806                        "inspect supports durable scopes only in v1".to_string(),
807                    ));
808                }
809                let project_key = self
810                    .resolve_project_key(project_key.as_deref(), Some(session_id))
811                    .await;
812                let result = self
813                    .memory_store
814                    .inspect_scope(scope, project_key.as_deref())
815                    .await
816                    .map_err(|error| {
817                        ToolError::Execution(format!("Failed to inspect memory: {error}"))
818                    })?;
819                Ok(ToolResult {
820                    success: true,
821                    result: json!({
822                        "action": "inspect",
823                        "data": result,
824                    })
825                    .to_string(),
826                    display_preference: Some("json".to_string()),
827                    images: Vec::new(),
828                })
829            }
830            MemoryArgs::Rebuild { scope, project_key } => {
831                let scope = Self::parse_scope(Some(&scope))?;
832                if scope == MemoryScope::Session {
833                    return Err(ToolError::InvalidArguments(
834                        "rebuild supports durable scopes only in v1".to_string(),
835                    ));
836                }
837                let project_key = self
838                    .resolve_project_key(project_key.as_deref(), Some(session_id))
839                    .await;
840                self.memory_store
841                    .rebuild_scope(scope, project_key.as_deref())
842                    .await
843                    .map_err(|error| {
844                        ToolError::Execution(format!("Failed to rebuild memory artifacts: {error}"))
845                    })?;
846                let inspect = self
847                    .memory_store
848                    .inspect_scope(scope, project_key.as_deref())
849                    .await
850                    .map_err(|error| {
851                        ToolError::Execution(format!("Failed to inspect rebuilt memory: {error}"))
852                    })?;
853                Ok(ToolResult {
854                    success: true,
855                    result: json!({
856                        "action": "rebuild",
857                        "scope": scope,
858                        "project_key": project_key,
859                        "data": inspect,
860                    })
861                    .to_string(),
862                    display_preference: Some("json".to_string()),
863                    images: Vec::new(),
864                })
865            }
866        }
867    }
868}