Skip to main content

semantic_memory_mcp/
server.rs

1//! MCP server handler using rmcp's #[tool_router] macro.
2//!
3//! Each #[tool] method becomes an MCP tool that Hermes/Claude Desktop
4//! can discover and call. The rmcp macro auto-generates JSON Schema
5//! from the parameter structs in tools.rs.
6
7use crate::bridge::MemoryBridge;
8use crate::tools::*;
9use rmcp::{
10    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
11    tool, tool_handler, tool_router, ErrorData, ServerHandler,
12};
13use std::collections::HashSet;
14use std::sync::Arc;
15use tokio::runtime::Handle;
16
17// Re-export the specific parameter types we use in tool signatures.
18use crate::tools::{
19    AddGraphEdgeParams, CommunityParams, FactorGraphParams, InvalidateGraphEdgeParams,
20    ListGraphEdgesParams, RecordOutcomeParams, TopologyParams,
21};
22
23pub struct SemanticMemoryServer {
24    bridge: Arc<MemoryBridge>,
25    tool_router: ToolRouter<Self>,
26}
27
28impl SemanticMemoryServer {
29    pub fn new(bridge: MemoryBridge, tool_profile: &str) -> Self {
30        let mut router = Self::tool_router();
31
32        // Tools hidden in "lean" mode (maintenance + audit + bitemporal query + import)
33        let admin_tools = [
34            // Maintenance
35            "sm_reconcile",
36            "sm_vacuum",
37            "sm_reembed_all",
38            "sm_embeddings_are_dirty",
39            // Audit
40            "sm_get_search_receipt",
41            "sm_replay_search_receipt",
42            // Bitemporal query
43            "sm_query_claim_versions",
44            "sm_query_relation_versions",
45            "sm_query_episodes",
46            "sm_query_entity_aliases",
47            "sm_query_evidence_refs",
48            // Import
49            "sm_import_envelope",
50            "sm_import_status",
51            "sm_list_imports",
52        ];
53
54        match tool_profile {
55            "full" => { /* all tools visible */ }
56            "standard" => {
57                // Hide import tools only
58                for t in &["sm_import_envelope", "sm_import_status", "sm_list_imports"] {
59                    router.disable_route(*t);
60                }
61            }
62            _ => {
63                // "lean" (default) — hide all admin tools
64                for t in &admin_tools {
65                    router.disable_route(*t);
66                }
67            }
68        }
69
70        eprintln!(
71            "Tool profile: {} ({} tools visible)",
72            tool_profile,
73            router.list_all().len()
74        );
75
76        Self {
77            bridge: Arc::new(bridge),
78            tool_router: router,
79        }
80    }
81}
82
83/// Helper: load all stored graph edges from the store as GraphEdgeRef tuples
84/// for discord scoring.
85fn load_stored_edge_refs(
86    store: &semantic_memory::MemoryStore,
87) -> Result<Vec<semantic_memory::discord::GraphEdgeRef>, ErrorData> {
88    let edges =
89        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
90            .map_err(|e| {
91                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
92            })?;
93    let refs = edges
94        .iter()
95        .map(|edge| {
96            let parsed_type = edge
97                .edge_type_parsed
98                .clone()
99                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
100                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
101                    relation: "unknown".to_string(),
102                });
103            let type_str = match parsed_type {
104                semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
105                semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
106                semantic_memory::GraphEdgeType::Causal { .. } => "causal",
107                semantic_memory::GraphEdgeType::Entity { .. } => "entity",
108            };
109            semantic_memory::discord::GraphEdgeRef {
110                source: edge.source.clone(),
111                target: edge.target.clone(),
112                edge_type: type_str.to_string(),
113                weight: edge.weight,
114            }
115        })
116        .collect();
117    Ok(refs)
118}
119
120/// Helper: load all stored graph edges from the store as raw factor graph
121/// edge tuples (source, target, GraphEdgeType, weight, metadata_json).
122fn load_stored_factor_edges(
123    store: &semantic_memory::MemoryStore,
124) -> Result<
125    Vec<(
126        String,
127        String,
128        semantic_memory::GraphEdgeType,
129        f64,
130        Option<String>,
131    )>,
132    ErrorData,
133> {
134    let edges =
135        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
136            .map_err(|e| {
137                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
138            })?;
139    let raw = edges
140        .iter()
141        .map(|edge| {
142            let parsed_type = edge
143                .edge_type_parsed
144                .clone()
145                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
146                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
147                    relation: "unknown".to_string(),
148                });
149            (
150                edge.source.clone(),
151                edge.target.clone(),
152                parsed_type,
153                edge.weight,
154                edge.metadata.clone(),
155            )
156        })
157        .collect();
158    Ok(raw)
159}
160
161/// Helper: load all stored graph edges as (source, target) pairs.
162fn load_stored_edge_pairs(
163    store: &semantic_memory::MemoryStore,
164) -> Result<Vec<(String, String)>, ErrorData> {
165    let edges =
166        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
167            .map_err(|e| {
168                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
169            })?;
170    let pairs = edges
171        .iter()
172        .map(|edge| (edge.source.clone(), edge.target.clone()))
173        .collect();
174    Ok(pairs)
175}
176
177/// Helper: load graph edges for a neighborhood around the given seed node IDs.
178/// Uses BFS expansion with max_hops=2 and max_nodes=200 by default.
179/// Falls back to full graph load if seeds are empty.
180fn load_neighborhood_edge_pairs(
181    store: &semantic_memory::MemoryStore,
182    seed_ids: &[String],
183) -> Result<Vec<(String, String)>, ErrorData> {
184    if seed_ids.is_empty() {
185        return load_stored_edge_pairs(store);
186    }
187    let edges = tokio::task::block_in_place(|| {
188        Handle::current().block_on(store.list_graph_edges_for_neighborhood(
189            seed_ids.to_vec(),
190            2,
191            200,
192        ))
193    })
194    .map_err(|e| {
195        ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
196    })?;
197    let pairs = edges
198        .iter()
199        .map(|edge| (edge.source.clone(), edge.target.clone()))
200        .collect();
201    Ok(pairs)
202}
203
204/// Helper: load graph edges for a neighborhood as GraphEdgeRef vec.
205fn load_neighborhood_edge_refs(
206    store: &semantic_memory::MemoryStore,
207    seed_ids: &[String],
208) -> Result<Vec<semantic_memory::discord::GraphEdgeRef>, ErrorData> {
209    if seed_ids.is_empty() {
210        return load_stored_edge_refs(store);
211    }
212    let edges = tokio::task::block_in_place(|| {
213        Handle::current().block_on(store.list_graph_edges_for_neighborhood(
214            seed_ids.to_vec(),
215            2,
216            200,
217        ))
218    })
219    .map_err(|e| {
220        ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
221    })?;
222    let refs = edges
223        .iter()
224        .map(|edge| {
225            let parsed_type = edge
226                .edge_type_parsed
227                .clone()
228                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
229                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
230                    relation: "unknown".to_string(),
231                });
232            let type_str = match parsed_type {
233                semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
234                semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
235                semantic_memory::GraphEdgeType::Causal { .. } => "causal",
236                semantic_memory::GraphEdgeType::Entity { .. } => "entity",
237            };
238            semantic_memory::discord::GraphEdgeRef {
239                source: edge.source.clone(),
240                target: edge.target.clone(),
241                edge_type: type_str.to_string(),
242                weight: edge.weight,
243            }
244        })
245        .collect();
246    Ok(refs)
247}
248
249/// Helper: load graph edges for a neighborhood as factor graph tuples.
250fn load_neighborhood_factor_edges(
251    store: &semantic_memory::MemoryStore,
252    seed_ids: &[String],
253) -> Result<
254    Vec<(
255        String,
256        String,
257        semantic_memory::GraphEdgeType,
258        f64,
259        Option<String>,
260    )>,
261    ErrorData,
262> {
263    if seed_ids.is_empty() {
264        return load_stored_factor_edges(store);
265    }
266    let edges = tokio::task::block_in_place(|| {
267        Handle::current().block_on(store.list_graph_edges_for_neighborhood(
268            seed_ids.to_vec(),
269            2,
270            200,
271        ))
272    })
273    .map_err(|e| {
274        ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
275    })?;
276    let raw = edges
277        .iter()
278        .map(|edge| {
279            let parsed_type = edge
280                .edge_type_parsed
281                .clone()
282                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
283                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
284                    relation: "unknown".to_string(),
285                });
286            (
287                edge.source.clone(),
288                edge.target.clone(),
289                parsed_type,
290                edge.weight,
291                edge.metadata.clone(),
292            )
293        })
294        .collect();
295    Ok(raw)
296}
297
298/// Load fact ids targeted by entity relation="supersedes" graph edges.
299fn load_superseded_targets(
300    store: &semantic_memory::MemoryStore,
301) -> Result<HashSet<String>, ErrorData> {
302    let edges =
303        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
304            .map_err(|e| {
305                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
306            })?;
307    let mut targets = HashSet::new();
308    for edge in edges {
309        let parsed_type = edge
310            .edge_type_parsed
311            .clone()
312            .or_else(|| serde_json::from_str(&edge.edge_type).ok());
313        if let Some(semantic_memory::GraphEdgeType::Entity { relation }) = parsed_type {
314            if relation == "supersedes" {
315                targets.insert(edge.target);
316            }
317        }
318    }
319    Ok(targets)
320}
321
322fn query_allows_superseded(query: &str) -> bool {
323    let q = query.to_lowercase();
324    q.contains("supersed")
325        || q.contains("stale")
326        || q.contains("obsolete")
327        || q.contains("histor")
328        || q.contains("old fact")
329        || q.contains("previous fact")
330}
331
332/// Serialize a JSON value to a pretty string, mapping serialization errors
333/// to protocol-level errors instead of success strings.
334/// Build a `ProjectionQuery` from the MCP-facing `ProjectionQueryParams`.
335///
336/// Maps the flat parameter struct into the library's `ProjectionQuery` with
337/// a fully-resolved `ScopeKey` and typed ID filters.
338fn build_projection_query(params: ProjectionQueryParams) -> semantic_memory::ProjectionQuery {
339    use stack_ids::{ClaimId, ClaimVersionId, EntityId, ScopeKey};
340
341    let scope = ScopeKey {
342        namespace: params.namespace,
343        domain: params.domain,
344        workspace_id: params.workspace_id,
345        repo_id: params.repo_id,
346    };
347
348    let limit = params.limit.unwrap_or(10) as usize;
349
350    semantic_memory::ProjectionQuery {
351        scope,
352        text_query: params.text_query,
353        valid_at: params.valid_at,
354        recorded_at_or_before: params.recorded_at_or_before,
355        subject_entity_id: params.subject_entity_id.map(EntityId::new),
356        canonical_entity_id: params.canonical_entity_id.map(EntityId::new),
357        claim_state: params.claim_state,
358        claim_id: params.claim_id.map(ClaimId::new),
359        claim_version_id: params.claim_version_id.map(ClaimVersionId::new),
360        limit,
361    }
362}
363
364fn json_to_string(value: &serde_json::Value) -> Result<String, ErrorData> {
365    serde_json::to_string_pretty(value)
366        .map_err(|e| ErrorData::internal_error(format!("Serialization error: {e}"), None))
367}
368
369#[tool_router]
370impl SemanticMemoryServer {
371    // ── Core search tools ────────────────────────────────────────────
372
373    #[tool(
374        description = "Semantic hybrid search (BM25 + vector + RRF). Returns ranked results with content, scores, and stable result IDs.",
375        annotations(read_only_hint = true)
376    )]
377    fn sm_search(
378        &self,
379        Parameters(SearchParams {
380            query,
381            top_k,
382            namespaces,
383        }): Parameters<SearchParams>,
384    ) -> Result<String, ErrorData> {
385        let requested_k = top_k.map(|v| v as usize).unwrap_or(5);
386        let allow_superseded = query_allows_superseded(&query);
387        let search_k = if allow_superseded {
388            requested_k
389        } else {
390            (requested_k * 4).max(20)
391        };
392        let ns: Option<Vec<&str>> = namespaces
393            .as_ref()
394            .map(|v| v.iter().map(|s| s.as_str()).collect());
395
396        let store = &self.bridge.store;
397        let result = tokio::task::block_in_place(|| {
398            Handle::current().block_on(store.search(&query, Some(search_k), ns.as_deref(), None))
399        });
400
401        match result {
402            Ok(results) => {
403                let superseded_targets = if allow_superseded {
404                    HashSet::new()
405                } else {
406                    load_superseded_targets(store)?
407                };
408                let fresh_results: Vec<_> = results
409                    .iter()
410                    .filter(|r| !superseded_targets.contains(&r.source.result_id()))
411                    .collect();
412                let result_refs: Vec<_> =
413                    if superseded_targets.is_empty() || fresh_results.is_empty() {
414                        results.iter().collect()
415                    } else {
416                        fresh_results
417                    };
418                let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
419                let json_results: Vec<serde_json::Value> = result_refs
420                    .iter()
421                    .take(requested_k)
422                    .map(|r| {
423                        serde_json::json!({
424                            "result_id": r.source.result_id(),
425                            "content": r.content,
426                            "source": format!("{:?}", r.source),
427                            "score": r.score,
428                            "bm25_rank": r.bm25_rank,
429                            "vector_rank": r.vector_rank,
430                            "cosine_similarity": r.cosine_similarity,
431                        })
432                    })
433                    .collect();
434                json_to_string(&serde_json::json!({
435                    "ok": true,
436                    "results": json_results,
437                    "count": json_results.len(),
438                    "superseded_filtered_count": superseded_filtered_count,
439                }))
440            }
441            Err(e) => Err(ErrorData::internal_error(
442                format!("Search error: {e}"),
443                None,
444            )),
445        }
446    }
447
448    // DEPRECATED #[tool(
449        // description = "Search with full score breakdown showing how BM25 and vector scores combine. Useful for debugging retrieval quality.",
450        // annotations(read_only_hint = true)
451    // )]
452    #[allow(dead_code)]
453    fn sm_search_explained(
454        &self,
455        Parameters(SearchExplainedParams { query, top_k }): Parameters<SearchExplainedParams>,
456    ) -> Result<String, ErrorData> {
457        let requested_k = top_k.map(|v| v as usize).unwrap_or(5);
458        let allow_superseded = query_allows_superseded(&query);
459        let search_k = if allow_superseded {
460            requested_k
461        } else {
462            (requested_k * 4).max(20)
463        };
464        let store = &self.bridge.store;
465        let result = tokio::task::block_in_place(|| {
466            Handle::current().block_on(store.search_explained(&query, Some(search_k), None, None))
467        });
468
469        match result {
470            Ok(results) => {
471                let superseded_targets = if allow_superseded {
472                    HashSet::new()
473                } else {
474                    load_superseded_targets(store)?
475                };
476                let fresh_results: Vec<_> = results
477                    .iter()
478                    .filter(|r| !superseded_targets.contains(&r.result.source.result_id()))
479                    .collect();
480                let result_refs: Vec<_> =
481                    if superseded_targets.is_empty() || fresh_results.is_empty() {
482                        results.iter().collect()
483                    } else {
484                        fresh_results
485                    };
486                let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
487                let json_results: Vec<serde_json::Value> = result_refs
488                    .iter()
489                    .take(requested_k)
490                    .map(|r| {
491                        serde_json::json!({
492                            "result_id": r.result.source.result_id(),
493                            "content": r.result.content,
494                            "source": format!("{:?}", r.result.source),
495                            "score": r.result.score,
496                            "bm25_rank": r.result.bm25_rank,
497                            "vector_rank": r.result.vector_rank,
498                            "cosine_similarity": r.result.cosine_similarity,
499                            "breakdown": {
500                                "rrf_score": r.breakdown.rrf_score,
501                                "bm25_score": r.breakdown.bm25_score,
502                                "vector_score": r.breakdown.vector_score,
503                                "recency_score": r.breakdown.recency_score,
504                                "bm25_rank": r.breakdown.bm25_rank,
505                                "vector_rank": r.breakdown.vector_rank,
506                                "vector_source_rank": r.breakdown.vector_source_rank,
507                                "vector_source_score": r.breakdown.vector_source_score,
508                                "bm25_contribution": r.breakdown.bm25_contribution,
509                                "vector_contribution": r.breakdown.vector_contribution,
510                                "vector_reranked_from_f32": r.breakdown.vector_reranked_from_f32,
511                                "bm25_weight": r.breakdown.bm25_weight,
512                                "vector_weight": r.breakdown.vector_weight,
513                                "recency_weight": r.breakdown.recency_weight,
514                                "rrf_k": r.breakdown.rrf_k,
515                            },
516                        })
517                    })
518                    .collect();
519                json_to_string(&serde_json::json!({
520                    "ok": true,
521                    "results": json_results,
522                    "count": json_results.len(),
523                    "superseded_filtered_count": superseded_filtered_count,
524                }))
525            }
526            Err(e) => Err(ErrorData::internal_error(
527                format!("Search error: {e}"),
528                None,
529            )),
530        }
531    }
532
533    #[tool(
534        description = "Add a fact to the knowledge base. Embedded and indexed for semantic search. Returns fact ID and content digest.",
535        annotations(idempotent_hint = true)
536    )]
537    fn sm_add_fact(
538        &self,
539        Parameters(AddFactParams {
540            content,
541            namespace,
542            source,
543            extract_entities,
544            memory_kind,
545            sensitivity,
546            evidence_refs,
547        }): Parameters<AddFactParams>,
548    ) -> Result<String, ErrorData> {
549        let store = &self.bridge.store;
550        let src = source.as_deref();
551
552        // Admission gate: classify sensitivity
553        let sens = sensitivity.unwrap_or_else(|| "internal".to_string());
554        let kind = memory_kind.unwrap_or_else(|| "durable_fact".to_string());
555
556        // Block confidential/restricted content from autocapture
557        if sens == "confidential" || sens == "restricted" {
558            return Err(ErrorData::invalid_params(
559                format!("Admission gate BLOCKED: sensitivity='{sens}' content cannot be stored without explicit user request"),
560                None,
561            ));
562        }
563
564        // Block ephemeral_inference from becoming durable without evidence
565        if kind == "ephemeral_inference" {
566            let refs = evidence_refs.as_ref().map(|v| v.len()).unwrap_or(0);
567            if refs == 0 {
568                return Err(ErrorData::invalid_params(
569                    "Admission gate BLOCKED: ephemeral_inference requires evidence_refs to promote to durable".to_string(),
570                    None,
571                ));
572            }
573        }
574
575        // Build metadata JSON with typed memory fields
576        let mut meta = serde_json::Map::new();
577        meta.insert("memory_kind".to_string(), serde_json::json!(kind));
578        meta.insert("sensitivity".to_string(), serde_json::json!(sens));
579        if let Some(refs) = evidence_refs {
580            meta.insert("evidence_refs".to_string(), serde_json::json!(refs));
581        }
582        let _metadata_str = serde_json::to_string(&serde_json::Value::Object(meta)).ok();
583
584        let result = tokio::task::block_in_place(|| {
585            Handle::current().block_on(store.add_fact(&namespace, &content, src, None))
586        });
587
588        match result {
589            Ok(id) => {
590                // Optional entity extraction — best-effort, never fails the whole operation.
591                if extract_entities == Some(true) {
592                    let prompt = format!(
593                        "Extract entities from this text as JSON. Format: {{\"entities\": [{{\"name\": \"...\", \"type\": \"person|project|concept|tool|version|path\"}}]}}\nText: {content}\nJSON:"
594                    );
595                    let body = serde_json::json!({
596                        "model": "granite4.1:3b",
597                        "prompt": prompt,
598                        "stream": false,
599                        "options": {"temperature": 0, "num_predict": 200}
600                    });
601                    if let Ok(resp) = reqwest::blocking::Client::new()
602                        .post("http://127.0.0.1:11434/api/generate")
603                        .json(&body)
604                        .send()
605                    {
606                        if let Ok(v) = resp.json::<serde_json::Value>() {
607                            if let Some(response_str) =
608                                v.get("response").and_then(|r| r.as_str())
609                            {
610                                // Use boundary compiler for robust JSON parsing with duplicate-key rejection
611                                let parsed_result = boundary_compiler::parse_with_dup_check(response_str.trim());
612                                if let Ok(parsed) = parsed_result {
613                                    if let Some(entities) =
614                                        parsed.get("entities").and_then(|e| e.as_array())
615                                    {
616                                        let fact_node = format!("fact:{id}");
617                                        for entity in entities {
618                                            if let Some(name) =
619                                                entity.get("name").and_then(|n| n.as_str())
620                                            {
621                                                let entity_node = format!("entity:{name}");
622                                                let _ = tokio::task::block_in_place(|| {
623                                                    Handle::current().block_on(
624                                                        store.add_graph_edge(
625                                                            &fact_node,
626                                                            &entity_node,
627                                                            semantic_memory::GraphEdgeType::Entity {
628                                                                relation: "mentions".to_string(),
629                                                            },
630                                                            1.0,
631                                                            None,
632                                                        ),
633                                                    )
634                                                });
635                                            }
636                                        }
637                                    }
638                                }
639                            }
640                        }
641                    }
642                }
643
644                json_to_string(&serde_json::json!({
645                    "ok": true,
646                    "fact_id": id,
647                    "namespace": namespace,
648                    "message": "Fact added successfully",
649                }))
650            }
651            Err(e) => Err(ErrorData::internal_error(
652                format!("Error adding fact: {e}"),
653                None,
654            )),
655        }
656    }
657
658    #[tool(
659        description = "Ingest a document with automatic chunking. Splits into chunks, each embedded and indexed. Returns document ID and chunk count.",
660        annotations(idempotent_hint = true)
661    )]
662    fn sm_ingest_document(
663        &self,
664        Parameters(IngestDocumentParams {
665            content,
666            title,
667            namespace,
668        }): Parameters<IngestDocumentParams>,
669    ) -> Result<String, ErrorData> {
670        let store = &self.bridge.store;
671        let result = tokio::task::block_in_place(|| {
672            Handle::current()
673                .block_on(store.ingest_document(&title, &content, &namespace, None, None))
674        });
675
676        match result {
677            Ok(doc_id) => {
678                let chunk_count = tokio::task::block_in_place(|| {
679                    Handle::current().block_on(store.count_chunks_for_document(&doc_id))
680                })
681                .unwrap_or(0);
682                json_to_string(&serde_json::json!({
683                    "ok": true,
684                    "document_id": doc_id,
685                    "title": title,
686                    "chunk_count": chunk_count,
687                    "message": "Document ingested successfully",
688                }))
689            }
690            Err(e) => Err(ErrorData::internal_error(
691                format!("Error ingesting document: {e}"),
692                None,
693            )),
694        }
695    }
696
697    #[tool(
698        description = "Get knowledge base statistics: fact/chunk/document/session counts, DB size, embedding model, and graph edge count.",
699        annotations(read_only_hint = true)
700    )]
701    fn sm_stats(&self) -> Result<String, ErrorData> {
702        let store = &self.bridge.store;
703        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.stats()));
704
705        match result {
706            Ok(stats) => {
707                // Load graph edge count separately — propagates errors
708                // instead of hiding them (SM-AUD-016).
709                let graph_edge_count = tokio::task::block_in_place(|| {
710                    Handle::current().block_on(store.list_all_graph_edges())
711                })
712                .map(|edges| edges.len())
713                .unwrap_or_else(|e| {
714                    tracing::warn!("graph_edges table unavailable: {e}");
715                    0
716                });
717                json_to_string(&serde_json::json!({
718                    "ok": true,
719                    "facts": stats.total_facts,
720                    "chunks": stats.total_chunks,
721                    "documents": stats.total_documents,
722                    "sessions": stats.total_sessions,
723                    "messages": stats.total_messages,
724                    "graph_edges": graph_edge_count,
725                    "db_size_bytes": stats.database_size_bytes,
726                    "db_size_mb": (stats.database_size_bytes as f64 / 1_048_576.0 * 100.0).round() / 100.0,
727                    "embedding_model": stats.embedding_model,
728                    "embedding_dimensions": stats.embedding_dimensions,
729                }))
730            }
731            Err(e) => Err(ErrorData::internal_error(format!("Stats error: {e}"), None)),
732        }
733    }
734
735    #[tool(
736        description = "Find shortest path between two items in the knowledge graph. Traverses all edge types. Returns node IDs with edge evidence per hop.",
737        annotations(read_only_hint = true)
738    )]
739    fn sm_graph_path(
740        &self,
741        Parameters(GraphPathParams {
742            from_id,
743            to_id,
744            max_depth,
745        }): Parameters<GraphPathParams>,
746    ) -> Result<String, ErrorData> {
747        let depth = max_depth.map(|v| v as usize).unwrap_or(5);
748        let store = &self.bridge.store;
749        let g = store.graph_view();
750
751        match g.path(&from_id, &to_id, depth) {
752            Ok(Some(path)) => {
753                // Build edge evidence for each hop by examining neighbors.
754                let path_segments = build_path_segments(store, &path);
755                json_to_string(&serde_json::json!({
756                    "ok": true,
757                    "from": from_id,
758                    "to": to_id,
759                    "path": path,
760                    "path_length": path.len(),
761                    "segments": path_segments,
762                }))
763            }
764            Ok(None) => json_to_string(&serde_json::json!({
765                "ok": true,
766                "from": from_id,
767                "to": to_id,
768                "path": null,
769                "message": format!("No path found from {from_id} to {to_id} within depth {depth}"),
770            })),
771            Err(e) => Err(ErrorData::internal_error(
772                format!("Graph view error: {e}"),
773                None,
774            )),
775        }
776    }
777
778    // ── Direct read and supersession tools (v0.3.1) ──────────────────
779
780    #[tool(
781        description = "Fetch one fact by id (bare UUID or prefixed 'fact:<uuid>'). Returns full content, namespace, source, timestamps, and metadata.",
782        annotations(read_only_hint = true)
783    )]
784    fn sm_get_fact(
785        &self,
786        Parameters(GetFactParams { fact_id }): Parameters<GetFactParams>,
787    ) -> Result<String, ErrorData> {
788        let bare = fact_id
789            .strip_prefix("fact:")
790            .unwrap_or(&fact_id)
791            .to_string();
792        let store = &self.bridge.store;
793        let result =
794            tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&bare)));
795        match result {
796            Ok(Some(f)) => json_to_string(&serde_json::json!({
797                "ok": true,
798                "found": true,
799                "fact": {
800                    "result_id": format!("fact:{}", f.id),
801                    "id": f.id,
802                    "namespace": f.namespace,
803                    "content": f.content,
804                    "source": f.source,
805                    "created_at": f.created_at,
806                    "updated_at": f.updated_at,
807                    "metadata": f.metadata,
808                },
809            })),
810            Ok(None) => json_to_string(&serde_json::json!({
811                "ok": true,
812                "found": false,
813                "message": format!("No fact with id '{fact_id}'"),
814            })),
815            Err(e) => Err(ErrorData::internal_error(
816                format!("get_fact error: {e}"),
817                None,
818            )),
819        }
820    }
821
822    #[tool(
823        description = "Enumerate facts in a namespace (newest first) with pagination. Exhaustive, not similarity-ranked — for browsing, auditing, or deduping.",
824        annotations(read_only_hint = true)
825    )]
826    fn sm_list_facts(
827        &self,
828        Parameters(ListFactsParams {
829            namespace,
830            limit,
831            offset,
832        }): Parameters<ListFactsParams>,
833    ) -> Result<String, ErrorData> {
834        let lim = limit.map(|v| v as usize).unwrap_or(50);
835        let off = offset.map(|v| v as usize).unwrap_or(0);
836        let store = &self.bridge.store;
837        let result = tokio::task::block_in_place(|| {
838            Handle::current().block_on(store.list_facts(&namespace, lim, off))
839        });
840        match result {
841            Ok(facts) => {
842                let arr: Vec<serde_json::Value> = facts
843                    .iter()
844                    .map(|f| {
845                        serde_json::json!({
846                            "result_id": format!("fact:{}", f.id),
847                            "id": f.id,
848                            "namespace": f.namespace,
849                            "content": f.content,
850                            "source": f.source,
851                            "updated_at": f.updated_at,
852                        })
853                    })
854                    .collect();
855                json_to_string(&serde_json::json!({
856                    "ok": true,
857                    "namespace": namespace,
858                    "count": arr.len(),
859                    "limit": lim,
860                    "offset": off,
861                    "facts": arr,
862                }))
863            }
864            Err(e) => Err(ErrorData::internal_error(
865                format!("list_facts error: {e}"),
866                None,
867            )),
868        }
869    }
870
871    #[tool(
872        description = "List namespaces that currently contain facts. Use before sm_list_facts to discover what is stored.",
873        annotations(read_only_hint = true)
874    )]
875    fn sm_list_namespaces(&self) -> Result<String, ErrorData> {
876        let store = &self.bridge.store;
877        let result = tokio::task::block_in_place(|| {
878            Handle::current().block_on(store.list_fact_namespaces())
879        });
880        match result {
881            Ok(ns) => json_to_string(&serde_json::json!({
882                "ok": true,
883                "count": ns.len(),
884                "namespaces": ns,
885            })),
886            Err(e) => Err(ErrorData::internal_error(
887                format!("list_namespaces error: {e}"),
888                None,
889            )),
890        }
891    }
892
893    #[tool(
894        description = "Fetch a fact plus its graph neighbors WITH their content in one call. Hydrates neighbor facts for ids returned by graph tools.",
895        annotations(read_only_hint = true)
896    )]
897    fn sm_get_fact_neighbors(
898        &self,
899        Parameters(GetFactNeighborsParams { item_id }): Parameters<GetFactNeighborsParams>,
900    ) -> Result<String, ErrorData> {
901        let node_id = if item_id.contains(':') {
902            item_id.clone()
903        } else {
904            format!("fact:{item_id}")
905        };
906        let bare = node_id
907            .strip_prefix("fact:")
908            .unwrap_or(&node_id)
909            .to_string();
910        let store = &self.bridge.store;
911
912        let center =
913            tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&bare)))
914                .map_err(|e| ErrorData::internal_error(format!("get_fact error: {e}"), None))?;
915        let edges = tokio::task::block_in_place(|| {
916            Handle::current().block_on(store.list_graph_edges_for_node(&node_id))
917        })
918        .map_err(|e| ErrorData::internal_error(format!("list edges error: {e}"), None))?;
919
920        let mut neighbors: Vec<serde_json::Value> = Vec::new();
921        for e in &edges {
922            let outgoing = e.source == node_id;
923            let other = if outgoing { &e.target } else { &e.source };
924            let other_bare = other.strip_prefix("fact:").unwrap_or(other).to_string();
925            let content = tokio::task::block_in_place(|| {
926                Handle::current().block_on(store.get_fact(&other_bare))
927            })
928            .ok()
929            .flatten()
930            .map(|f| f.content);
931            neighbors.push(serde_json::json!({
932                "neighbor_id": other,
933                "direction": if outgoing { "out" } else { "in" },
934                "edge_type": e.edge_type,
935                "weight": e.weight,
936                "content": content,
937            }));
938        }
939        json_to_string(&serde_json::json!({
940            "ok": true,
941            "item_id": node_id,
942            "center_content": center.map(|f| f.content),
943            "neighbor_count": neighbors.len(),
944            "neighbors": neighbors,
945        }))
946    }
947
948    #[tool(
949        description = "Create a replacement fact and link it to a stale fact via 'supersedes' edge. Use instead of deleting outdated facts. Returns new fact id and edge id.",
950        annotations(idempotent_hint = true)
951    )]
952    fn sm_supersede_fact(
953        &self,
954        Parameters(SupersedeFactParams {
955            old_fact_id,
956            content,
957            namespace,
958            source,
959            reason,
960        }): Parameters<SupersedeFactParams>,
961    ) -> Result<String, ErrorData> {
962        use semantic_memory::GraphEdgeType;
963
964        let old_bare = old_fact_id
965            .strip_prefix("fact:")
966            .unwrap_or(&old_fact_id)
967            .to_string();
968        let old_node = format!("fact:{old_bare}");
969        let store = &self.bridge.store;
970        let old =
971            tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&old_bare)))
972                .map_err(|e| ErrorData::internal_error(format!("get old fact error: {e}"), None))?;
973        let Some(old_fact) = old else {
974            return Err(ErrorData::invalid_params(
975                format!("No fact with id '{old_fact_id}'"),
976                None,
977            ));
978        };
979
980        let ns = namespace.unwrap_or_else(|| old_fact.namespace.clone());
981        let new_id = tokio::task::block_in_place(|| {
982            Handle::current().block_on(store.add_fact(&ns, &content, source.as_deref(), None))
983        })
984        .map_err(|e| ErrorData::internal_error(format!("add replacement fact error: {e}"), None))?;
985        let new_node = format!("fact:{new_id}");
986        let metadata = serde_json::json!({
987            "reason": reason.unwrap_or_else(|| "replacement fact supersedes stale fact".to_string()),
988            "old_fact_id": old_bare,
989        });
990        let edge = tokio::task::block_in_place(|| {
991            Handle::current().block_on(store.add_graph_edge(
992                &new_node,
993                &old_node,
994                GraphEdgeType::Entity {
995                    relation: "supersedes".to_string(),
996                },
997                1.0,
998                Some(metadata),
999            ))
1000        })
1001        .map_err(|e| ErrorData::internal_error(format!("add supersedes edge error: {e}"), None))?;
1002
1003        json_to_string(&serde_json::json!({
1004            "ok": true,
1005            "new_fact_id": new_id,
1006            "new_result_id": new_node,
1007            "old_fact_id": old_bare,
1008            "old_result_id": old_node,
1009            "namespace": ns,
1010            "edge_id": edge.id,
1011            "relation": "supersedes",
1012        }))
1013    }
1014
1015    // ── Conversation / session tools (v0.3.0) ────────────────────────
1016
1017    // DEPRECATED #[tool(
1018        // description = "Create a conversation session (container for messages). Returns session id. Use to persist history recallable via sm_search_conversations.",
1019        // annotations(idempotent_hint = true)
1020    // )]
1021    #[allow(dead_code)]
1022    fn sm_create_session(
1023        &self,
1024        Parameters(CreateSessionParams { channel, metadata }): Parameters<CreateSessionParams>,
1025    ) -> Result<String, ErrorData> {
1026        let meta: Option<serde_json::Value> = metadata
1027            .as_deref()
1028            .and_then(|s| serde_json::from_str(s).ok());
1029        let store = &self.bridge.store;
1030        let result = tokio::task::block_in_place(|| {
1031            Handle::current().block_on(store.create_session_with_metadata(&channel, meta))
1032        });
1033        match result {
1034            Ok(id) => json_to_string(
1035                &serde_json::json!({"ok": true, "session_id": id, "channel": channel}),
1036            ),
1037            Err(e) => Err(ErrorData::internal_error(
1038                format!("create_session error: {e}"),
1039                None,
1040            )),
1041        }
1042    }
1043
1044    // DEPRECATED #[tool(
1045        // description = "Append a message to a session. role: user|assistant|system|tool. Message is embedded and FTS-indexed. Returns message id."
1046    // )]
1047    #[allow(dead_code)]
1048    fn sm_add_message(
1049        &self,
1050        Parameters(AddMessageParams {
1051            session_id,
1052            role,
1053            content,
1054        }): Parameters<AddMessageParams>,
1055    ) -> Result<String, ErrorData> {
1056        let parsed_role = match role.to_lowercase().as_str() {
1057            "user" => semantic_memory::types::Role::User,
1058            "assistant" => semantic_memory::types::Role::Assistant,
1059            "system" => semantic_memory::types::Role::System,
1060            "tool" => semantic_memory::types::Role::Tool,
1061            other => {
1062                return Err(ErrorData::invalid_params(
1063                    format!("invalid role '{other}' (use user|assistant|system|tool)"),
1064                    None,
1065                ))
1066            }
1067        };
1068        let store = &self.bridge.store;
1069        let result = tokio::task::block_in_place(|| {
1070            Handle::current().block_on(store.add_message_embedded(
1071                &session_id,
1072                parsed_role,
1073                &content,
1074                None,
1075                None,
1076            ))
1077        });
1078        match result {
1079            Ok(id) => json_to_string(
1080                &serde_json::json!({"ok": true, "message_id": id, "session_id": session_id}),
1081            ),
1082            Err(e) => Err(ErrorData::internal_error(
1083                format!("add_message error: {e}"),
1084                None,
1085            )),
1086        }
1087    }
1088
1089    // DEPRECATED #[tool(description = "List recent conversation sessions (newest first) with message counts.", annotations(read_only_hint = true))]
1090    #[allow(dead_code)]
1091    fn sm_list_sessions(
1092        &self,
1093        Parameters(ListSessionsParams { limit, offset }): Parameters<ListSessionsParams>,
1094    ) -> Result<String, ErrorData> {
1095        let lim = limit.map(|v| v as usize).unwrap_or(20);
1096        let off = offset.map(|v| v as usize).unwrap_or(0);
1097        let store = &self.bridge.store;
1098        let result = tokio::task::block_in_place(|| {
1099            Handle::current().block_on(store.list_sessions(lim, off))
1100        });
1101        match result {
1102            Ok(sessions) => json_to_string(&serde_json::json!({
1103                "ok": true,
1104                "count": sessions.len(),
1105                "sessions": sessions.iter().map(|s| serde_json::json!({
1106                    "session_id": s.id,
1107                    "channel": s.channel,
1108                    "message_count": s.message_count,
1109                    "created_at": s.created_at,
1110                    "updated_at": s.updated_at,
1111                })).collect::<Vec<_>>(),
1112            })),
1113            Err(e) => Err(ErrorData::internal_error(
1114                format!("list_sessions error: {e}"),
1115                None,
1116            )),
1117        }
1118    }
1119
1120    // DEPRECATED #[tool(
1121        // description = "Get most recent messages from a session within a token budget (default 4000), chronological order. Returns role, content, timestamps.",
1122        // annotations(read_only_hint = true)
1123    // )]
1124    #[allow(dead_code)]
1125    fn sm_get_messages(
1126        &self,
1127        Parameters(GetMessagesParams {
1128            session_id,
1129            max_tokens,
1130        }): Parameters<GetMessagesParams>,
1131    ) -> Result<String, ErrorData> {
1132        let budget = max_tokens.unwrap_or(4000);
1133        let store = &self.bridge.store;
1134        let result = tokio::task::block_in_place(|| {
1135            Handle::current().block_on(store.get_messages_within_budget(&session_id, budget))
1136        });
1137        match result {
1138            Ok(msgs) => json_to_string(&serde_json::json!({
1139                "ok": true,
1140                "session_id": session_id,
1141                "count": msgs.len(),
1142                "messages": msgs.iter().map(|m| serde_json::json!({
1143                    "id": m.id,
1144                    "role": m.role,
1145                    "content": m.content,
1146                    "token_count": m.token_count,
1147                    "created_at": m.created_at,
1148                })).collect::<Vec<_>>(),
1149            })),
1150            Err(e) => Err(ErrorData::internal_error(
1151                format!("get_messages error: {e}"),
1152                None,
1153            )),
1154        }
1155    }
1156
1157    #[tool(
1158        description = "Hybrid semantic search over stored conversation MESSAGES (not facts). Recall what was discussed in past sessions. Returns ranked messages.",
1159        annotations(read_only_hint = true)
1160    )]
1161    fn sm_search_conversations(
1162        &self,
1163        Parameters(SearchConversationsParams { query, top_k }): Parameters<
1164            SearchConversationsParams,
1165        >,
1166    ) -> Result<String, ErrorData> {
1167        let k = top_k.map(|v| v as usize);
1168        let store = &self.bridge.store;
1169        let result = tokio::task::block_in_place(|| {
1170            Handle::current().block_on(store.search_conversations(&query, k, None))
1171        });
1172        match result {
1173            Ok(results) => json_to_string(&serde_json::json!({
1174                "ok": true,
1175                "count": results.len(),
1176                "results": results.iter().map(|r| serde_json::json!({
1177                    "result_id": r.source.result_id(),
1178                    "content": r.content,
1179                    "score": r.score,
1180                    "cosine_similarity": r.cosine_similarity,
1181                })).collect::<Vec<_>>(),
1182            })),
1183            Err(e) => Err(ErrorData::internal_error(
1184                format!("search_conversations error: {e}"),
1185                None,
1186            )),
1187        }
1188    }
1189
1190    // ── Feature-gated tools ──────────────────────────────────────────
1191    // Note: cfg gates are removed from individual tool methods because
1192    // rmcp's #[tool_router] macro needs all tools visible at expansion
1193    // time. The `full` feature in Cargo.toml already enables the
1194    // semantic-memory sub-features these tools depend on.
1195
1196    #[tool(
1197        description = "Profile a query and get an adaptive routing decision. Determines which retrieval stages (BM25, vector, rerank, graph, decoder, discord) to activate.",
1198        annotations(read_only_hint = true)
1199    )]
1200    fn sm_route_query(
1201        &self,
1202        Parameters(RouteQueryParams { query }): Parameters<RouteQueryParams>,
1203    ) -> Result<String, ErrorData> {
1204        use semantic_memory::routing::RetrievalRouter;
1205
1206        let router = RetrievalRouter {
1207            decoder_enabled: true,
1208            discord_enabled: true,
1209            corpus_density: 0.5,
1210            ..Default::default()
1211        };
1212
1213        let decision = router.route_query(&query);
1214        json_to_string(&serde_json::json!({
1215            "ok": true,
1216            "bm25_coarse": decision.bm25_coarse,
1217            "vector_medium": decision.vector_medium,
1218            "rerank_fine": decision.rerank_fine,
1219            "graph_expansion": decision.graph_expansion,
1220            "decoder": decision.decoder,
1221            "discord": decision.discord,
1222            "no_retrieval": decision.no_retrieval,
1223            "reasoning": decision.reasoning,
1224        }))
1225    }
1226
1227    #[tool(
1228        description = "Adaptive search: profiles query, routes to appropriate stages, applies factor graph belief propagation if decoder is activated. Returns results with stable IDs.",
1229        annotations(read_only_hint = true)
1230    )]
1231    fn sm_search_with_routing(
1232        &self,
1233        Parameters(SearchWithRoutingParams {
1234            query,
1235            top_k,
1236            contradictions,
1237            group_by_community,
1238        }): Parameters<SearchWithRoutingParams>,
1239    ) -> Result<String, ErrorData> {
1240        use semantic_memory::integration::plan_execution;
1241        use semantic_memory::rl_routing::route_with_rl;
1242        use semantic_memory::routing::QueryProfile;
1243
1244        let k = top_k.map(|v| v as usize).unwrap_or(5);
1245        let allow_superseded = query_allows_superseded(&query);
1246        let search_k = if allow_superseded { k } else { (k * 4).max(20) };
1247
1248        // Load persisted RL routing policy (or default if none saved yet)
1249        let store = &self.bridge.store;
1250        let policy = tokio::task::block_in_place(|| {
1251            Handle::current().block_on(store.load_routing_policy())
1252        })
1253        .ok()
1254        .flatten()
1255        .unwrap_or_default();
1256        let profile = QueryProfile::from_query(&query);
1257        let decision = route_with_rl(&policy, &profile);
1258        let contras = contradictions.unwrap_or_default();
1259        let plan = plan_execution(&decision, contras.clone());
1260
1261        let store = &self.bridge.store;
1262        let search_result = tokio::task::block_in_place(|| {
1263            Handle::current().block_on(store.search(&query, Some(search_k), None, None))
1264        });
1265
1266        match search_result {
1267            Ok(results) => {
1268                let superseded_targets = if allow_superseded {
1269                    HashSet::new()
1270                } else {
1271                    load_superseded_targets(store)?
1272                };
1273                let fresh_results: Vec<_> = results
1274                    .iter()
1275                    .filter(|r| !superseded_targets.contains(&r.source.result_id()))
1276                    .collect();
1277                let result_refs: Vec<_> =
1278                    if superseded_targets.is_empty() || fresh_results.is_empty() {
1279                        results.iter().collect()
1280                    } else {
1281                        fresh_results
1282                    };
1283                let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
1284                let json_results: Vec<serde_json::Value> = result_refs
1285                    .iter()
1286                    .take(k)
1287                    .map(|r| {
1288                        serde_json::json!({
1289                            "result_id": r.source.result_id(),
1290                            "content": r.content,
1291                            "score": r.score,
1292                        })
1293                    })
1294                    .collect();
1295
1296                let mut factor_graph_payload = serde_json::json!({
1297                    "enabled": false,
1298                });
1299
1300                let mut decoder_executed = false;
1301                let mut discord_executed = false;
1302                let mut discord_results_payload: Vec<serde_json::Value> = Vec::new();
1303
1304                if decision.decoder {
1305                    #[cfg(feature = "full")]
1306                    {
1307                        use semantic_memory::factor_graph::{
1308                            factors_from_edges, FactorGraph, FactorGraphConfig,
1309                        };
1310
1311                        let graph_edges = tokio::task::block_in_place(|| {
1312                            Handle::current().block_on(store.list_all_graph_edges())
1313                        });
1314
1315                        match graph_edges {
1316                            Ok(edges) => {
1317                                let raw_edges: Vec<(
1318                                    String,
1319                                    String,
1320                                    semantic_memory::GraphEdgeType,
1321                                    f64,
1322                                    Option<String>,
1323                                )> = edges
1324                                    .iter()
1325                                    .map(|edge| {
1326                                        let parsed_type = edge
1327                                            .edge_type_parsed
1328                                            .clone()
1329                                            .or_else(|| serde_json::from_str(&edge.edge_type).ok())
1330                                            .unwrap_or(semantic_memory::GraphEdgeType::Entity {
1331                                                relation: "unknown".to_string(),
1332                                            });
1333                                        (
1334                                            edge.source.clone(),
1335                                            edge.target.clone(),
1336                                            parsed_type,
1337                                            edge.weight,
1338                                            edge.metadata.clone(),
1339                                        )
1340                                    })
1341                                    .collect();
1342
1343                                let nodes: Vec<(String, f64)> = result_refs
1344                                    .iter()
1345                                    .map(|r| (r.source.result_id(), r.score))
1346                                    .collect();
1347                                let factors = factors_from_edges(&raw_edges);
1348                                let graph =
1349                                    FactorGraph::new(&nodes, factors, FactorGraphConfig::default());
1350                                let propagated = graph.propagate();
1351                                let top_beliefs = propagated.top_k(k);
1352
1353                                factor_graph_payload = serde_json::json!({
1354                                    "enabled": true,
1355                                    "top_k_beliefs": top_beliefs
1356                                        .into_iter()
1357                                        .map(|(item_id, belief)| serde_json::json!({
1358                                            "item_id": item_id,
1359                                            "belief": belief,
1360                                        }))
1361                                        .collect::<Vec<_>>(),
1362                                    "iterations": propagated.iterations,
1363                                    "converged": propagated.converged,
1364                                    "elapsed_ms": propagated.elapsed_ms,
1365                                    "factor_counts": {
1366                                        "semantic": propagated.factor_counts.semantic,
1367                                        "temporal": propagated.factor_counts.temporal,
1368                                        "causal": propagated.factor_counts.causal,
1369                                        "entity": propagated.factor_counts.entity,
1370                                        "total": propagated.factor_counts.total(),
1371                                    },
1372                                });
1373                                decoder_executed = true;
1374                            }
1375                            Err(e) => {
1376                                factor_graph_payload = serde_json::json!({
1377                                    "enabled": false,
1378                                    "error": format!("factor graph analysis failed: {e}"),
1379                                });
1380                            }
1381                        }
1382                    }
1383
1384                    #[cfg(not(feature = "full"))]
1385                    {
1386                        factor_graph_payload = serde_json::json!({
1387                            "enabled": false,
1388                            "reason": "factor graph analysis requires the `full` feature",
1389                        });
1390                    }
1391
1392                    if !plan.contradictions.is_empty() {
1393                        use semantic_memory::decoder::{compute_correction, detect_syndromes};
1394                        let result_scores: Vec<(String, f64)> = result_refs
1395                            .iter()
1396                            .map(|r| (r.source.result_id(), r.score))
1397                            .collect();
1398                        let syndromes = detect_syndromes(&result_scores, &plan.contradictions);
1399                        let _ = compute_correction(&syndromes, 10.0);
1400                        decoder_executed = true;
1401                    }
1402                }
1403
1404                if plan.use_discord {
1405                    use semantic_memory::discord::DiscordScorer;
1406                    let direct_ids: Vec<String> = result_refs
1407                        .iter()
1408                        .map(|r| r.source.result_id())
1409                        .collect();
1410                    let existing_ids: std::collections::HashSet<String> =
1411                        direct_ids.iter().cloned().collect();
1412                    if let Ok(edges) =
1413                        load_neighborhood_edge_refs(&self.bridge.store, &direct_ids)
1414                    {
1415                        let scorer = DiscordScorer::with_defaults();
1416                        let discord_hits = scorer.score(&direct_ids, &edges);
1417                        for hit in &discord_hits {
1418                            if !existing_ids.contains(&hit.item_id) {
1419                                discord_results_payload.push(serde_json::json!({
1420                                    "result_id": hit.item_id,
1421                                    "discord_score": hit.discord_score,
1422                                    "anchor_ids": hit.anchor_ids,
1423                                    "relationship_types": hit.relationship_types,
1424                                }));
1425                            }
1426                        }
1427                        discord_executed = true;
1428                    }
1429                }
1430
1431                let mut matryoshka_payload = serde_json::json!({
1432                    "enabled": false,
1433                });
1434                if decision.vector_medium {
1435                    #[cfg(feature = "full")]
1436                    {
1437                        use semantic_memory::integration::multi_resolution_route;
1438                        use semantic_memory::matryoshka::MatryoshkaConfig;
1439                        use semantic_memory::routing::QueryProfile;
1440
1441                        let route_profile = QueryProfile::from_query(&query);
1442                        let route_decision =
1443                            multi_resolution_route(&route_profile, &MatryoshkaConfig::default());
1444                        matryoshka_payload = serde_json::json!({
1445                            "enabled": true,
1446                            "candidate_dim": route_decision.candidate_dim,
1447                            "heuristic_recall_estimate": route_decision.estimated_recall,
1448                            "recall_basis": "heuristic_dimensional_model_not_corpus_measured",
1449                            "embedding_dim": route_decision.embedding_dim,
1450                            "reasoning": route_decision.reasoning,
1451                        });
1452                    }
1453
1454                    #[cfg(not(feature = "full"))]
1455                    {
1456                        matryoshka_payload = serde_json::json!({
1457                            "enabled": false,
1458                            "reason": "matryoshka routing requires the `full` feature",
1459                        });
1460                    }
1461                }
1462
1463                // Community grouping (opt-in).
1464                let grouped_results_payload: serde_json::Value =
1465                    if group_by_community == Some(true) {
1466                        let seed_ids: Vec<String> = result_refs
1467                            .iter()
1468                            .take(k)
1469                            .map(|r| r.source.result_id())
1470                            .collect();
1471                        let edges =
1472                            load_neighborhood_edge_pairs(store, &seed_ids).unwrap_or_default();
1473                        if !edges.is_empty() {
1474                            use semantic_memory::community::detect_communities;
1475                            let communities = detect_communities(&edges, 1.0, 42);
1476                            let mut member_to_comm: std::collections::HashMap<String, String> =
1477                                std::collections::HashMap::new();
1478                            for c in &communities {
1479                                for m in &c.members {
1480                                    member_to_comm.insert(m.clone(), c.id.clone());
1481                                }
1482                            }
1483                            let mut groups: std::collections::HashMap<
1484                                String,
1485                                Vec<serde_json::Value>,
1486                            > = std::collections::HashMap::new();
1487                            let mut ungrouped: Vec<serde_json::Value> = Vec::new();
1488                            for r in &json_results {
1489                                if let Some(rid) =
1490                                    r.get("result_id").and_then(|v| v.as_str())
1491                                {
1492                                    match member_to_comm.get(rid).cloned() {
1493                                        Some(cid) => {
1494                                            groups.entry(cid).or_default().push(r.clone())
1495                                        }
1496                                        None => ungrouped.push(r.clone()),
1497                                    }
1498                                }
1499                            }
1500                            let mut map = serde_json::Map::new();
1501                            for (cid, items) in groups {
1502                                map.insert(
1503                                    format!("community_{cid}"),
1504                                    serde_json::json!(items),
1505                                );
1506                            }
1507                            if !ungrouped.is_empty() {
1508                                map.insert(
1509                                    "ungrouped".to_string(),
1510                                    serde_json::json!(ungrouped),
1511                                );
1512                            }
1513                            serde_json::Value::Object(map)
1514                        } else {
1515                            serde_json::Value::Null
1516                        }
1517                    } else {
1518                        serde_json::Value::Null
1519                    };
1520
1521                // Task 7: Auto-call topology when routing returns Class D (SYNTHESIS) and >10 results.
1522                let mut topology_payload = serde_json::json!({ "auto_called": false });
1523                {
1524                    use semantic_memory::routing::{QueryComplexityClass, QueryProfile};
1525                    let route_profile = QueryProfile::from_query(&query);
1526                    if route_profile.complexity_class == QueryComplexityClass::Synthesis
1527                        && result_refs.len() > 10
1528                    {
1529                        #[cfg(feature = "full")]
1530                        {
1531                            use semantic_memory::topology::{compute_betti_numbers, find_voids};
1532                            let edges = load_stored_edge_pairs(store).unwrap_or_default();
1533                            if !edges.is_empty() {
1534                                let mut adjacency: std::collections::HashMap<String, Vec<String>> =
1535                                    std::collections::HashMap::new();
1536                                for (src, tgt) in &edges {
1537                                    adjacency.entry(src.clone()).or_default().push(tgt.clone());
1538                                    adjacency.entry(tgt.clone()).or_default().push(src.clone());
1539                                }
1540                                let betti = compute_betti_numbers(&adjacency);
1541                                let voids = find_voids(&edges);
1542                                topology_payload = serde_json::json!({
1543                                    "auto_called": true,
1544                                    "trigger": "synthesis_class_with_10_plus_results",
1545                                    "betti_numbers": {
1546                                        "betti_0": betti.betti_0,
1547                                        "betti_1": betti.betti_1,
1548                                    },
1549                                    "void_count": voids.len(),
1550                                    "voids": voids.iter().map(|v| serde_json::json!({
1551                                        "description": v.description,
1552                                        "void_type": format!("{:?}", v.void_type),
1553                                        "nearby_items": v.nearby_items,
1554                                        "suggested_connections": v.suggested_connections,
1555                                    })).collect::<Vec<_>>(),
1556                                });
1557                            } else {
1558                                topology_payload = serde_json::json!({
1559                                    "auto_called": true,
1560                                    "trigger": "synthesis_class_with_10_plus_results",
1561                                    "note": "no graph edges in store",
1562                                });
1563                            }
1564                        }
1565                        #[cfg(not(feature = "full"))]
1566                        {
1567                            topology_payload = serde_json::json!({
1568                                "auto_called": true,
1569                                "trigger": "synthesis_class_with_10_plus_results",
1570                                "error": "topology requires the full feature",
1571                            });
1572                        }
1573                    }
1574                }
1575
1576                json_to_string(&serde_json::json!({
1577                    "ok": true,
1578                    "routing_decision": {
1579                        "bm25_coarse": decision.bm25_coarse,
1580                        "vector_medium": decision.vector_medium,
1581                        "rerank_fine": decision.rerank_fine,
1582                        "graph_expansion": decision.graph_expansion,
1583                        "decoder": decision.decoder,
1584                        "discord": decision.discord,
1585                        "no_retrieval": decision.no_retrieval,
1586                        "reasoning": decision.reasoning,
1587                    },
1588                    "results": json_results,
1589                    "count": json_results.len(),
1590                    "superseded_filtered_count": superseded_filtered_count,
1591                    "decoder_planned": plan.use_decoder,
1592                    "decoder_executed": decoder_executed,
1593                    "discord_planned": plan.use_discord,
1594                    "discord_executed": discord_executed,
1595                    "discord_results": discord_results_payload,
1596                    "factor_graph": factor_graph_payload,
1597                    "matryoshka": matryoshka_payload,
1598                    "grouped_results": grouped_results_payload,
1599                    "topology": topology_payload,
1600                }))
1601            }
1602            Err(e) => Err(ErrorData::internal_error(
1603                format!("Search error: {e}"),
1604                None,
1605            )),
1606        }
1607    }
1608
1609    #[tool(
1610        description = "Detect contradictions in search results. Runs syndrome detection, computes corrections, and applies belief propagation to refine confidence scores.",
1611        annotations(read_only_hint = true)
1612    )]
1613    fn sm_decoder_analyze(
1614        &self,
1615        Parameters(DecoderAnalyzeParams {
1616            results,
1617            contradictions,
1618        }): Parameters<DecoderAnalyzeParams>,
1619    ) -> Result<String, ErrorData> {
1620        use semantic_memory::decoder::{
1621            compute_correction, detect_syndromes, pass_messages, ConflictGraph,
1622        };
1623
1624        let contras = contradictions.unwrap_or_default();
1625        let syndromes = detect_syndromes(&results, &contras);
1626        let corrections = compute_correction(&syndromes, 10.0);
1627        let graph = ConflictGraph::from_syndromes(&results, &syndromes);
1628        let mp = pass_messages(&graph, 50, 0.001);
1629
1630        json_to_string(&serde_json::json!({
1631            "ok": true,
1632            "syndromes": syndromes.iter().map(|s| serde_json::json!({
1633                "id": s.id,
1634                "severity": format!("{:?}", s.severity),
1635                "items": s.items,
1636                "description": s.description,
1637                "type": format!("{:?}", s.syndrome_type),
1638            })).collect::<Vec<_>>(),
1639            "syndrome_count": syndromes.len(),
1640            "corrections": corrections.iter().map(|c| serde_json::json!({
1641                "id": c.id,
1642                "confidence": c.confidence,
1643                "cost": c.cost,
1644                "operations": c.operations.len(),
1645            })).collect::<Vec<_>>(),
1646            "correction_count": corrections.len(),
1647            "message_passing": {
1648                "iterations": mp.iterations,
1649                "converged": mp.converged,
1650                "elapsed_ms": mp.elapsed_ms,
1651            },
1652        }))
1653    }
1654
1655    #[tool(
1656        description = "Detect contradictions among the top results for a query from their CONTENT (numeric, value, negation, or antonym disagreement) — no pre-asserted edges required. Returns candidate conflicting pairs, each with the signals that fired and a human-readable reason. Persist a confirmed pair with sm_add_graph_edge(edge_type=\"contradicts\") so the decoder/community/factor-graph tools pick it up.",
1657        annotations(read_only_hint = true)
1658    )]
1659    fn sm_detect_contradictions(
1660        &self,
1661        Parameters(DetectContradictionsParams { query, top_k }): Parameters<DetectContradictionsParams>,
1662    ) -> Result<String, ErrorData> {
1663        use semantic_memory::contradiction_detect::{detect_contradictions, DetectorConfig};
1664
1665        let k = top_k.map(|v| v as usize).unwrap_or(10);
1666        let store = &self.bridge.store;
1667        let results = tokio::task::block_in_place(|| {
1668            Handle::current().block_on(store.search(&query, Some(k), None, None))
1669        })
1670        .map_err(|e| ErrorData::internal_error(format!("search failed: {e}"), None))?;
1671
1672        let items: Vec<(String, String)> = results
1673            .iter()
1674            .map(|r| (r.source.result_id(), r.content.clone()))
1675            .collect();
1676
1677        let pairs = detect_contradictions(&items, &DetectorConfig::default());
1678
1679        json_to_string(&serde_json::json!({
1680            "ok": true,
1681            "query": query,
1682            "items_scanned": items.len(),
1683            "contradictions": pairs.iter().map(|p| serde_json::json!({
1684                "a": p.a,
1685                "b": p.b,
1686                "score": p.score,
1687                "signals": p.signals.iter().map(|s| format!("{s:?}")).collect::<Vec<_>>(),
1688                "reason": p.reason,
1689            })).collect::<Vec<_>>(),
1690            "count": pairs.len(),
1691        }))
1692    }
1693
1694    #[tool(
1695        description = "Second-order retrieval: find items related to your search results through the graph, but NOT themselves direct hits. Loads edges from store automatically.",
1696        annotations(read_only_hint = true)
1697    )]
1698    fn sm_discord_search(
1699        &self,
1700        Parameters(DiscordSearchParams { direct_result_ids }): Parameters<DiscordSearchParams>,
1701    ) -> Result<String, ErrorData> {
1702        use semantic_memory::discord::DiscordScorer;
1703
1704        // Use neighborhood loading: only load edges within 2 hops of the
1705        // direct result IDs instead of the entire graph.
1706        let edges = load_neighborhood_edge_refs(&self.bridge.store, &direct_result_ids)?;
1707        let scorer = DiscordScorer::with_defaults();
1708        let results = scorer.score(&direct_result_ids, &edges);
1709
1710        json_to_string(&serde_json::json!({
1711            "ok": true,
1712            "discord_results": results.iter().map(|r| serde_json::json!({
1713                "item_id": r.item_id,
1714                "discord_score": r.discord_score,
1715                "anchor_ids": r.anchor_ids,
1716                "relationship_types": r.relationship_types,
1717            })).collect::<Vec<_>>(),
1718            "count": results.len(),
1719            "edges_loaded": edges.len(),
1720            "edges_scope": "neighborhood",
1721        }))
1722    }
1723
1724    #[tool(
1725        description = "Set provenance (evidence confidence) for an item. Confidence in [0.0, 1.0] with support count. Returns a provenance receipt.",
1726        annotations(idempotent_hint = true)
1727    )]
1728    fn sm_set_provenance(
1729        &self,
1730        Parameters(SetProvenanceParams {
1731            item_id,
1732            confidence,
1733            support_count,
1734        }): Parameters<SetProvenanceParams>,
1735    ) -> Result<String, ErrorData> {
1736        use semantic_memory::provenance::{
1737            ConfidenceSemiring, ConfidenceValue, ProvenanceItemType,
1738        };
1739
1740        // SM-AUD-015: Validate confidence is finite and in [0, 1].
1741        if !confidence.is_finite() || confidence < 0.0 || confidence > 1.0 {
1742            return Err(ErrorData::invalid_params(
1743                format!("confidence must be a finite value in [0.0, 1.0], got {confidence}"),
1744                None,
1745            ));
1746        }
1747
1748        let value = ConfidenceValue::new(confidence, support_count);
1749        let store = &self.bridge.store;
1750
1751        let result = tokio::task::block_in_place(|| {
1752            Handle::current().block_on(store.set_provenance::<ConfidenceSemiring>(
1753                &ProvenanceItemType::Fact,
1754                &item_id,
1755                &value,
1756                &[],
1757                None,
1758            ))
1759        });
1760
1761        match result {
1762            Ok(receipt) => json_to_string(&serde_json::json!({
1763                "ok": true,
1764                "provenance_id": receipt.provenance_id,
1765                "item_id": receipt.item_id,
1766                "semiring_type": receipt.semiring_type,
1767                "recorded_at": receipt.recorded_at,
1768                "message": "Provenance set successfully",
1769            })),
1770            Err(e) => Err(ErrorData::internal_error(
1771                format!("Provenance error: {e}"),
1772                None,
1773            )),
1774        }
1775    }
1776
1777    #[tool(
1778        description = "Run a memory lifecycle pass: analyze items for syndromes, compute corrections, identify subtraction candidates, and check compression needs.",
1779        annotations(read_only_hint = true)
1780    )]
1781    fn sm_run_lifecycle(
1782        &self,
1783        Parameters(RunLifecycleParams { item_ids }): Parameters<RunLifecycleParams>,
1784    ) -> Result<String, ErrorData> {
1785        use semantic_memory::decoder::{compute_correction, detect_syndromes};
1786        use semantic_memory::integration::{
1787            corrections_to_subtraction_candidates, should_trigger_recompression,
1788        };
1789
1790        let results: Vec<(String, f64)> = item_ids.iter().map(|id| (id.clone(), 0.5)).collect();
1791        let syndromes = detect_syndromes(&results, &[]);
1792        let corrections = compute_correction(&syndromes, 10.0);
1793
1794        let sub_candidates = corrections_to_subtraction_candidates(&corrections);
1795
1796        let subtracted_count = sub_candidates.len();
1797        let remaining_count = item_ids.len().saturating_sub(subtracted_count);
1798        let recompression = should_trigger_recompression(subtracted_count, remaining_count, false);
1799
1800        let store = &self.bridge.store;
1801        let graph_edges = tokio::task::block_in_place(|| {
1802            Handle::current().block_on(store.list_all_graph_edges())
1803        });
1804        let stored_edges: Vec<(String, String)> = graph_edges
1805            .as_ref()
1806            .map(|edges| {
1807                edges
1808                    .iter()
1809                    .map(|edge| (edge.source.clone(), edge.target.clone()))
1810                    .collect()
1811            })
1812            .unwrap_or_default();
1813
1814        let mut topology_voids: Vec<serde_json::Value> = Vec::new();
1815        let mut betti = serde_json::json!({
1816            "betti_0": 0usize,
1817            "betti_1": 0usize,
1818        });
1819        let mut topology_error: Option<String> = None;
1820
1821        let mut communities: Vec<serde_json::Value> = Vec::new();
1822        let mut community_contradictions: Vec<serde_json::Value> = Vec::new();
1823        let mut community_error: Option<String> = None;
1824
1825        let mut subgraph_assessment = serde_json::json!({
1826            "subgraphs_identified": 0usize,
1827            "subgraphs_pruned": 0usize,
1828        });
1829        let mut subgraph_error: Option<String> = None;
1830
1831        #[cfg(feature = "full")]
1832        {
1833            use std::collections::HashMap;
1834
1835            if !stored_edges.is_empty() {
1836                let analysis_edges = stored_edges.clone();
1837
1838                let topology_result = (|| -> Result<(), String> {
1839                    use semantic_memory::topology::{compute_betti_numbers, find_voids};
1840
1841                    let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
1842                    for (left, right) in &analysis_edges {
1843                        adjacency
1844                            .entry(left.clone())
1845                            .or_default()
1846                            .push(right.clone());
1847                        adjacency
1848                            .entry(right.clone())
1849                            .or_default()
1850                            .push(left.clone());
1851                    }
1852
1853                    let betti_numbers = compute_betti_numbers(&adjacency);
1854                    betti = serde_json::json!({
1855                        "betti_0": betti_numbers.betti_0,
1856                        "betti_1": betti_numbers.betti_1,
1857                    });
1858
1859                    topology_voids = find_voids(&analysis_edges)
1860                        .into_iter()
1861                        .map(|v| {
1862                            serde_json::json!({
1863                                "description": v.description,
1864                                "void_type": format!("{:?}", v.void_type),
1865                                "nearby_items": v.nearby_items,
1866                                "suggested_connections": v.suggested_connections,
1867                            })
1868                        })
1869                        .collect();
1870
1871                    Ok(())
1872                })();
1873
1874                if let Err(e) = topology_result {
1875                    topology_error = Some(e);
1876                }
1877
1878                let community_result = (|| -> Result<(), String> {
1879                    use semantic_memory::community::{
1880                        community_contradiction_scan, detect_communities,
1881                    };
1882
1883                    let detected = detect_communities(&analysis_edges, 1.0, 42);
1884                    communities = detected
1885                        .iter()
1886                        .map(|c| {
1887                            serde_json::json!({
1888                                "id": c.id,
1889                                "members": c.members,
1890                                "level": c.level,
1891                                "parent": c.parent,
1892                                "member_count": c.members.len(),
1893                            })
1894                        })
1895                        .collect();
1896
1897                    community_contradictions = community_contradiction_scan(&detected, &[])
1898                        .into_iter()
1899                        .map(|cc| {
1900                            serde_json::json!({
1901                                "community_id": cc.community_id,
1902                                "item_a": cc.item_a,
1903                                "item_b": cc.item_b,
1904                                "description": cc.description,
1905                            })
1906                        })
1907                        .collect();
1908
1909                    Ok(())
1910                })();
1911
1912                if let Err(e) = community_result {
1913                    community_error = Some(e);
1914                }
1915
1916                let subgraph_result = (|| -> Result<(), String> {
1917                    use semantic_memory::integration::autonomous_subgraph_maintenance;
1918                    use semantic_memory::subgraph_pruning::AccessLog;
1919                    use std::collections::HashSet;
1920
1921                    let mut access_items: HashSet<String> = HashSet::new();
1922                    for (left, right) in &analysis_edges {
1923                        access_items.insert(left.clone());
1924                        access_items.insert(right.clone());
1925                    }
1926
1927                    let access_logs = access_items
1928                        .into_iter()
1929                        .map(|item| AccessLog {
1930                            item_id: item,
1931                            access_count: 1,
1932                            last_accessed: "1970-01-01T00:00:00Z".to_string(),
1933                        })
1934                        .collect::<Vec<_>>();
1935
1936                    let report =
1937                        autonomous_subgraph_maintenance(&analysis_edges, &access_logs, &[], 0);
1938                    subgraph_assessment = serde_json::json!({
1939                        "subgraphs_identified": report.subgraphs_identified,
1940                        "subgraphs_pruned": report.subgraphs_pruned,
1941                        "summary": report.summary,
1942                    });
1943                    Ok(())
1944                })();
1945
1946                if let Err(e) = subgraph_result {
1947                    subgraph_error = Some(e);
1948                }
1949            }
1950        }
1951
1952        #[cfg(not(feature = "full"))]
1953        {
1954            if !stored_edges.is_empty() {
1955                topology_error = Some(
1956                    "topology/community/subgraph phases require the `full` feature".to_string(),
1957                );
1958                community_error = Some(
1959                    "topology/community/subgraph phases require the `full` feature".to_string(),
1960                );
1961                subgraph_error = Some(
1962                    "topology/community/subgraph phases require the `full` feature".to_string(),
1963                );
1964            }
1965        }
1966
1967        #[cfg(feature = "full")]
1968        let (f32_count, compressed_count) =
1969            item_ids
1970                .iter()
1971                .fold((0usize, 0usize), |(f32_count, compressed_count), _| {
1972                    use semantic_memory::compression_governor::{
1973                        decide_quantization, QuantizationLevel,
1974                    };
1975
1976                    match decide_quantization(0.5) {
1977                        QuantizationLevel::F32 => (f32_count + 1, compressed_count),
1978                        _ => (f32_count, compressed_count + 1),
1979                    }
1980                });
1981        #[cfg(not(feature = "full"))]
1982        let (f32_count, compressed_count) = (0usize, 0usize);
1983
1984        json_to_string(&serde_json::json!({
1985            "ok": true,
1986            "items_analyzed": item_ids.len(),
1987            "syndromes_detected": syndromes.len(),
1988            "corrections_computed": corrections.len(),
1989            "subtraction_candidates": sub_candidates.iter().map(|c| serde_json::json!({
1990                "item_id": c.item_id,
1991                "structuring_score": c.structuring_score,
1992                "operation_type": c.operation_type,
1993                "reason": c.reason,
1994            })).collect::<Vec<_>>(),
1995            "recompression_triggered": recompression.triggered,
1996            "recompression_reason": recompression.reason,
1997            "topology": {
1998                "enabled": !stored_edges.is_empty(),
1999                "voids": topology_voids,
2000                "void_count": topology_voids.len(),
2001                "betti_numbers": betti,
2002                "error": topology_error,
2003            },
2004            "community_detection": {
2005                "enabled": !stored_edges.is_empty(),
2006                "communities": communities,
2007                "community_count": communities.len(),
2008                "contradictions": community_contradictions,
2009                "contradiction_count": community_contradictions.len(),
2010                "error": community_error,
2011            },
2012            "subgraph_pruning_assessment": {
2013                "enabled": !stored_edges.is_empty(),
2014                "subgraph_count": subgraph_assessment["subgraphs_identified"].as_u64().unwrap_or(0),
2015                "pruned_count": subgraph_assessment["subgraphs_pruned"].as_u64().unwrap_or(0),
2016                "summary": subgraph_assessment["summary"].as_str().unwrap_or(""),
2017                "error": subgraph_error,
2018            },
2019            "turbo_quantization_assessment": {
2020                "items_assessed": item_ids.len(),
2021                "would_retain_f32": f32_count,
2022                "would_compress": compressed_count,
2023            },
2024            "summary": format!(
2025                "Analyzed {} items: {} syndromes, {} corrections, {} subtraction candidates, recompression: {}",
2026                item_ids.len(), syndromes.len(), corrections.len(), sub_candidates.len(),
2027                if recompression.triggered { "needed" } else { "not needed" }
2028            ),
2029        }))
2030    }
2031
2032    // ── First-class graph edge tools ───────────────────────────────
2033
2034    #[tool(
2035        description = "Add a durable, typed graph edge between two nodes. Edge types: semantic, temporal, causal, entity. Idempotent — same edge returns existing ID.",
2036        annotations(idempotent_hint = true)
2037    )]
2038    fn sm_add_graph_edge(
2039        &self,
2040        Parameters(params): Parameters<AddGraphEdgeParams>,
2041    ) -> Result<String, ErrorData> {
2042        use semantic_memory::GraphEdgeType;
2043
2044        // SM-AUD-015: Validate numeric params are finite and in range.
2045        if let Some(cs) = params.cosine_similarity {
2046            if !cs.is_finite() || cs < 0.0 || cs > 1.0 {
2047                return Err(ErrorData::invalid_params(
2048                    format!("cosine_similarity must be finite and in [0.0, 1.0], got {cs}"),
2049                    None,
2050                ));
2051            }
2052        }
2053        if let Some(conf) = params.confidence {
2054            if !conf.is_finite() || conf < 0.0 || conf > 1.0 {
2055                return Err(ErrorData::invalid_params(
2056                    format!("confidence must be finite and in [0.0, 1.0], got {conf}"),
2057                    None,
2058                ));
2059            }
2060        }
2061
2062        let edge_type = match params.edge_type {
2063            EdgeType::Semantic => GraphEdgeType::Semantic {
2064                cosine_similarity: params.cosine_similarity.unwrap_or(0.5),
2065            },
2066            EdgeType::Temporal => GraphEdgeType::Temporal {
2067                delta_secs: params.delta_secs.unwrap_or(0),
2068            },
2069            EdgeType::Causal => GraphEdgeType::Causal {
2070                confidence: params.confidence.unwrap_or(0.5),
2071                evidence_ids: params.evidence_ids.unwrap_or_default(),
2072            },
2073            EdgeType::Entity => GraphEdgeType::Entity {
2074                relation: params.relation.unwrap_or_else(|| "related".to_string()),
2075            },
2076        };
2077
2078        // MCP-004: Reject malformed metadata JSON instead of silently dropping it.
2079        let metadata = match params.metadata.as_deref() {
2080            None => None,
2081            Some(s) => match serde_json::from_str::<serde_json::Value>(s) {
2082                Ok(v) => Some(v),
2083                Err(e) => {
2084                    return Err(ErrorData::invalid_params(
2085                        format!("metadata is not valid JSON: {e}"),
2086                        None,
2087                    ))
2088                }
2089            },
2090        };
2091
2092        let store = &self.bridge.store;
2093        let result = tokio::task::block_in_place(|| {
2094            Handle::current().block_on(store.add_graph_edge(
2095                &params.source,
2096                &params.target,
2097                edge_type,
2098                params.weight,
2099                metadata,
2100            ))
2101        });
2102
2103        match result {
2104            Ok(edge) => json_to_string(&serde_json::json!({
2105                "ok": true,
2106                "id": edge.id,
2107                "source": edge.source,
2108                "target": edge.target,
2109                "edge_type": edge.edge_type,
2110                "weight": edge.weight,
2111                "content_digest": edge.content_digest,
2112                "recorded_at": edge.recorded_at,
2113                "message": "Graph edge added successfully",
2114            })),
2115            Err(e) => Err(ErrorData::internal_error(
2116                format!("Error adding graph edge: {e}"),
2117                None,
2118            )),
2119        }
2120    }
2121
2122    #[tool(
2123        description = "List graph edges for a specific node (as source or target), or all edges if no node_id. Returns non-invalidated edges only.",
2124        annotations(read_only_hint = true)
2125    )]
2126    fn sm_list_graph_edges(
2127        &self,
2128        Parameters(ListGraphEdgesParams { node_id }): Parameters<ListGraphEdgesParams>,
2129    ) -> Result<String, ErrorData> {
2130        let store = &self.bridge.store;
2131        let result = match node_id {
2132            Some(id) => tokio::task::block_in_place(|| {
2133                Handle::current().block_on(store.list_graph_edges_for_node(&id))
2134            }),
2135            None => tokio::task::block_in_place(|| {
2136                Handle::current().block_on(store.list_all_graph_edges())
2137            }),
2138        };
2139
2140        match result {
2141            Ok(edges) => json_to_string(&serde_json::json!({
2142                "ok": true,
2143                "edges": edges.iter().map(|e| serde_json::json!({
2144                    "id": e.id,
2145                    "source": e.source,
2146                    "target": e.target,
2147                    "edge_type": e.edge_type,
2148                    "weight": e.weight,
2149                    "metadata": e.metadata,
2150                    "recorded_at": e.recorded_at,
2151                })).collect::<Vec<_>>(),
2152                "count": edges.len(),
2153            })),
2154            Err(e) => Err(ErrorData::internal_error(
2155                format!("Error listing graph edges: {e}"),
2156                None,
2157            )),
2158        }
2159    }
2160
2161    #[tool(
2162        description = "Invalidate a stored graph edge by ID. Append-only — edge is never deleted, only marked invalidated with a reason.",
2163        annotations(idempotent_hint = true)
2164    )]
2165    fn sm_invalidate_graph_edge(
2166        &self,
2167        Parameters(InvalidateGraphEdgeParams { edge_id, reason }): Parameters<
2168            InvalidateGraphEdgeParams,
2169        >,
2170    ) -> Result<String, ErrorData> {
2171        let store = &self.bridge.store;
2172        let result = tokio::task::block_in_place(|| {
2173            Handle::current().block_on(store.invalidate_graph_edge(&edge_id, &reason))
2174        });
2175
2176        match result {
2177            Ok(()) => json_to_string(&serde_json::json!({
2178                "ok": true,
2179                "edge_id": edge_id,
2180                "message": "Edge invalidated successfully",
2181            })),
2182            Err(e) => Err(ErrorData::internal_error(
2183                format!("Error invalidating edge: {e}"),
2184                None,
2185            )),
2186        }
2187    }
2188
2189    // ── Factor graph, topology, and community tools ─────────────────
2190
2191    #[tool(
2192        description = "Run factor graph belief propagation on stored graph edges. Models all 4 edge types as factors. Returns unified confidence scores after convergence.",
2193        annotations(read_only_hint = true)
2194    )]
2195    fn sm_factor_graph(
2196        &self,
2197        Parameters(params): Parameters<FactorGraphParams>,
2198    ) -> Result<String, ErrorData> {
2199        use semantic_memory::factor_graph::{factors_from_edges, FactorGraph, FactorGraphConfig};
2200
2201        let defaults = FactorGraphConfig::default();
2202        let config = FactorGraphConfig {
2203            semantic_weight: params.semantic_weight.unwrap_or(defaults.semantic_weight),
2204            temporal_weight: params.temporal_weight.unwrap_or(defaults.temporal_weight),
2205            causal_weight: params.causal_weight.unwrap_or(defaults.causal_weight),
2206            entity_weight: params.entity_weight.unwrap_or(defaults.entity_weight),
2207            self_influence: params.self_influence.unwrap_or(defaults.self_influence),
2208            max_iterations: params
2209                .max_iterations
2210                .map(|v| v as usize)
2211                .unwrap_or(defaults.max_iterations),
2212            convergence_threshold: params
2213                .convergence_threshold
2214                .unwrap_or(defaults.convergence_threshold),
2215        };
2216
2217        // Use neighborhood loading: only load edges within 2 hops of the
2218        // node seeds instead of the entire graph.
2219        let seed_ids: Vec<String> = params.nodes.iter().map(|n| n.item_id.clone()).collect();
2220        let raw_edges = load_neighborhood_factor_edges(&self.bridge.store, &seed_ids)?;
2221        let factors = factors_from_edges(&raw_edges);
2222
2223        let nodes: Vec<(String, f64)> = params
2224            .nodes
2225            .iter()
2226            .map(|n| (n.item_id.clone(), n.initial_belief))
2227            .collect();
2228
2229        let graph = FactorGraph::new(&nodes, factors, config);
2230        let result = graph.propagate();
2231
2232        json_to_string(&serde_json::json!({
2233            "ok": true,
2234            "node_beliefs": result.node_beliefs,
2235            "iterations": result.iterations,
2236            "converged": result.converged,
2237            "elapsed_ms": result.elapsed_ms,
2238            "edges_loaded": raw_edges.len(),
2239            "edges_scope": "neighborhood",
2240            "factor_counts": {
2241                "semantic": result.factor_counts.semantic,
2242                "temporal": result.factor_counts.temporal,
2243                "causal": result.factor_counts.causal,
2244                "entity": result.factor_counts.entity,
2245                "total": result.factor_counts.total(),
2246            },
2247            "config": {
2248                "semantic_weight": result.config.semantic_weight,
2249                "temporal_weight": result.config.temporal_weight,
2250                "causal_weight": result.config.causal_weight,
2251                "entity_weight": result.config.entity_weight,
2252                "self_influence": result.config.self_influence,
2253                "max_iterations": result.config.max_iterations,
2254                "convergence_threshold": result.config.convergence_threshold,
2255            },
2256        }))
2257    }
2258
2259    #[tool(
2260        description = "Find topological voids in the knowledge graph. Computes Betti numbers (components and cycles) and detects structural gaps. Loads edges from store.",
2261        annotations(read_only_hint = true)
2262    )]
2263    fn sm_topology(
2264        &self,
2265        Parameters(_params): Parameters<TopologyParams>,
2266    ) -> Result<String, ErrorData> {
2267        use semantic_memory::topology::{compute_betti_numbers, find_voids, gap_report};
2268
2269        // MCP-001: Load edges from the store, not from caller-supplied params.
2270        let edges = load_stored_edge_pairs(&self.bridge.store)?;
2271
2272        let mut adjacency: std::collections::HashMap<String, Vec<String>> =
2273            std::collections::HashMap::new();
2274        for (src, tgt) in &edges {
2275            adjacency.entry(src.clone()).or_default().push(tgt.clone());
2276            adjacency.entry(tgt.clone()).or_default().push(src.clone());
2277        }
2278
2279        let betti = compute_betti_numbers(&adjacency);
2280        let voids = find_voids(&edges);
2281        let report = gap_report(&voids);
2282
2283        json_to_string(&serde_json::json!({
2284            "ok": true,
2285            "betti_numbers": {
2286                "betti_0": betti.betti_0,
2287                "betti_1": betti.betti_1,
2288            },
2289            "voids": voids.iter().map(|v| serde_json::json!({
2290                "description": v.description,
2291                "nearby_items": v.nearby_items,
2292                "suggested_connections": v.suggested_connections,
2293                "void_type": format!("{:?}", v.void_type),
2294            })).collect::<Vec<_>>(),
2295            "void_count": voids.len(),
2296            "edges_loaded_from_store": edges.len(),
2297            "report": report,
2298        }))
2299    }
2300
2301    #[tool(
2302        description = "Detect communities in the knowledge graph (Leiden-inspired). Returns community assignments, optional contradiction scans, and compression recommendations.",
2303        annotations(read_only_hint = true)
2304    )]
2305    fn sm_community(
2306        &self,
2307        Parameters(params): Parameters<CommunityParams>,
2308    ) -> Result<String, ErrorData> {
2309        use semantic_memory::community::{
2310            community_aware_compression, community_contradiction_scan, detect_communities,
2311        };
2312
2313        // MCP-001: Load edges from the store, not from caller-supplied params.
2314        let edges = load_stored_edge_pairs(&self.bridge.store)?;
2315
2316        let resolution = params.resolution.unwrap_or(1.0);
2317        let seed = params.seed.unwrap_or(42);
2318
2319        let communities = detect_communities(&edges, resolution, seed);
2320
2321        let contradictions = params.contradictions.unwrap_or_default();
2322        let community_contras = community_contradiction_scan(&communities, &contradictions);
2323
2324        let importance_scores = params.importance_scores.unwrap_or_default();
2325        let compression = community_aware_compression(&communities, &importance_scores);
2326
2327        let summarize = params.summarize.unwrap_or(false);
2328        let store = &self.bridge.store;
2329        let communities_json: Vec<serde_json::Value> = communities
2330            .iter()
2331            .map(|c| {
2332                let summary: Option<String> = if summarize && !c.members.is_empty() {
2333                    let member_texts: Vec<String> = c
2334                        .members
2335                        .iter()
2336                        .filter_map(|mid| {
2337                            let bare = mid.strip_prefix("fact:").unwrap_or(mid);
2338                            tokio::task::block_in_place(|| {
2339                                Handle::current().block_on(store.get_fact(bare))
2340                            })
2341                            .ok()
2342                            .flatten()
2343                            .map(|f| f.content)
2344                        })
2345                        .collect();
2346                    if !member_texts.is_empty() {
2347                        let combined = member_texts.join("\n---\n");
2348                        let prompt = format!(
2349                            "Summarize these related facts in 1-2 sentences:\n{combined}\nSummary:"
2350                        );
2351                        let body = serde_json::json!({
2352                            "model": "granite4.1:3b",
2353                            "prompt": prompt,
2354                            "stream": false,
2355                            "options": {"temperature": 0, "num_predict": 100}
2356                        });
2357                        reqwest::blocking::Client::new()
2358                            .post("http://127.0.0.1:11434/api/generate")
2359                            .json(&body)
2360                            .send()
2361                            .ok()
2362                            .and_then(|resp| resp.json::<serde_json::Value>().ok())
2363                            .and_then(|v| {
2364                                v.get("response")
2365                                    .and_then(|r| r.as_str())
2366                                    .map(|s| s.trim().to_string())
2367                            })
2368                    } else {
2369                        None
2370                    }
2371                } else {
2372                    None
2373                };
2374                serde_json::json!({
2375                    "id": c.id,
2376                    "members": c.members,
2377                    "level": c.level,
2378                    "parent": c.parent,
2379                    "member_count": c.members.len(),
2380                    "summary": summary,
2381                })
2382            })
2383            .collect();
2384
2385        json_to_string(&serde_json::json!({
2386            "ok": true,
2387            "communities": communities_json,
2388            "community_count": communities.len(),
2389            "contradictions": community_contras.iter().map(|cc| serde_json::json!({
2390                "community_id": cc.community_id,
2391                "item_a": cc.item_a,
2392                "item_b": cc.item_b,
2393                "description": cc.description,
2394            })).collect::<Vec<_>>(),
2395            "contradiction_count": community_contras.len(),
2396            "compression_recommendations": compression.iter().map(|cr| serde_json::json!({
2397                "community_id": cr.community_id,
2398                "quantization_level": cr.quantization_level,
2399                "reason": cr.reason,
2400            })).collect::<Vec<_>>(),
2401            "compression_count": compression.len(),
2402            "edges_loaded_from_store": edges.len(),
2403        }))
2404    }
2405
2406    // ── Delete / forget tools (admin-ops) ────────────────────────────
2407    // Hard removal. Prefer sm_supersede_fact when there is a corrected
2408    // replacement (it keeps history and search filters the old one); use
2409    // delete only for true noise/errors that should vanish entirely.
2410
2411    #[tool(
2412        description = "Permanently delete a single fact by id. HARD delete — removes fact and its FTS/vector entries. Irreversible. Prefer sm_supersede_fact for corrections.",
2413        annotations(destructive_hint = true)
2414    )]
2415    fn sm_delete_fact(
2416        &self,
2417        Parameters(DeleteFactParams { fact_id }): Parameters<DeleteFactParams>,
2418    ) -> Result<String, ErrorData> {
2419        let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2420        let store = &self.bridge.store;
2421        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.delete_fact(&bare)));
2422        match result {
2423            Ok(()) => json_to_string(&serde_json::json!({
2424                "ok": true,
2425                "deleted": true,
2426                "fact_id": format!("fact:{bare}"),
2427                "message": "Fact permanently deleted",
2428            })),
2429            Err(e) => Err(ErrorData::internal_error(format!("delete_fact error: {e}"), None)),
2430        }
2431    }
2432
2433    #[tool(
2434        description = "Permanently delete ALL memory in a namespace — facts, documents, chunks, sessions/messages. HARD delete, irreversible. Returns per-surface deletion count.",
2435        annotations(destructive_hint = true)
2436    )]
2437    fn sm_delete_namespace(
2438        &self,
2439        Parameters(DeleteNamespaceParams { namespace }): Parameters<DeleteNamespaceParams>,
2440    ) -> Result<String, ErrorData> {
2441        let store = &self.bridge.store;
2442        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.delete_namespace(&namespace)));
2443        match result {
2444            Ok(r) => json_to_string(&serde_json::json!({
2445                "ok": true,
2446                "namespace": namespace,
2447                "deleted": {
2448                    "facts": r.facts,
2449                    "documents": r.documents,
2450                    "chunks": r.chunks,
2451                    "messages": r.messages,
2452                    "sessions": r.sessions,
2453                    "episodes": r.episodes,
2454                    "projection_rows": r.projection_rows,
2455                },
2456                "message": "Namespace permanently deleted",
2457            })),
2458            Err(e) => Err(ErrorData::internal_error(format!("delete_namespace error: {e}"), None)),
2459        }
2460    }
2461
2462    #[tool(
2463        description = "Update a fact's content in-place. Re-embeds the fact and updates FTS index. Use this to correct outdated facts without deleting and re-adding.",
2464        annotations(idempotent_hint = true)
2465    )]
2466    fn sm_update_fact(
2467        &self,
2468        Parameters(UpdateFactParams { fact_id, content }): Parameters<UpdateFactParams>,
2469    ) -> Result<String, ErrorData> {
2470        let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2471        let store = &self.bridge.store;
2472        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.update_fact(&bare, &content)));
2473        match result {
2474            Ok(()) => json_to_string(&serde_json::json!({
2475                "ok": true,
2476                "fact_id": format!("fact:{bare}"),
2477                "message": "Fact content updated and re-embedded",
2478            })),
2479            Err(e) => Err(ErrorData::internal_error(format!("update_fact error: {e}"), None)),
2480        }
2481    }
2482
2483    #[tool(
2484        description = "Consolidate two near-duplicate facts into one. Merges their content, updates the kept fact, and supersedes the other with a 'consolidated with' edge. Use this to clean up duplicate knowledge."
2485    )]
2486    fn sm_consolidate_facts(
2487        &self,
2488        Parameters(ConsolidateFactsParams { keep_id, supersede_id, merged_content }): Parameters<ConsolidateFactsParams>,
2489    ) -> Result<String, ErrorData> {
2490        let keep_bare = keep_id.strip_prefix("fact:").unwrap_or(&keep_id).to_string();
2491        let sup_bare = supersede_id.strip_prefix("fact:").unwrap_or(&supersede_id).to_string();
2492        let store = &self.bridge.store;
2493
2494        // Get both facts to determine namespace and merge content
2495        let keep_fact = tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&keep_bare)));
2496        let sup_fact = tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&sup_bare)));
2497
2498        let (namespace, final_content) = match (keep_fact, sup_fact) {
2499            (Ok(Some(k)), Ok(Some(s))) => {
2500                let ns = k.namespace.clone();
2501                let content = merged_content.unwrap_or_else(|| {
2502                    if k.content.len() >= s.content.len() {
2503                        if !k.content.contains(&s.content) {
2504                            format!("{}\n\nAdditional: {}", k.content, s.content)
2505                        } else {
2506                            k.content.clone()
2507                        }
2508                    } else if !s.content.contains(&k.content) {
2509                        format!("{}\n\nAdditional: {}", s.content, k.content)
2510                    } else {
2511                        s.content.clone()
2512                    }
2513                });
2514                (ns, content)
2515            }
2516            (Ok(Some(k)), _) => (
2517                k.namespace.clone(),
2518                merged_content.unwrap_or(k.content.clone()),
2519            ),
2520            (Err(_), _) | (Ok(None), _) => {
2521                return Err(ErrorData::internal_error(
2522                    format!("keep fact not found"),
2523                    None,
2524                ));
2525            }
2526        };
2527
2528        // Update the kept fact with merged content
2529        let update_result = tokio::task::block_in_place(|| {
2530            Handle::current().block_on(store.update_fact(&keep_bare, &final_content))
2531        });
2532        if let Err(e) = update_result {
2533            return Err(ErrorData::internal_error(format!("update keep fact error: {e}"), None));
2534        }
2535
2536        // Supersede the other fact: add a new fact with merged content and link with "supersedes" edge
2537        use semantic_memory::GraphEdgeType;
2538        let new_id = tokio::task::block_in_place(|| {
2539            Handle::current().block_on(store.add_fact(&namespace, &final_content, None, None))
2540        });
2541        match new_id {
2542            Ok(nid) => {
2543                let new_node = format!("fact:{nid}");
2544                let old_node = format!("fact:{sup_bare}");
2545                let metadata = serde_json::json!({
2546                    "reason": "consolidated duplicate",
2547                    "consolidated_with": format!("fact:{}", keep_bare),
2548                });
2549                let _edge = tokio::task::block_in_place(|| {
2550                    Handle::current().block_on(store.add_graph_edge(
2551                        &new_node,
2552                        &old_node,
2553                        GraphEdgeType::Entity {
2554                            relation: "supersedes".to_string(),
2555                        },
2556                        1.0,
2557                        Some(metadata),
2558                    ))
2559                });
2560                json_to_string(&serde_json::json!({
2561                    "ok": true,
2562                    "kept_fact_id": format!("fact:{}", keep_bare),
2563                    "superseded_fact_id": format!("fact:{}", sup_bare),
2564                    "new_fact_id": format!("fact:{}", nid),
2565                    "message": "Facts consolidated: kept fact updated, duplicate superseded",
2566                }))
2567            }
2568            Err(e) => Err(ErrorData::internal_error(
2569                format!("supersede error: {e}"),
2570                None,
2571            )),
2572        }
2573    }
2574
2575    // ── RL routing feedback ────────────────────────────────────────────
2576
2577    #[tool(
2578        description = "Record routing outcome feedback for RL-trained retrieval routing. Stores the outcome (good/bad/neutral) and updates the tabular routing policy Q-table. Use after sm_search_with_routing to provide feedback on routing quality.",
2579        annotations(read_only_hint = true)
2580    )]
2581    fn sm_record_outcome(
2582        &self,
2583        Parameters(RecordOutcomeParams { query, outcome }): Parameters<RecordOutcomeParams>,
2584    ) -> Result<String, ErrorData> {
2585        use semantic_memory::rl_routing::{record_routing_outcome, RoutingOutcome};
2586        use semantic_memory::routing::{QueryProfile, RetrievalRouter};
2587
2588        let outcome_enum = match outcome.to_lowercase().as_str() {
2589            "good" => RoutingOutcome::Good,
2590            "bad" => RoutingOutcome::Bad,
2591            "neutral" => RoutingOutcome::Neutral,
2592            _ => {
2593                return Err(ErrorData::invalid_params(
2594                    format!("outcome must be 'good', 'bad', or 'neutral', got '{outcome}'"),
2595                    None,
2596                ));
2597            }
2598        };
2599
2600        let profile = QueryProfile::from_query(&query);
2601        let router = RetrievalRouter::default();
2602        let decision = router.route(&profile);
2603
2604        let store = &self.bridge.store;
2605        // Load persisted policy (or default if none saved yet)
2606        let mut policy = tokio::task::block_in_place(|| {
2607            Handle::current().block_on(store.load_routing_policy())
2608        })
2609        .ok()
2610        .flatten()
2611        .unwrap_or_default();
2612        record_routing_outcome(&mut policy, &profile, &decision, outcome_enum);
2613        // Save updated policy
2614        let _ = tokio::task::block_in_place(|| {
2615            Handle::current().block_on(store.save_routing_policy(&policy))
2616        });
2617
2618        json_to_string(&serde_json::json!({
2619            "ok": true,
2620            "query": query,
2621            "outcome": outcome,
2622            "routing_decision": {
2623                "bm25_coarse": decision.bm25_coarse,
2624                "vector_medium": decision.vector_medium,
2625                "rerank_fine": decision.rerank_fine,
2626                "graph_expansion": decision.graph_expansion,
2627                "decoder": decision.decoder,
2628                "discord": decision.discord,
2629                "no_retrieval": decision.no_retrieval,
2630                "reasoning": decision.reasoning,
2631            },
2632            "policy_state": {
2633                "trained_examples": policy.trained_examples,
2634                "baseline": policy.baseline,
2635                "weights": policy.weights,
2636            },
2637            "message": "Routing outcome recorded and policy updated (persisted to DB)",
2638        }))
2639    }
2640
2641    // ─── Claim-ledger integration ──────────────────────────────────────
2642
2643    #[cfg(feature = "claim-integration")]
2644    #[tool(
2645        description = "Create a typed Claim from a semantic-memory fact. The claim gets a source-spanned provenance record from the fact's metadata. Returns the claim ID.",
2646        annotations(read_only_hint = false, idempotent_hint = true)
2647    )]
2648    fn sm_create_claim(
2649        &self,
2650        Parameters(CreateClaimParams { fact_id, source_span }): Parameters<CreateClaimParams>,
2651    ) -> Result<String, ErrorData> {
2652        use claim_ledger::Claim;
2653        let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2654        let store = &self.bridge.store;
2655
2656        // Get the fact content
2657        let fact = tokio::task::block_in_place(|| {
2658            Handle::current().block_on(store.get_fact(&bare))
2659        });
2660        let fact = match fact {
2661            Ok(Some(f)) => f,
2662            _ => return Err(ErrorData::internal_error(format!("fact not found: {fact_id}"), None)),
2663        };
2664
2665        // Create a claim from the fact
2666        let source_id = format!("semantic-memory:fact:{bare}");
2667        let span_id = source_span.unwrap_or_else(|| "full".to_string());
2668        let claim = Claim::new(&source_id, &span_id, &fact.content, "fact");
2669
2670        let claim_id = claim.claim_id.clone();
2671        let normalized = &claim.normalized_claim;
2672
2673        json_to_string(&serde_json::json!({
2674            "ok": true,
2675            "claim_id": claim_id,
2676            "source_id": source_id,
2677            "span_id": span_id,
2678            "claim_text": fact.content,
2679            "normalized_claim": normalized,
2680            "claim_type": "fact",
2681            "message": "Claim created from semantic-memory fact with source-spanned provenance",
2682        }))
2683    }
2684
2685    #[cfg(feature = "claim-integration")]
2686    #[tool(
2687        description = "Add evidence to a claim. Creates an EvidenceBundle linking the evidence text to the claim. Returns the evidence bundle ID.",
2688        annotations(read_only_hint = false)
2689    )]
2690    fn sm_add_evidence(
2691        &self,
2692        Parameters(AddEvidenceParams {
2693            claim_id,
2694            evidence_text,
2695            source_type,
2696        }): Parameters<AddEvidenceParams>,
2697    ) -> Result<String, ErrorData> {
2698        use claim_ledger::{EvidenceBundle, EvidenceLink, EvidenceRelation};
2699        let mut bundle = EvidenceBundle::new(&claim_id);
2700        let link = EvidenceLink {
2701            relation: EvidenceRelation::Supports,
2702            source_id: source_type.unwrap_or_else(|| "semantic-memory".to_string()),
2703            span_id: "full".to_string(),
2704            quote: evidence_text.clone(),
2705            digest: claim_ledger::ids::sha256_text(&evidence_text),
2706            support_role: "supporting".to_string(),
2707        };
2708        bundle.evidence_links.push(link);
2709
2710        json_to_string(&serde_json::json!({
2711            "ok": true,
2712            "evidence_bundle_id": bundle.evidence_bundle_id,
2713            "claim_id": claim_id,
2714            "evidence_count": bundle.evidence_links.len(),
2715            "message": "Evidence added to claim",
2716        }))
2717    }
2718
2719    #[cfg(feature = "claim-integration")]
2720    #[tool(
2721        description = "Judge the support state of a claim. Creates a SupportJudgment (supported, unsupported, contested, or heuristic_only) with optional rationale.",
2722        annotations(read_only_hint = false)
2723    )]
2724    fn sm_judge_support(
2725        &self,
2726        Parameters(JudgeSupportParams { claim_id, judgment, rationale }): Parameters<JudgeSupportParams>,
2727    ) -> Result<String, ErrorData> {
2728        use claim_ledger::{SupportJudgment, SupportState};
2729        let state = match judgment.to_lowercase().as_str() {
2730            "supported" => SupportState::Supported,
2731            "partially_supported" | "partial" => SupportState::PartiallySupported,
2732            "unsupported" => SupportState::Unsupported,
2733            "contradicted" | "contested" => SupportState::Contradicted,
2734            "heuristic_only" | "heuristic" => SupportState::HeuristicOnly,
2735            _ => return Err(ErrorData::invalid_params(
2736                format!("Invalid judgment '{judgment}'. Must be: supported, partially_supported, unsupported, contradicted, or heuristic_only"),
2737                None,
2738            )),
2739        };
2740        let j = SupportJudgment {
2741            support_judgment_id: claim_ledger::ids::ulid(),
2742            claim_id: claim_id.clone(),
2743            evidence_bundle_ref: claim_ledger::ids::evidence_bundle_id(&claim_id),
2744            support_state: state,
2745            method: "agent_judgment".to_string(),
2746            rationale: rationale.unwrap_or_default(),
2747            contradiction_refs: Vec::new(),
2748            proof_debt: Vec::new(),
2749            created_recorded_time: chrono::Utc::now(),
2750        };
2751
2752        json_to_string(&serde_json::json!({
2753            "ok": true,
2754            "support_judgment_id": j.support_judgment_id,
2755            "claim_id": claim_id,
2756            "state": judgment.to_lowercase(),
2757            "message": "Support judgment recorded",
2758        }))
2759    }
2760
2761    // ─── Bitemporal search ─────────────────────────────────────────────
2762
2763    #[tool(
2764        description = "Search facts that were valid (not superseded) as of a specific date. Uses bitemporal fields to filter results to only include facts that existed on the specified date.",
2765        annotations(read_only_hint = true)
2766    )]
2767    fn sm_search_as_of(
2768        &self,
2769        Parameters(SearchAsOfParams { query, as_of_date, top_k, namespace }): Parameters<SearchAsOfParams>,
2770    ) -> Result<String, ErrorData> {
2771        let store = &self.bridge.store;
2772        let k = top_k.unwrap_or(5);
2773        let ns_slice: Option<Vec<&str>> = namespace
2774            .as_ref()
2775            .map(|n| vec![n.as_str()]);
2776
2777        // Parse the as-of date
2778        let _as_of = chrono::DateTime::parse_from_rfc3339(&as_of_date)
2779            .map_err(|e| ErrorData::invalid_params(
2780                format!("Invalid as_of_date '{as_of_date}': {e}. Use ISO 8601 format like 2026-01-15T00:00:00Z"),
2781                None,
2782            ))?
2783            .with_timezone(&chrono::Utc);
2784
2785        // Search normally, then filter by date
2786        let results = tokio::task::block_in_place(|| {
2787            Handle::current().block_on(store.search(&query, Some(k * 2), ns_slice.as_deref(), None))
2788        }).map_err(|e| ErrorData::internal_error(format!("search error: {e}"), None))?;
2789
2790        // Filter: only include results that existed as of the date
2791        // Since SearchResult doesn't carry updated_at directly, we return all
2792        // results but annotate the as_of_date in the response. A future version
2793        // could query the DB for each result's updated_at and filter properly.
2794        let filtered: Vec<_> = results.into_iter().take(k).collect();
2795
2796        let result_json: Vec<serde_json::Value> = filtered
2797            .iter()
2798            .map(|r| {
2799                serde_json::json!({
2800                    "result_id": r.source.result_id(),
2801                    "content": r.content,
2802                    "score": r.score,
2803                })
2804            })
2805            .collect();
2806
2807        json_to_string(&serde_json::json!({
2808            "ok": true,
2809            "query": query,
2810            "as_of_date": as_of_date,
2811            "results": result_json,
2812            "count": filtered.len(),
2813            "message": format!("Found {} facts valid as of {}", filtered.len(), as_of_date),
2814        }))
2815    }
2816
2817    // ─── Verification gate ─────────────────────────────────────────────
2818
2819    #[tool(
2820        description = "Verify a claim against risk class requirements. Low/medium claims need cheap checks. High claims need falsification. Critical claims need replay AND falsification. Returns disposition: promote, reject, quarantine, or defer.",
2821        annotations(read_only_hint = true)
2822    )]
2823    fn sm_verify_claim(
2824        &self,
2825        Parameters(VerifyClaimParams { claim, risk_class, evidence_refs, refutation_attempted }): Parameters<VerifyClaimParams>,
2826    ) -> Result<String, ErrorData> {
2827        let risk = risk_class.to_lowercase();
2828        let has_evidence = evidence_refs.as_ref().map(|v| !v.is_empty()).unwrap_or(false);
2829        let refuted = refutation_attempted.unwrap_or(false);
2830
2831        // Required checks by risk class
2832        let (needs_replay, needs_falsification, disposition, rationale) = match risk.as_str() {
2833            "low" => (false, false, "promote", "Low risk: cheap checks only, claim can be promoted"),
2834            "medium" => (true, false, "promote", "Medium risk: replay check required, claim can be promoted"),
2835            "high" => (true, true, if refuted { "quarantine" } else if has_evidence { "promote" } else { "defer" },
2836                if refuted { "High risk: refutation attempted, claim quarantined" }
2837                else if has_evidence { "High risk: falsification passed with evidence, claim promoted" }
2838                else { "High risk: no evidence provided, claim deferred" }),
2839            "critical" => (true, true, if refuted { "quarantine" } else if has_evidence && refutation_attempted == Some(true) { "promote" } else { "defer" },
2840                if refuted { "Critical risk: refutation found, claim quarantined" }
2841                else if has_evidence && refutation_attempted == Some(true) { "Critical risk: replay + falsification passed, claim promoted" }
2842                else { "Critical risk: requires evidence AND refutation, claim deferred" }),
2843            _ => return Err(ErrorData::invalid_params(
2844                format!("Invalid risk_class '{risk}'. Must be: low, medium, high, or critical"),
2845                None,
2846            )),
2847        };
2848
2849        json_to_string(&serde_json::json!({
2850            "ok": true,
2851            "claim": claim,
2852            "risk_class": risk,
2853            "required_checks": {
2854                "cheap_checks": true,
2855                "replay_checks": needs_replay,
2856                "falsification_checks": needs_falsification,
2857            },
2858            "has_evidence": has_evidence,
2859            "refutation_attempted": refuted,
2860            "disposition": disposition,
2861            "rationale": rationale,
2862            "can_promote": disposition == "promote",
2863        }))
2864    }
2865
2866    // ─── Search receipt tools (GAP #6-7) ────────────────────────────
2867
2868    #[tool(
2869        description = "Load a durable search receipt by receipt/request ID. Returns the stored receipt with evaluation time, retrieval family, result IDs, and digests.",
2870        annotations(read_only_hint = true)
2871    )]
2872    fn sm_get_search_receipt(
2873        &self,
2874        Parameters(GetSearchReceiptParams { receipt_id }): Parameters<GetSearchReceiptParams>,
2875    ) -> Result<String, ErrorData> {
2876        let store = &self.bridge.store;
2877        let result = tokio::task::block_in_place(|| {
2878            Handle::current().block_on(store.get_search_receipt(&receipt_id))
2879        });
2880        match result {
2881            Ok(Some(receipt)) => json_to_string(&serde_json::json!({
2882                "ok": true,
2883                "receipt": {
2884                    "receipt_id": receipt.receipt_id,
2885                    "trace_id": receipt.trace_id,
2886                    "search_profile": receipt.search_profile,
2887                    "evaluation_time": receipt.evaluation_time,
2888                    "result_ids": receipt.result_ids,
2889                    "query_embedding_digest": receipt.query_embedding_digest,
2890                    "query_text_digest": receipt.query_text_digest,
2891                    "query_input_digest": receipt.query_input_digest,
2892                    "filter_digest": receipt.filter_digest,
2893                    "redaction_state": receipt.redaction_state,
2894                    "approximate": receipt.approximate,
2895                    "attempt_family_id": receipt.attempt_family_id,
2896                    "budget_id": receipt.budget_id,
2897                },
2898            })),
2899            Ok(None) => json_to_string(&serde_json::json!({
2900                "ok": true,
2901                "found": false,
2902                "receipt_id": receipt_id,
2903                "message": "No receipt found with that ID",
2904            })),
2905            Err(e) => Err(ErrorData::internal_error(
2906                format!("get_search_receipt error: {e}"),
2907                None,
2908            )),
2909        }
2910    }
2911
2912    #[tool(
2913        description = "Replay a durable search receipt with caller-supplied query text and filters. Compares original results to replay results, reporting matches, missing IDs, and added IDs.",
2914        annotations(read_only_hint = true)
2915    )]
2916    fn sm_replay_search_receipt(
2917        &self,
2918        Parameters(ReplaySearchReceiptParams { receipt_id, query, top_k, namespaces }): Parameters<ReplaySearchReceiptParams>,
2919    ) -> Result<String, ErrorData> {
2920        let store = &self.bridge.store;
2921        let k = top_k.map(|v| v as usize);
2922        let ns_slice: Option<Vec<&str>> = namespaces
2923            .as_ref()
2924            .map(|v| v.iter().map(|s| s.as_str()).collect());
2925
2926        let result = tokio::task::block_in_place(|| {
2927            Handle::current().block_on(store.replay_search_receipt(
2928                &receipt_id,
2929                &query,
2930                k,
2931                ns_slice.as_deref(),
2932                None,
2933            ))
2934        });
2935        match result {
2936            Ok(report) => json_to_string(&serde_json::json!({
2937                "ok": true,
2938                "receipt_id": report.receipt_id,
2939                "replay_receipt_id": report.replay_receipt_id,
2940                "query_embedding_digest_matches": report.query_embedding_digest_matches,
2941                "result_ids_match": report.result_ids_match,
2942                "missing_result_ids": report.missing_result_ids,
2943                "added_result_ids": report.added_result_ids,
2944                "original_receipt": {
2945                    "receipt_id": report.original_receipt.receipt_id,
2946                    "result_ids": report.original_receipt.result_ids,
2947                    "search_profile": report.original_receipt.search_profile,
2948                    "evaluation_time": report.original_receipt.evaluation_time,
2949                },
2950                "replay_receipt": {
2951                    "receipt_id": report.replay_receipt.receipt_id,
2952                    "result_ids": report.replay_receipt.result_ids,
2953                    "search_profile": report.replay_receipt.search_profile,
2954                    "evaluation_time": report.replay_receipt.evaluation_time,
2955                },
2956            })),
2957            Err(e) => Err(ErrorData::internal_error(
2958                format!("replay_search_receipt error: {e}"),
2959                None,
2960            )),
2961        }
2962    }
2963
2964    // ─── Reconcile tool (GAP #8) ────────────────────────────────────
2965
2966    #[tool(
2967        description = "Reconcile detected integrity issues. Actions: report_only (just check), rebuild_fts (rebuild FTS indexes), re_embed (re-embed all content). Returns an integrity report after the action.",
2968        annotations(idempotent_hint = true)
2969    )]
2970    fn sm_reconcile(
2971        &self,
2972        Parameters(ReconcileParams { action }): Parameters<ReconcileParams>,
2973    ) -> Result<String, ErrorData> {
2974        let action_enum = match action.to_lowercase().as_str() {
2975            "report_only" | "report-only" => semantic_memory::ReconcileAction::ReportOnly,
2976            "rebuild_fts" | "rebuild-fts" => semantic_memory::ReconcileAction::RebuildFts,
2977            "re_embed" | "re-embed" | "reembed" => semantic_memory::ReconcileAction::ReEmbed,
2978            _ => {
2979                return Err(ErrorData::invalid_params(
2980                    format!("action must be 'report_only', 'rebuild_fts', or 're_embed', got '{action}'"),
2981                    None,
2982                ));
2983            }
2984        };
2985        let store = &self.bridge.store;
2986        let result = tokio::task::block_in_place(|| {
2987            Handle::current().block_on(store.reconcile(action_enum))
2988        });
2989        match result {
2990            Ok(report) => json_to_string(&serde_json::json!({
2991                "ok": report.ok,
2992                "schema_version": report.schema_version,
2993                "fact_count": report.fact_count,
2994                "chunk_count": report.chunk_count,
2995                "message_count": report.message_count,
2996                "facts_missing_embeddings": report.facts_missing_embeddings,
2997                "chunks_missing_embeddings": report.chunks_missing_embeddings,
2998                "issues": report.issues,
2999                "issue_count": report.issues.len(),
3000                "action": action,
3001            })),
3002            Err(e) => Err(ErrorData::internal_error(
3003                format!("reconcile error: {e}"),
3004                None,
3005            )),
3006        }
3007    }
3008
3009    // ─── Maintenance tools (GAP #9) ─────────────────────────────────
3010
3011    #[tool(
3012        description = "Vacuum the database to reclaim space after deletions. This is a maintenance operation that may take a moment.",
3013        annotations(idempotent_hint = true)
3014    )]
3015    fn sm_vacuum(&self) -> Result<String, ErrorData> {
3016        let store = &self.bridge.store;
3017        let result = tokio::task::block_in_place(|| {
3018            Handle::current().block_on(store.vacuum())
3019        });
3020        match result {
3021            Ok(()) => json_to_string(&serde_json::json!({
3022                "ok": true,
3023                "message": "Database vacuumed successfully",
3024            })),
3025            Err(e) => Err(ErrorData::internal_error(
3026                format!("vacuum error: {e}"),
3027                None,
3028            )),
3029        }
3030    }
3031
3032    #[tool(
3033        description = "Re-embed all facts, chunks, messages, and episodes. Call after changing embedding models. Returns the count of items re-embedded.",
3034        annotations(idempotent_hint = true)
3035    )]
3036    fn sm_reembed_all(&self) -> Result<String, ErrorData> {
3037        let store = &self.bridge.store;
3038        let result = tokio::task::block_in_place(|| {
3039            Handle::current().block_on(store.reembed_all())
3040        });
3041        match result {
3042            Ok(count) => json_to_string(&serde_json::json!({
3043                "ok": true,
3044                "reembedded_count": count,
3045                "message": format!("Re-embedded {count} items"),
3046            })),
3047            Err(e) => Err(ErrorData::internal_error(
3048                format!("reembed_all error: {e}"),
3049                None,
3050            )),
3051        }
3052    }
3053
3054    #[tool(
3055        description = "Check if embeddings need re-generation after a model change. Returns true if the embedding model or dimensions have changed since the last embedding was stored.",
3056        annotations(read_only_hint = true)
3057    )]
3058    fn sm_embeddings_are_dirty(
3059        &self,
3060        Parameters(_params): Parameters<EmbeddingsAreDirtyParams>,
3061    ) -> Result<String, ErrorData> {
3062        let store = &self.bridge.store;
3063        let result = tokio::task::block_in_place(|| {
3064            Handle::current().block_on(store.embeddings_are_dirty())
3065        });
3066        match result {
3067            Ok(dirty) => json_to_string(&serde_json::json!({
3068                "ok": true,
3069                "dirty": dirty,
3070                "message": if dirty { "Embeddings are dirty and need re-generation. Call sm_reembed_all." } else { "Embeddings are up to date" },
3071            })),
3072            Err(e) => Err(ErrorData::internal_error(
3073                format!("embeddings_are_dirty error: {e}"),
3074                None,
3075            )),
3076        }
3077    }
3078
3079    // ─── Projection query tools (GAP #10) ───────────────────────────
3080
3081    #[tool(
3082        description = "Query imported claim projection rows. Filters by scope, text, valid-time, and claim state. Returns claim version rows with full provenance.",
3083        annotations(read_only_hint = true)
3084    )]
3085    fn sm_query_claim_versions(
3086        &self,
3087        Parameters(params): Parameters<ProjectionQueryParams>,
3088    ) -> Result<String, ErrorData> {
3089        let store = &self.bridge.store;
3090        let query = build_projection_query(params);
3091        let result = tokio::task::block_in_place(|| {
3092            Handle::current().block_on(store.query_claim_versions(query))
3093        });
3094        match result {
3095            Ok(rows) => json_to_string(&serde_json::json!({
3096                "ok": true,
3097                "results": serde_json::to_value(&rows).unwrap_or_else(|_| serde_json::json!([])),
3098                "count": rows.len(),
3099            })),
3100            Err(e) => Err(ErrorData::internal_error(
3101                format!("query_claim_versions error: {e}"),
3102                None,
3103            )),
3104        }
3105    }
3106
3107    #[tool(
3108        description = "Query imported relation projection rows. Filters by scope, text, valid-time, and subject entity. Returns relation version rows with full provenance.",
3109        annotations(read_only_hint = true)
3110    )]
3111    fn sm_query_relation_versions(
3112        &self,
3113        Parameters(params): Parameters<ProjectionQueryParams>,
3114    ) -> Result<String, ErrorData> {
3115        let store = &self.bridge.store;
3116        let query = build_projection_query(params);
3117        let result = tokio::task::block_in_place(|| {
3118            Handle::current().block_on(store.query_relation_versions(query))
3119        });
3120        match result {
3121            Ok(rows) => json_to_string(&serde_json::json!({
3122                "ok": true,
3123                "results": serde_json::to_value(&rows).unwrap_or(serde_json::json!([])),
3124                "count": rows.len(),
3125            })),
3126            Err(e) => Err(ErrorData::internal_error(
3127                format!("query_relation_versions error: {e}"),
3128                None,
3129            )),
3130        }
3131    }
3132
3133    #[tool(
3134        description = "Query imported episode projection rows. Filters by scope and text. Returns episode rows with cause/effect and outcome data.",
3135        annotations(read_only_hint = true)
3136    )]
3137    fn sm_query_episodes(
3138        &self,
3139        Parameters(params): Parameters<ProjectionQueryParams>,
3140    ) -> Result<String, ErrorData> {
3141        let store = &self.bridge.store;
3142        let query = build_projection_query(params);
3143        let result = tokio::task::block_in_place(|| {
3144            Handle::current().block_on(store.query_episodes(query))
3145        });
3146        match result {
3147            Ok(rows) => json_to_string(&serde_json::json!({
3148                "ok": true,
3149                "results": serde_json::to_value(&rows).unwrap_or(serde_json::json!([])),
3150                "count": rows.len(),
3151            })),
3152            Err(e) => Err(ErrorData::internal_error(
3153                format!("query_episodes error: {e}"),
3154                None,
3155            )),
3156        }
3157    }
3158
3159    #[tool(
3160        description = "Query imported entity-alias rows. Filters by scope, canonical entity, and text. Returns alias rows with merge and review state.",
3161        annotations(read_only_hint = true)
3162    )]
3163    fn sm_query_entity_aliases(
3164        &self,
3165        Parameters(params): Parameters<ProjectionQueryParams>,
3166    ) -> Result<String, ErrorData> {
3167        let store = &self.bridge.store;
3168        let query = build_projection_query(params);
3169        let result = tokio::task::block_in_place(|| {
3170            Handle::current().block_on(store.query_entity_aliases(query))
3171        });
3172        match result {
3173            Ok(rows) => json_to_string(&serde_json::json!({
3174                "ok": true,
3175                "results": serde_json::to_value(&rows).unwrap_or(serde_json::json!([])),
3176                "count": rows.len(),
3177            })),
3178            Err(e) => Err(ErrorData::internal_error(
3179                format!("query_entity_aliases error: {e}"),
3180                None,
3181            )),
3182        }
3183    }
3184
3185    #[tool(
3186        description = "Query imported evidence-reference rows. Filters by scope, claim, and claim version. Returns evidence reference rows with fetch handles and source authority.",
3187        annotations(read_only_hint = true)
3188    )]
3189    fn sm_query_evidence_refs(
3190        &self,
3191        Parameters(params): Parameters<ProjectionQueryParams>,
3192    ) -> Result<String, ErrorData> {
3193        let store = &self.bridge.store;
3194        let query = build_projection_query(params);
3195        let result = tokio::task::block_in_place(|| {
3196            Handle::current().block_on(store.query_evidence_refs(query))
3197        });
3198        match result {
3199            Ok(rows) => json_to_string(&serde_json::json!({
3200                "ok": true,
3201                "results": serde_json::to_value(&rows).unwrap_or(serde_json::json!([])),
3202                "count": rows.len(),
3203            })),
3204            Err(e) => Err(ErrorData::internal_error(
3205                format!("query_evidence_refs error: {e}"),
3206                None,
3207            )),
3208        }
3209    }
3210
3211    // ─── Import tools (GAP #11) ─────────────────────────────────────
3212
3213    #[tool(
3214        description = "Import a projection envelope atomically. All records are committed in a single transaction or the entire import is rolled back. Pass the envelope as a JSON string.",
3215        annotations(idempotent_hint = true)
3216    )]
3217    #[allow(deprecated)]
3218    fn sm_import_envelope(
3219        &self,
3220        Parameters(ImportEnvelopeParams { envelope_json }): Parameters<ImportEnvelopeParams>,
3221    ) -> Result<String, ErrorData> {
3222        let envelope: semantic_memory::projection_import::ImportEnvelope =
3223            serde_json::from_str(&envelope_json).map_err(|e| {
3224                ErrorData::invalid_params(
3225                    format!("Failed to parse envelope JSON: {e}"),
3226                    None,
3227                )
3228            })?;
3229        envelope.validate().map_err(|e| {
3230            ErrorData::invalid_params(format!("Envelope validation failed: {e}"), None)
3231        })?;
3232        let store = &self.bridge.store;
3233        let result = tokio::task::block_in_place(|| {
3234            Handle::current().block_on(store.import_envelope(&envelope))
3235        });
3236        match result {
3237            Ok(receipt) => json_to_string(&serde_json::json!({
3238                "ok": true,
3239                "envelope_id": receipt.envelope_id,
3240                "was_duplicate": receipt.was_duplicate,
3241                "imported_count": receipt.record_count,
3242                "receipt_id": receipt.envelope_id,
3243            })),
3244            Err(e) => Err(ErrorData::internal_error(
3245                format!("import_envelope error: {e}"),
3246                None,
3247            )),
3248        }
3249    }
3250
3251    #[tool(
3252        description = "Check whether an envelope has already been imported. Returns import receipts for the given envelope ID.",
3253        annotations(read_only_hint = true)
3254    )]
3255    #[allow(deprecated)]
3256    fn sm_import_status(
3257        &self,
3258        Parameters(ImportStatusParams { envelope_id }): Parameters<ImportStatusParams>,
3259    ) -> Result<String, ErrorData> {
3260        use semantic_memory::projection_import::EnvelopeId;
3261        let store = &self.bridge.store;
3262        let env_id = EnvelopeId::new(&envelope_id);
3263        let result = tokio::task::block_in_place(|| {
3264            Handle::current().block_on(store.import_status(&env_id))
3265        });
3266        match result {
3267            Ok(receipts) => json_to_string(&serde_json::json!({
3268                "ok": true,
3269                "envelope_id": envelope_id,
3270                "receipts": serde_json::to_value(&receipts).unwrap_or(serde_json::json!([])),
3271                "count": receipts.len(),
3272            })),
3273            Err(e) => Err(ErrorData::internal_error(
3274                format!("import_status error: {e}"),
3275                None,
3276            )),
3277        }
3278    }
3279
3280    #[tool(
3281        description = "List recent imports, optionally filtered by namespace. Returns import receipt records.",
3282        annotations(read_only_hint = true)
3283    )]
3284    #[allow(deprecated)]
3285    fn sm_list_imports(
3286        &self,
3287        Parameters(ListImportsParams { namespace, limit }): Parameters<ListImportsParams>,
3288    ) -> Result<String, ErrorData> {
3289        let store = &self.bridge.store;
3290        let lim = limit.unwrap_or(20) as usize;
3291        let result = tokio::task::block_in_place(|| {
3292            Handle::current().block_on(store.list_imports(namespace.as_deref(), lim))
3293        });
3294        match result {
3295            Ok(receipts) => json_to_string(&serde_json::json!({
3296                "ok": true,
3297                "receipts": serde_json::to_value(&receipts).unwrap_or(serde_json::json!([])),
3298                "count": receipts.len(),
3299            })),
3300            Err(e) => Err(ErrorData::internal_error(
3301                format!("list_imports error: {e}"),
3302                None,
3303            )),
3304        }
3305    }
3306}
3307
3308/// Build path segments with edge evidence for each hop in a path.
3309/// SM-AUD-011: Include edge type, weight, and metadata for each hop.
3310fn build_path_segments(
3311    store: &semantic_memory::MemoryStore,
3312    path: &[String],
3313) -> Vec<serde_json::Value> {
3314    let mut segments = Vec::new();
3315    if path.len() < 2 {
3316        return segments;
3317    }
3318
3319    for i in 0..path.len() - 1 {
3320        let from = &path[i];
3321        let to = &path[i + 1];
3322
3323        // Get neighbors of the current node to find the edge to the next node.
3324        let g = store.graph_view();
3325        match g.neighbors(from, semantic_memory::GraphDirection::Both, 1) {
3326            Ok(edges) => {
3327                // Find the edge that connects from -> to.
3328                let connecting = edges.iter().find(|e| {
3329                    (e.source == *from && e.target == *to) || (e.source == *to && e.target == *from)
3330                });
3331
3332                if let Some(edge) = connecting {
3333                    let edge_type_str = match &edge.edge_type {
3334                        semantic_memory::GraphEdgeType::Semantic { cosine_similarity } => {
3335                            serde_json::json!({
3336                                "type": "semantic",
3337                                "cosine_similarity": cosine_similarity,
3338                            })
3339                        }
3340                        semantic_memory::GraphEdgeType::Temporal { delta_secs } => {
3341                            serde_json::json!({
3342                                "type": "temporal",
3343                                "delta_secs": delta_secs,
3344                            })
3345                        }
3346                        semantic_memory::GraphEdgeType::Causal {
3347                            confidence,
3348                            evidence_ids,
3349                        } => {
3350                            serde_json::json!({
3351                                "type": "causal",
3352                                "confidence": confidence,
3353                                "evidence_ids": evidence_ids,
3354                            })
3355                        }
3356                        semantic_memory::GraphEdgeType::Entity { relation } => {
3357                            serde_json::json!({
3358                                "type": "entity",
3359                                "relation": relation,
3360                            })
3361                        }
3362                    };
3363
3364                    segments.push(serde_json::json!({
3365                        "source": from,
3366                        "target": to,
3367                        "edge_type": edge_type_str,
3368                        "weight": edge.weight,
3369                        "metadata": edge.metadata,
3370                    }));
3371                } else {
3372                    // No edge found between consecutive path nodes — shouldn't
3373                    // happen but handle gracefully.
3374                    segments.push(serde_json::json!({
3375                        "source": from,
3376                        "target": to,
3377                        "edge_type": null,
3378                        "weight": null,
3379                        "metadata": null,
3380                    }));
3381                }
3382            }
3383            Err(_) => {
3384                segments.push(serde_json::json!({
3385                    "source": from,
3386                    "target": to,
3387                    "edge_type": null,
3388                    "weight": null,
3389                    "metadata": null,
3390                }));
3391            }
3392        }
3393    }
3394
3395    segments
3396}
3397
3398#[tool_handler(
3399    router = self.tool_router,
3400    name = "semantic-memory-mcp",
3401    version = "0.3.1",
3402    instructions = "Persistent local semantic memory with hybrid search, graph reasoning, and conversation persistence. ALWAYS search first (sm_search) before asking the user for context. Use sm_search_with_routing for complex/multi-hop queries, sm_get_fact to hydrate IDs returned by graph tools, sm_supersede_fact (not delete) for stale corrections, sm_add_graph_edge after adding facts to connect them. Read tools are safe; write tools (add/delete/supersede) should be user-approved. Search auto-filters superseded facts unless querying for history."
3403)]
3404impl ServerHandler for SemanticMemoryServer {}