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, ToolClass, ToolCtx, ToolError, ToolOutcome, 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": {
122                    "type": "object",
123                    "description": "Optional narrowing for `query`/`purge`. Each sub-filter is independent and defaults to unfiltered when omitted or empty.",
124                    "properties": {
125                        "type": {
126                            "type": "array",
127                            "items": {"type": "string", "enum": ["user", "feedback", "project", "reference"]},
128                            "description": "Restrict results to these memory types. Omit for no type filtering."
129                        },
130                        "status": {
131                            "type": "array",
132                            "items": {"type": "string", "enum": ["active", "stale", "superseded", "contradicted", "archived"]},
133                            "description": "Restrict results to these statuses. Omit for no status filtering."
134                        },
135                        "granularity": {
136                            "type": "array",
137                            "items": {
138                                "type": "string",
139                                "enum": ["day", "week", "month", "quarter", "year"]
140                            },
141                            "description": "Restrict results to these temporal granularities: day (today's working context), week (sprint), month, quarter (direction), year (long-term goals). This is a hard filter (unlike the passive recall-into-prompt ordering): a memory whose granularity doesn't appear in this list is excluded entirely, and a memory with no granularity never matches a non-empty filter here. Omit for no granularity filtering."
142                        }
143                    }
144                },
145                "options": {"type": "object"},
146                "reason": {"type": "string"}
147            },
148            "required": ["action"]
149        })
150    }
151
152    fn classify(&self, args: &serde_json::Value) -> ToolClass {
153        let action = args
154            .get("action")
155            .and_then(|value| value.as_str())
156            .unwrap_or("")
157            .trim()
158            .to_ascii_lowercase();
159        match action.as_str() {
160            "session_read"
161            | "session_list_topics"
162            | "query"
163            | "get"
164            | "find_duplicates"
165            | "scan_blobs"
166            | "scan_duplicates"
167            | "inspect" => ToolClass::READONLY_PARALLEL,
168            _ => ToolClass::MUTATING_SERIAL,
169        }
170    }
171
172    async fn invoke(
173        &self,
174        args: serde_json::Value,
175        ctx: ToolCtx,
176    ) -> Result<ToolOutcome, ToolError> {
177        let session_id = ctx.session_id().ok_or_else(|| {
178            ToolError::Execution("memory requires a session_id in tool context".to_string())
179        })?;
180
181        let parsed: MemoryArgs = serde_json::from_value(args).map_err(|error| {
182            ToolError::InvalidArguments(format!("Invalid memory args: {error}"))
183        })?;
184
185        let result = match parsed {
186            MemoryArgs::SessionRead { topic, options } => {
187                let max_chars = options.and_then(|value| value.max_chars);
188                execute_session_memory_action(
189                    &self.memory_store,
190                    session_id,
191                    SessionMemoryAction::Read,
192                    topic.as_deref(),
193                    None,
194                    max_chars,
195                    MEMORY_SESSION_ACTION_NAMES,
196                )
197                .await
198            }
199            MemoryArgs::SessionAppend { topic, content } => {
200                execute_session_memory_action(
201                    &self.memory_store,
202                    session_id,
203                    SessionMemoryAction::Append,
204                    topic.as_deref(),
205                    Some(content.as_str()),
206                    None,
207                    MEMORY_SESSION_ACTION_NAMES,
208                )
209                .await
210            }
211            MemoryArgs::SessionReplace { topic, content } => {
212                execute_session_memory_action(
213                    &self.memory_store,
214                    session_id,
215                    SessionMemoryAction::Replace,
216                    topic.as_deref(),
217                    Some(content.as_str()),
218                    None,
219                    MEMORY_SESSION_ACTION_NAMES,
220                )
221                .await
222            }
223            MemoryArgs::SessionClear { topic } => {
224                execute_session_memory_action(
225                    &self.memory_store,
226                    session_id,
227                    SessionMemoryAction::Clear,
228                    topic.as_deref(),
229                    None,
230                    None,
231                    MEMORY_SESSION_ACTION_NAMES,
232                )
233                .await
234            }
235            MemoryArgs::SessionListTopics => {
236                execute_session_memory_action(
237                    &self.memory_store,
238                    session_id,
239                    SessionMemoryAction::ListTopics,
240                    None,
241                    None,
242                    None,
243                    MEMORY_SESSION_ACTION_NAMES,
244                )
245                .await
246            }
247            MemoryArgs::Query {
248                scope,
249                query,
250                filters,
251                project_key,
252                options,
253            } => {
254                let scope = Self::parse_scope(Some(&scope))?;
255                if scope == MemoryScope::Session {
256                    return Err(ToolError::InvalidArguments(
257                        "query supports durable scopes only; use session_read/session_list_topics for session scope"
258                            .to_string(),
259                    ));
260                }
261                let project_key = self
262                    .resolve_project_key(project_key.as_deref(), Some(session_id))
263                    .await;
264                let options = MemoryQueryOptions {
265                    limit: options
266                        .as_ref()
267                        .and_then(|value| value.limit)
268                        .map(|value| value.min(MAX_QUERY_LIMIT)),
269                    max_chars: options
270                        .as_ref()
271                        .and_then(|value| value.max_chars)
272                        .map(|value| value.min(MAX_MAX_CHARS)),
273                    cursor: options.as_ref().and_then(|value| value.cursor.clone()),
274                    include_related: options
275                        .as_ref()
276                        .and_then(|value| value.include_related)
277                        .unwrap_or(false),
278                };
279                let (filter_types, filter_statuses, filter_granularity) =
280                    Self::parse_query_filters(filters.as_ref())?;
281                let result = self
282                    .memory_store
283                    .query_scope(
284                        scope,
285                        project_key.as_deref(),
286                        query.as_deref(),
287                        filter_types.as_ref(),
288                        filter_statuses.as_ref(),
289                        filter_granularity.as_ref(),
290                        &options,
291                    )
292                    .await
293                    .map_err(|error| {
294                        ToolError::Execution(format!("Failed to query memory: {error}"))
295                    })?;
296                Ok(ToolResult {
297                    success: true,
298                    result: json!({
299                        "action": "query",
300                        "success": true,
301                        "data": result,
302                        "summary": bamboo_memory::memory_store::summary_json(result.returned_count, result.matched_count),
303                        "warnings": [],
304                    }).to_string(),
305                    display_preference: Some("json".to_string()),
306                    images: Vec::new(),
307                })
308            }
309            MemoryArgs::Get {
310                id,
311                project_key,
312                options,
313            } => {
314                let project_key = self
315                    .resolve_project_key(project_key.as_deref(), Some(session_id))
316                    .await;
317                let max_chars = options
318                    .and_then(|value| value.max_chars)
319                    .unwrap_or(MAX_MAX_CHARS)
320                    .min(MAX_MAX_CHARS);
321                let Some(mut doc) = self
322                    .memory_store
323                    .get_memory(id.trim(), project_key.as_deref())
324                    .await
325                    .map_err(|error| {
326                        ToolError::Execution(format!("Failed to get memory: {error}"))
327                    })?
328                else {
329                    return Err(ToolError::Execution(format!(
330                        "memory not found: {}",
331                        id.trim()
332                    )));
333                };
334                let (body, truncated) =
335                    bamboo_memory::memory_store::truncate_chars(&doc.body, max_chars);
336                doc.body = body;
337                Ok(ToolResult {
338                    success: true,
339                    result: json!({
340                        "action": "get",
341                        "id": doc.frontmatter.id,
342                        "memory": {
343                            "frontmatter": doc.frontmatter,
344                            "body": doc.body,
345                            "path": doc.path,
346                            "body_truncated": truncated,
347                        }
348                    })
349                    .to_string(),
350                    display_preference: Some("json".to_string()),
351                    images: Vec::new(),
352                })
353            }
354            MemoryArgs::Write {
355                scope,
356                r#type,
357                title,
358                content,
359                tags,
360                project_key,
361                granularity,
362                options,
363            } => {
364                let scope = Self::parse_scope(Some(&scope))?;
365                if scope == MemoryScope::Session {
366                    return Err(ToolError::InvalidArguments(
367                        "write supports durable scopes only; use session_replace/session_append for session scope"
368                            .to_string(),
369                    ));
370                }
371                let granularity = Self::parse_granularity(granularity.as_deref())?;
372                let project_key = self
373                    .resolve_project_key(project_key.as_deref(), Some(session_id))
374                    .await;
375                let doc = self
376                    .memory_store
377                    .write_memory(
378                        scope,
379                        project_key.as_deref(),
380                        Self::parse_type(&r#type)?,
381                        &title,
382                        &content,
383                        &tags,
384                        Some(session_id),
385                        "main-model",
386                        options
387                            .and_then(|value| value.allow_merge_if_similar)
388                            .unwrap_or(false),
389                        granularity,
390                    )
391                    .await
392                    .map_err(|error| {
393                        ToolError::Execution(format!("Failed to write memory: {error}"))
394                    })?;
395                Ok(ToolResult {
396                    success: true,
397                    result: json!({
398                        "action": "write",
399                        "memory": {
400                            "id": doc.frontmatter.id,
401                            "title": doc.frontmatter.title,
402                            "type": doc.frontmatter.r#type,
403                            "scope": doc.frontmatter.scope,
404                            "status": doc.frontmatter.status,
405                            "project_key": doc.frontmatter.project_key,
406                            "path": doc.path,
407                        }
408                    })
409                    .to_string(),
410                    display_preference: Some("json".to_string()),
411                    images: Vec::new(),
412                })
413            }
414            MemoryArgs::Merge {
415                id,
416                content,
417                tags,
418                project_key,
419                source_memory_ids,
420                mode,
421                reason,
422            } => {
423                let project_key = self
424                    .resolve_project_key(project_key.as_deref(), Some(session_id))
425                    .await;
426                let mode = Self::parse_merge_mode(mode.as_deref())?;
427                if matches!(mode.as_deref(), Some("contradict")) {
428                    let Some(result) = self
429                        .memory_store
430                        .mark_memory_contradicted(
431                            id.trim(),
432                            project_key.as_deref(),
433                            &source_memory_ids,
434                            reason.as_deref().or(Some(content.trim())),
435                            Some(session_id),
436                            "main-model",
437                        )
438                        .await
439                        .map_err(|error| {
440                            ToolError::Execution(format!("Failed to contradict memory: {error}"))
441                        })?
442                    else {
443                        return Err(ToolError::Execution(format!(
444                            "memory not found: {}",
445                            id.trim()
446                        )));
447                    };
448                    Ok(ToolResult {
449                        success: true,
450                        result: json!({
451                            "action": "merge",
452                            "mode": "contradict",
453                            "data": result,
454                        })
455                        .to_string(),
456                        display_preference: Some("json".to_string()),
457                        images: Vec::new(),
458                    })
459                } else {
460                    let Some(result) = self
461                        .memory_store
462                        .merge_memory(
463                            id.trim(),
464                            project_key.as_deref(),
465                            &content,
466                            &tags,
467                            Some(session_id),
468                            "main-model",
469                            &source_memory_ids,
470                        )
471                        .await
472                        .map_err(|error| {
473                            ToolError::Execution(format!("Failed to merge memory: {error}"))
474                        })?
475                    else {
476                        return Err(ToolError::Execution(format!(
477                            "memory not found: {}",
478                            id.trim()
479                        )));
480                    };
481                    Ok(ToolResult {
482                        success: true,
483                        result: json!({
484                            "action": "merge",
485                            "mode": mode.unwrap_or_else(|| "merge".to_string()),
486                            "data": result,
487                        })
488                        .to_string(),
489                        display_preference: Some("json".to_string()),
490                        images: Vec::new(),
491                    })
492                }
493            }
494            MemoryArgs::FindDuplicates {
495                scope,
496                title,
497                content,
498                r#type,
499                tags,
500                project_key,
501                options,
502            } => {
503                let scope = Self::parse_scope(Some(&scope))?;
504                if scope == MemoryScope::Session {
505                    return Err(ToolError::InvalidArguments(
506                        "find_duplicates supports durable scopes only".to_string(),
507                    ));
508                }
509                let r#type = match r#type.as_deref() {
510                    Some(value) => Some(Self::parse_type(value)?),
511                    None => None,
512                };
513                let project_key = self
514                    .resolve_project_key(project_key.as_deref(), Some(session_id))
515                    .await;
516                let limit = options
517                    .and_then(|value| value.limit)
518                    .unwrap_or(5)
519                    .clamp(1, MAX_QUERY_LIMIT);
520                let candidates = self
521                    .memory_store
522                    .find_duplicate_candidates(
523                        scope,
524                        project_key.as_deref(),
525                        r#type,
526                        &title,
527                        content.as_deref().unwrap_or(""),
528                        &tags,
529                        limit,
530                    )
531                    .await
532                    .map_err(|error| {
533                        ToolError::Execution(format!("Failed to find duplicates: {error}"))
534                    })?;
535                Ok(ToolResult {
536                    success: true,
537                    result: json!({
538                        "action": "find_duplicates",
539                        "candidates": candidates,
540                    })
541                    .to_string(),
542                    display_preference: Some("json".to_string()),
543                    images: Vec::new(),
544                })
545            }
546            MemoryArgs::Split {
547                id,
548                project_key,
549                pieces,
550            } => {
551                if pieces.is_empty() {
552                    return Err(ToolError::InvalidArguments(
553                        "split requires at least one piece".to_string(),
554                    ));
555                }
556                let project_key = self
557                    .resolve_project_key(project_key.as_deref(), Some(session_id))
558                    .await;
559                let mut split_pieces = Vec::with_capacity(pieces.len());
560                for piece in pieces {
561                    let r#type = match piece.r#type.as_deref() {
562                        Some(value) => Some(Self::parse_type(value)?),
563                        None => None,
564                    };
565                    split_pieces.push(bamboo_memory::memory_store::MemorySplitPiece {
566                        title: piece.title,
567                        r#type,
568                        content: piece.content,
569                        tags: piece.tags,
570                    });
571                }
572                let Some(result) = self
573                    .memory_store
574                    .split_memory(
575                        id.trim(),
576                        project_key.as_deref(),
577                        &split_pieces,
578                        Some(session_id),
579                        "main-model",
580                    )
581                    .await
582                    .map_err(|error| {
583                        ToolError::Execution(format!("Failed to split memory: {error}"))
584                    })?
585                else {
586                    return Err(ToolError::Execution(format!(
587                        "memory not found: {}",
588                        id.trim()
589                    )));
590                };
591                Ok(ToolResult {
592                    success: true,
593                    result: json!({
594                        "action": "split",
595                        "data": result,
596                    })
597                    .to_string(),
598                    display_preference: Some("json".to_string()),
599                    images: Vec::new(),
600                })
601            }
602            MemoryArgs::ScanBlobs {
603                scope,
604                project_key,
605                min_sections,
606                options,
607            } => {
608                let scope = Self::parse_scope(Some(&scope))?;
609                if scope == MemoryScope::Session {
610                    return Err(ToolError::InvalidArguments(
611                        "scan_blobs supports durable scopes only".to_string(),
612                    ));
613                }
614                let project_key = self
615                    .resolve_project_key(project_key.as_deref(), Some(session_id))
616                    .await;
617                let min_sections = min_sections.unwrap_or(3);
618                let limit = options
619                    .and_then(|value| value.limit)
620                    .unwrap_or(20)
621                    .clamp(1, 200);
622                let report = self
623                    .memory_store
624                    .scan_blob_candidates(scope, project_key.as_deref(), min_sections, limit)
625                    .await
626                    .map_err(|error| {
627                        ToolError::Execution(format!("Failed to scan blobs: {error}"))
628                    })?;
629                Ok(ToolResult {
630                    success: true,
631                    result: json!({
632                        "action": "scan_blobs",
633                        "report": report,
634                    })
635                    .to_string(),
636                    display_preference: Some("json".to_string()),
637                    images: Vec::new(),
638                })
639            }
640            MemoryArgs::ScanDuplicates {
641                scope,
642                project_key,
643                min_score,
644                options,
645            } => {
646                let scope = Self::parse_scope(Some(&scope))?;
647                if scope == MemoryScope::Session {
648                    return Err(ToolError::InvalidArguments(
649                        "scan_duplicates supports durable scopes only".to_string(),
650                    ));
651                }
652                let project_key = self
653                    .resolve_project_key(project_key.as_deref(), Some(session_id))
654                    .await;
655                let min_score = min_score.unwrap_or(0.6);
656                let limit = options
657                    .and_then(|value| value.limit)
658                    .unwrap_or(20)
659                    .clamp(1, 200);
660                let report = self
661                    .memory_store
662                    .scan_duplicate_clusters(scope, project_key.as_deref(), min_score, 5, limit)
663                    .await
664                    .map_err(|error| {
665                        ToolError::Execution(format!("Failed to scan duplicates: {error}"))
666                    })?;
667                Ok(ToolResult {
668                    success: true,
669                    result: json!({
670                        "action": "scan_duplicates",
671                        "report": report,
672                    })
673                    .to_string(),
674                    display_preference: Some("json".to_string()),
675                    images: Vec::new(),
676                })
677            }
678            MemoryArgs::Consolidate {
679                ids,
680                title,
681                content,
682                r#type,
683                tags,
684                project_key,
685            } => {
686                if ids.len() < 2 {
687                    return Err(ToolError::InvalidArguments(
688                        "consolidate requires at least two source memory ids".to_string(),
689                    ));
690                }
691                let r#type = match r#type.as_deref() {
692                    Some(value) => Some(Self::parse_type(value)?),
693                    None => None,
694                };
695                let project_key = self
696                    .resolve_project_key(project_key.as_deref(), Some(session_id))
697                    .await;
698                let merged = bamboo_memory::memory_store::MemorySplitPiece {
699                    title,
700                    r#type,
701                    content,
702                    tags,
703                };
704                let ids: Vec<String> = ids.iter().map(|id| id.trim().to_string()).collect();
705                let Some(result) = self
706                    .memory_store
707                    .consolidate_memories(
708                        &ids,
709                        project_key.as_deref(),
710                        &merged,
711                        Some(session_id),
712                        "main-model",
713                    )
714                    .await
715                    .map_err(|error| {
716                        ToolError::Execution(format!("Failed to consolidate memories: {error}"))
717                    })?
718                else {
719                    return Err(ToolError::Execution(
720                        "one or more source memories not found".to_string(),
721                    ));
722                };
723                Ok(ToolResult {
724                    success: true,
725                    result: json!({
726                        "action": "consolidate",
727                        "data": result,
728                    })
729                    .to_string(),
730                    display_preference: Some("json".to_string()),
731                    images: Vec::new(),
732                })
733            }
734            MemoryArgs::Purge {
735                id,
736                scope,
737                reason,
738                project_key,
739                filters,
740                mode,
741            } => {
742                let mode = match mode
743                    .as_deref()
744                    .map(str::trim)
745                    .filter(|value| !value.is_empty())
746                {
747                    Some(value) => Self::parse_status(value)?,
748                    None => DurableMemoryStatus::Archived,
749                };
750                let project_key = self
751                    .resolve_project_key(project_key.as_deref(), Some(session_id))
752                    .await;
753
754                if let Some(id) = id
755                    .as_deref()
756                    .map(str::trim)
757                    .filter(|value| !value.is_empty())
758                {
759                    let Some(doc) = self
760                        .memory_store
761                        .archive_memory(id, project_key.as_deref(), mode, reason.as_deref())
762                        .await
763                        .map_err(|error| {
764                            ToolError::Execution(format!("Failed to purge memory: {error}"))
765                        })?
766                    else {
767                        return Err(ToolError::Execution(format!("memory not found: {}", id)));
768                    };
769                    Ok(ToolResult {
770                        success: true,
771                        result: json!({
772                            "action": "purge",
773                            "id": doc.frontmatter.id,
774                            "status": doc.frontmatter.status,
775                        })
776                        .to_string(),
777                        display_preference: Some("json".to_string()),
778                        images: Vec::new(),
779                    })
780                } else {
781                    let scope = Self::parse_scope(scope.as_deref())?;
782                    if scope == MemoryScope::Session {
783                        return Err(ToolError::InvalidArguments(
784                            "purge supports durable scopes only in v1".to_string(),
785                        ));
786                    }
787                    let (filter_types, filter_statuses, filter_granularity) =
788                        Self::parse_query_filters(filters.as_ref())?;
789                    let result = self
790                        .memory_store
791                        .purge_memories(
792                            scope,
793                            project_key.as_deref(),
794                            filter_types.as_ref(),
795                            filter_statuses.as_ref(),
796                            filter_granularity.as_ref(),
797                            mode,
798                            reason.as_deref(),
799                        )
800                        .await
801                        .map_err(|error| {
802                            ToolError::Execution(format!("Failed to purge memory: {error}"))
803                        })?;
804                    Ok(ToolResult {
805                        success: true,
806                        result: json!({
807                            "action": "purge",
808                            "data": result,
809                        })
810                        .to_string(),
811                        display_preference: Some("json".to_string()),
812                        images: Vec::new(),
813                    })
814                }
815            }
816            MemoryArgs::Inspect { scope, project_key } => {
817                let scope = Self::parse_scope(Some(&scope))?;
818                if scope == MemoryScope::Session {
819                    return Err(ToolError::InvalidArguments(
820                        "inspect supports durable scopes only in v1".to_string(),
821                    ));
822                }
823                let project_key = self
824                    .resolve_project_key(project_key.as_deref(), Some(session_id))
825                    .await;
826                let result = self
827                    .memory_store
828                    .inspect_scope(scope, project_key.as_deref())
829                    .await
830                    .map_err(|error| {
831                        ToolError::Execution(format!("Failed to inspect memory: {error}"))
832                    })?;
833                Ok(ToolResult {
834                    success: true,
835                    result: json!({
836                        "action": "inspect",
837                        "data": result,
838                    })
839                    .to_string(),
840                    display_preference: Some("json".to_string()),
841                    images: Vec::new(),
842                })
843            }
844            MemoryArgs::Rebuild { scope, project_key } => {
845                let scope = Self::parse_scope(Some(&scope))?;
846                if scope == MemoryScope::Session {
847                    return Err(ToolError::InvalidArguments(
848                        "rebuild supports durable scopes only in v1".to_string(),
849                    ));
850                }
851                let project_key = self
852                    .resolve_project_key(project_key.as_deref(), Some(session_id))
853                    .await;
854                self.memory_store
855                    .rebuild_scope(scope, project_key.as_deref())
856                    .await
857                    .map_err(|error| {
858                        ToolError::Execution(format!("Failed to rebuild memory artifacts: {error}"))
859                    })?;
860                let inspect = self
861                    .memory_store
862                    .inspect_scope(scope, project_key.as_deref())
863                    .await
864                    .map_err(|error| {
865                        ToolError::Execution(format!("Failed to inspect rebuilt memory: {error}"))
866                    })?;
867                Ok(ToolResult {
868                    success: true,
869                    result: json!({
870                        "action": "rebuild",
871                        "scope": scope,
872                        "project_key": project_key,
873                        "data": inspect,
874                    })
875                    .to_string(),
876                    display_preference: Some("json".to_string()),
877                    images: Vec::new(),
878                })
879            }
880        }?;
881        Ok(ToolOutcome::Completed(result))
882    }
883}