1use 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
17use 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 let admin_tools = [
34 "sm_reconcile",
36 "sm_vacuum",
37 "sm_reembed_all",
38 "sm_embeddings_are_dirty",
39 "sm_get_search_receipt",
41 "sm_replay_search_receipt",
42 "sm_query_claim_versions",
44 "sm_query_relation_versions",
45 "sm_query_episodes",
46 "sm_query_entity_aliases",
47 "sm_query_evidence_refs",
48 "sm_import_envelope",
50 "sm_import_status",
51 "sm_list_imports",
52 ];
53
54 match tool_profile {
55 "full" => { }
56 "standard" => {
57 for t in &["sm_import_envelope", "sm_import_status", "sm_list_imports"] {
59 router.disable_route(*t);
60 }
61 }
62 _ => {
63 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
83fn 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
120fn 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
161fn 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
177fn 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
204fn 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
249fn 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
298fn 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
332fn 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 #[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 #[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 let sens = sensitivity.unwrap_or_else(|| "internal".to_string());
554 let kind = memory_kind.unwrap_or_else(|| "durable_fact".to_string());
555
556 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 #[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 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 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 ¶ms.source,
2096 ¶ms.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 #[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 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 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 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 #[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 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 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 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 #[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 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 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 #[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 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 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 #[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 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 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 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 #[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 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 #[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 #[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 #[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 #[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 #[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
3308fn 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 let g = store.graph_view();
3325 match g.neighbors(from, semantic_memory::GraphDirection::Both, 1) {
3326 Ok(edges) => {
3327 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 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 {}