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}