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) -> Self {
30 Self {
31 bridge: Arc::new(bridge),
32 tool_router: Self::tool_router(),
33 }
34 }
35}
36
37fn load_stored_edge_refs(
40 store: &semantic_memory::MemoryStore,
41) -> Result<Vec<semantic_memory::discord::GraphEdgeRef>, ErrorData> {
42 let edges =
43 tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
44 .map_err(|e| {
45 ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
46 })?;
47 let refs = edges
48 .iter()
49 .map(|edge| {
50 let parsed_type = edge
51 .edge_type_parsed
52 .clone()
53 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
54 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
55 relation: "unknown".to_string(),
56 });
57 let type_str = match parsed_type {
58 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
59 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
60 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
61 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
62 };
63 semantic_memory::discord::GraphEdgeRef {
64 source: edge.source.clone(),
65 target: edge.target.clone(),
66 edge_type: type_str.to_string(),
67 weight: edge.weight,
68 }
69 })
70 .collect();
71 Ok(refs)
72}
73
74fn load_stored_factor_edges(
77 store: &semantic_memory::MemoryStore,
78) -> Result<
79 Vec<(
80 String,
81 String,
82 semantic_memory::GraphEdgeType,
83 f64,
84 Option<String>,
85 )>,
86 ErrorData,
87> {
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 raw = 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 (
104 edge.source.clone(),
105 edge.target.clone(),
106 parsed_type,
107 edge.weight,
108 edge.metadata.clone(),
109 )
110 })
111 .collect();
112 Ok(raw)
113}
114
115fn load_stored_edge_pairs(
117 store: &semantic_memory::MemoryStore,
118) -> Result<Vec<(String, String)>, ErrorData> {
119 let edges =
120 tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
121 .map_err(|e| {
122 ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
123 })?;
124 let pairs = edges
125 .iter()
126 .map(|edge| (edge.source.clone(), edge.target.clone()))
127 .collect();
128 Ok(pairs)
129}
130
131fn load_neighborhood_edge_pairs(
135 store: &semantic_memory::MemoryStore,
136 seed_ids: &[String],
137) -> Result<Vec<(String, String)>, ErrorData> {
138 if seed_ids.is_empty() {
139 return load_stored_edge_pairs(store);
140 }
141 let edges = tokio::task::block_in_place(|| {
142 Handle::current().block_on(store.list_graph_edges_for_neighborhood(
143 seed_ids.to_vec(),
144 2,
145 200,
146 ))
147 })
148 .map_err(|e| {
149 ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
150 })?;
151 let pairs = edges
152 .iter()
153 .map(|edge| (edge.source.clone(), edge.target.clone()))
154 .collect();
155 Ok(pairs)
156}
157
158fn load_neighborhood_edge_refs(
160 store: &semantic_memory::MemoryStore,
161 seed_ids: &[String],
162) -> Result<Vec<semantic_memory::discord::GraphEdgeRef>, ErrorData> {
163 if seed_ids.is_empty() {
164 return load_stored_edge_refs(store);
165 }
166 let edges = tokio::task::block_in_place(|| {
167 Handle::current().block_on(store.list_graph_edges_for_neighborhood(
168 seed_ids.to_vec(),
169 2,
170 200,
171 ))
172 })
173 .map_err(|e| {
174 ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
175 })?;
176 let refs = edges
177 .iter()
178 .map(|edge| {
179 let parsed_type = edge
180 .edge_type_parsed
181 .clone()
182 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
183 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
184 relation: "unknown".to_string(),
185 });
186 let type_str = match parsed_type {
187 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
188 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
189 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
190 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
191 };
192 semantic_memory::discord::GraphEdgeRef {
193 source: edge.source.clone(),
194 target: edge.target.clone(),
195 edge_type: type_str.to_string(),
196 weight: edge.weight,
197 }
198 })
199 .collect();
200 Ok(refs)
201}
202
203fn load_neighborhood_factor_edges(
205 store: &semantic_memory::MemoryStore,
206 seed_ids: &[String],
207) -> Result<
208 Vec<(
209 String,
210 String,
211 semantic_memory::GraphEdgeType,
212 f64,
213 Option<String>,
214 )>,
215 ErrorData,
216> {
217 if seed_ids.is_empty() {
218 return load_stored_factor_edges(store);
219 }
220 let edges = tokio::task::block_in_place(|| {
221 Handle::current().block_on(store.list_graph_edges_for_neighborhood(
222 seed_ids.to_vec(),
223 2,
224 200,
225 ))
226 })
227 .map_err(|e| {
228 ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
229 })?;
230 let raw = edges
231 .iter()
232 .map(|edge| {
233 let parsed_type = edge
234 .edge_type_parsed
235 .clone()
236 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
237 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
238 relation: "unknown".to_string(),
239 });
240 (
241 edge.source.clone(),
242 edge.target.clone(),
243 parsed_type,
244 edge.weight,
245 edge.metadata.clone(),
246 )
247 })
248 .collect();
249 Ok(raw)
250}
251
252fn load_superseded_targets(
254 store: &semantic_memory::MemoryStore,
255) -> Result<HashSet<String>, ErrorData> {
256 let edges =
257 tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
258 .map_err(|e| {
259 ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
260 })?;
261 let mut targets = HashSet::new();
262 for edge in edges {
263 let parsed_type = edge
264 .edge_type_parsed
265 .clone()
266 .or_else(|| serde_json::from_str(&edge.edge_type).ok());
267 if let Some(semantic_memory::GraphEdgeType::Entity { relation }) = parsed_type {
268 if relation == "supersedes" {
269 targets.insert(edge.target);
270 }
271 }
272 }
273 Ok(targets)
274}
275
276fn query_allows_superseded(query: &str) -> bool {
277 let q = query.to_lowercase();
278 q.contains("supersed")
279 || q.contains("stale")
280 || q.contains("obsolete")
281 || q.contains("histor")
282 || q.contains("old fact")
283 || q.contains("previous fact")
284}
285
286fn json_to_string(value: &serde_json::Value) -> Result<String, ErrorData> {
289 serde_json::to_string_pretty(value)
290 .map_err(|e| ErrorData::internal_error(format!("Serialization error: {e}"), None))
291}
292
293#[tool_router]
294impl SemanticMemoryServer {
295 #[tool(
298 description = "Semantic hybrid search (BM25 + vector + RRF). Returns ranked results with content, scores, and stable result IDs.",
299 annotations(read_only_hint = true)
300 )]
301 fn sm_search(
302 &self,
303 Parameters(SearchParams {
304 query,
305 top_k,
306 namespaces,
307 }): Parameters<SearchParams>,
308 ) -> Result<String, ErrorData> {
309 let requested_k = top_k.map(|v| v as usize).unwrap_or(5);
310 let allow_superseded = query_allows_superseded(&query);
311 let search_k = if allow_superseded {
312 requested_k
313 } else {
314 (requested_k * 4).max(20)
315 };
316 let ns: Option<Vec<&str>> = namespaces
317 .as_ref()
318 .map(|v| v.iter().map(|s| s.as_str()).collect());
319
320 let store = &self.bridge.store;
321 let result = tokio::task::block_in_place(|| {
322 Handle::current().block_on(store.search(&query, Some(search_k), ns.as_deref(), None))
323 });
324
325 match result {
326 Ok(results) => {
327 let superseded_targets = if allow_superseded {
328 HashSet::new()
329 } else {
330 load_superseded_targets(store)?
331 };
332 let fresh_results: Vec<_> = results
333 .iter()
334 .filter(|r| !superseded_targets.contains(&r.source.result_id()))
335 .collect();
336 let result_refs: Vec<_> =
337 if superseded_targets.is_empty() || fresh_results.is_empty() {
338 results.iter().collect()
339 } else {
340 fresh_results
341 };
342 let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
343 let json_results: Vec<serde_json::Value> = result_refs
344 .iter()
345 .take(requested_k)
346 .map(|r| {
347 serde_json::json!({
348 "result_id": r.source.result_id(),
349 "content": r.content,
350 "source": format!("{:?}", r.source),
351 "score": r.score,
352 "bm25_rank": r.bm25_rank,
353 "vector_rank": r.vector_rank,
354 "cosine_similarity": r.cosine_similarity,
355 })
356 })
357 .collect();
358 json_to_string(&serde_json::json!({
359 "ok": true,
360 "results": json_results,
361 "count": json_results.len(),
362 "superseded_filtered_count": superseded_filtered_count,
363 }))
364 }
365 Err(e) => Err(ErrorData::internal_error(
366 format!("Search error: {e}"),
367 None,
368 )),
369 }
370 }
371
372 #[allow(dead_code)]
377 fn sm_search_explained(
378 &self,
379 Parameters(SearchExplainedParams { query, top_k }): Parameters<SearchExplainedParams>,
380 ) -> Result<String, ErrorData> {
381 let requested_k = top_k.map(|v| v as usize).unwrap_or(5);
382 let allow_superseded = query_allows_superseded(&query);
383 let search_k = if allow_superseded {
384 requested_k
385 } else {
386 (requested_k * 4).max(20)
387 };
388 let store = &self.bridge.store;
389 let result = tokio::task::block_in_place(|| {
390 Handle::current().block_on(store.search_explained(&query, Some(search_k), None, None))
391 });
392
393 match result {
394 Ok(results) => {
395 let superseded_targets = if allow_superseded {
396 HashSet::new()
397 } else {
398 load_superseded_targets(store)?
399 };
400 let fresh_results: Vec<_> = results
401 .iter()
402 .filter(|r| !superseded_targets.contains(&r.result.source.result_id()))
403 .collect();
404 let result_refs: Vec<_> =
405 if superseded_targets.is_empty() || fresh_results.is_empty() {
406 results.iter().collect()
407 } else {
408 fresh_results
409 };
410 let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
411 let json_results: Vec<serde_json::Value> = result_refs
412 .iter()
413 .take(requested_k)
414 .map(|r| {
415 serde_json::json!({
416 "result_id": r.result.source.result_id(),
417 "content": r.result.content,
418 "source": format!("{:?}", r.result.source),
419 "score": r.result.score,
420 "bm25_rank": r.result.bm25_rank,
421 "vector_rank": r.result.vector_rank,
422 "cosine_similarity": r.result.cosine_similarity,
423 "breakdown": {
424 "rrf_score": r.breakdown.rrf_score,
425 "bm25_score": r.breakdown.bm25_score,
426 "vector_score": r.breakdown.vector_score,
427 "recency_score": r.breakdown.recency_score,
428 "bm25_rank": r.breakdown.bm25_rank,
429 "vector_rank": r.breakdown.vector_rank,
430 "vector_source_rank": r.breakdown.vector_source_rank,
431 "vector_source_score": r.breakdown.vector_source_score,
432 "bm25_contribution": r.breakdown.bm25_contribution,
433 "vector_contribution": r.breakdown.vector_contribution,
434 "vector_reranked_from_f32": r.breakdown.vector_reranked_from_f32,
435 "bm25_weight": r.breakdown.bm25_weight,
436 "vector_weight": r.breakdown.vector_weight,
437 "recency_weight": r.breakdown.recency_weight,
438 "rrf_k": r.breakdown.rrf_k,
439 },
440 })
441 })
442 .collect();
443 json_to_string(&serde_json::json!({
444 "ok": true,
445 "results": json_results,
446 "count": json_results.len(),
447 "superseded_filtered_count": superseded_filtered_count,
448 }))
449 }
450 Err(e) => Err(ErrorData::internal_error(
451 format!("Search error: {e}"),
452 None,
453 )),
454 }
455 }
456
457 #[tool(
458 description = "Add a fact to the knowledge base. Embedded and indexed for semantic search. Returns fact ID and content digest.",
459 annotations(idempotent_hint = true)
460 )]
461 fn sm_add_fact(
462 &self,
463 Parameters(AddFactParams {
464 content,
465 namespace,
466 source,
467 extract_entities,
468 memory_kind,
469 sensitivity,
470 evidence_refs,
471 }): Parameters<AddFactParams>,
472 ) -> Result<String, ErrorData> {
473 let store = &self.bridge.store;
474 let src = source.as_deref();
475
476 let sens = sensitivity.unwrap_or_else(|| "internal".to_string());
478 let kind = memory_kind.unwrap_or_else(|| "durable_fact".to_string());
479
480 if sens == "confidential" || sens == "restricted" {
482 return Err(ErrorData::invalid_params(
483 format!("Admission gate BLOCKED: sensitivity='{sens}' content cannot be stored without explicit user request"),
484 None,
485 ));
486 }
487
488 if kind == "ephemeral_inference" {
490 let refs = evidence_refs.as_ref().map(|v| v.len()).unwrap_or(0);
491 if refs == 0 {
492 return Err(ErrorData::invalid_params(
493 "Admission gate BLOCKED: ephemeral_inference requires evidence_refs to promote to durable".to_string(),
494 None,
495 ));
496 }
497 }
498
499 let mut meta = serde_json::Map::new();
501 meta.insert("memory_kind".to_string(), serde_json::json!(kind));
502 meta.insert("sensitivity".to_string(), serde_json::json!(sens));
503 if let Some(refs) = evidence_refs {
504 meta.insert("evidence_refs".to_string(), serde_json::json!(refs));
505 }
506 let _metadata_str = serde_json::to_string(&serde_json::Value::Object(meta)).ok();
507
508 let result = tokio::task::block_in_place(|| {
509 Handle::current().block_on(store.add_fact(&namespace, &content, src, None))
510 });
511
512 match result {
513 Ok(id) => {
514 if extract_entities == Some(true) {
516 let prompt = format!(
517 "Extract entities from this text as JSON. Format: {{\"entities\": [{{\"name\": \"...\", \"type\": \"person|project|concept|tool|version|path\"}}]}}\nText: {content}\nJSON:"
518 );
519 let body = serde_json::json!({
520 "model": "granite4.1:3b",
521 "prompt": prompt,
522 "stream": false,
523 "options": {"temperature": 0, "num_predict": 200}
524 });
525 if let Ok(resp) = reqwest::blocking::Client::new()
526 .post("http://127.0.0.1:11434/api/generate")
527 .json(&body)
528 .send()
529 {
530 if let Ok(v) = resp.json::<serde_json::Value>() {
531 if let Some(response_str) =
532 v.get("response").and_then(|r| r.as_str())
533 {
534 let parsed_result = boundary_compiler::parse_with_dup_check(response_str.trim());
536 if let Ok(parsed) = parsed_result {
537 if let Some(entities) =
538 parsed.get("entities").and_then(|e| e.as_array())
539 {
540 let fact_node = format!("fact:{id}");
541 for entity in entities {
542 if let Some(name) =
543 entity.get("name").and_then(|n| n.as_str())
544 {
545 let entity_node = format!("entity:{name}");
546 let _ = tokio::task::block_in_place(|| {
547 Handle::current().block_on(
548 store.add_graph_edge(
549 &fact_node,
550 &entity_node,
551 semantic_memory::GraphEdgeType::Entity {
552 relation: "mentions".to_string(),
553 },
554 1.0,
555 None,
556 ),
557 )
558 });
559 }
560 }
561 }
562 }
563 }
564 }
565 }
566 }
567
568 json_to_string(&serde_json::json!({
569 "ok": true,
570 "fact_id": id,
571 "namespace": namespace,
572 "message": "Fact added successfully",
573 }))
574 }
575 Err(e) => Err(ErrorData::internal_error(
576 format!("Error adding fact: {e}"),
577 None,
578 )),
579 }
580 }
581
582 #[tool(
583 description = "Ingest a document with automatic chunking. Splits into chunks, each embedded and indexed. Returns document ID and chunk count.",
584 annotations(idempotent_hint = true)
585 )]
586 fn sm_ingest_document(
587 &self,
588 Parameters(IngestDocumentParams {
589 content,
590 title,
591 namespace,
592 }): Parameters<IngestDocumentParams>,
593 ) -> Result<String, ErrorData> {
594 let store = &self.bridge.store;
595 let result = tokio::task::block_in_place(|| {
596 Handle::current()
597 .block_on(store.ingest_document(&title, &content, &namespace, None, None))
598 });
599
600 match result {
601 Ok(doc_id) => {
602 let chunk_count = tokio::task::block_in_place(|| {
603 Handle::current().block_on(store.count_chunks_for_document(&doc_id))
604 })
605 .unwrap_or(0);
606 json_to_string(&serde_json::json!({
607 "ok": true,
608 "document_id": doc_id,
609 "title": title,
610 "chunk_count": chunk_count,
611 "message": "Document ingested successfully",
612 }))
613 }
614 Err(e) => Err(ErrorData::internal_error(
615 format!("Error ingesting document: {e}"),
616 None,
617 )),
618 }
619 }
620
621 #[tool(
622 description = "Get knowledge base statistics: fact/chunk/document/session counts, DB size, embedding model, and graph edge count.",
623 annotations(read_only_hint = true)
624 )]
625 fn sm_stats(&self) -> Result<String, ErrorData> {
626 let store = &self.bridge.store;
627 let result = tokio::task::block_in_place(|| Handle::current().block_on(store.stats()));
628
629 match result {
630 Ok(stats) => {
631 let graph_edge_count = tokio::task::block_in_place(|| {
634 Handle::current().block_on(store.list_all_graph_edges())
635 })
636 .map(|edges| edges.len())
637 .unwrap_or_else(|e| {
638 tracing::warn!("graph_edges table unavailable: {e}");
639 0
640 });
641 json_to_string(&serde_json::json!({
642 "ok": true,
643 "facts": stats.total_facts,
644 "chunks": stats.total_chunks,
645 "documents": stats.total_documents,
646 "sessions": stats.total_sessions,
647 "messages": stats.total_messages,
648 "graph_edges": graph_edge_count,
649 "db_size_bytes": stats.database_size_bytes,
650 "db_size_mb": (stats.database_size_bytes as f64 / 1_048_576.0 * 100.0).round() / 100.0,
651 "embedding_model": stats.embedding_model,
652 "embedding_dimensions": stats.embedding_dimensions,
653 }))
654 }
655 Err(e) => Err(ErrorData::internal_error(format!("Stats error: {e}"), None)),
656 }
657 }
658
659 #[tool(
660 description = "Find shortest path between two items in the knowledge graph. Traverses all edge types. Returns node IDs with edge evidence per hop.",
661 annotations(read_only_hint = true)
662 )]
663 fn sm_graph_path(
664 &self,
665 Parameters(GraphPathParams {
666 from_id,
667 to_id,
668 max_depth,
669 }): Parameters<GraphPathParams>,
670 ) -> Result<String, ErrorData> {
671 let depth = max_depth.map(|v| v as usize).unwrap_or(5);
672 let store = &self.bridge.store;
673 let g = store.graph_view();
674
675 match g.path(&from_id, &to_id, depth) {
676 Ok(Some(path)) => {
677 let path_segments = build_path_segments(store, &path);
679 json_to_string(&serde_json::json!({
680 "ok": true,
681 "from": from_id,
682 "to": to_id,
683 "path": path,
684 "path_length": path.len(),
685 "segments": path_segments,
686 }))
687 }
688 Ok(None) => json_to_string(&serde_json::json!({
689 "ok": true,
690 "from": from_id,
691 "to": to_id,
692 "path": null,
693 "message": format!("No path found from {from_id} to {to_id} within depth {depth}"),
694 })),
695 Err(e) => Err(ErrorData::internal_error(
696 format!("Graph view error: {e}"),
697 None,
698 )),
699 }
700 }
701
702 #[tool(
705 description = "Fetch one fact by id (bare UUID or prefixed 'fact:<uuid>'). Returns full content, namespace, source, timestamps, and metadata.",
706 annotations(read_only_hint = true)
707 )]
708 fn sm_get_fact(
709 &self,
710 Parameters(GetFactParams { fact_id }): Parameters<GetFactParams>,
711 ) -> Result<String, ErrorData> {
712 let bare = fact_id
713 .strip_prefix("fact:")
714 .unwrap_or(&fact_id)
715 .to_string();
716 let store = &self.bridge.store;
717 let result =
718 tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&bare)));
719 match result {
720 Ok(Some(f)) => json_to_string(&serde_json::json!({
721 "ok": true,
722 "found": true,
723 "fact": {
724 "result_id": format!("fact:{}", f.id),
725 "id": f.id,
726 "namespace": f.namespace,
727 "content": f.content,
728 "source": f.source,
729 "created_at": f.created_at,
730 "updated_at": f.updated_at,
731 "metadata": f.metadata,
732 },
733 })),
734 Ok(None) => json_to_string(&serde_json::json!({
735 "ok": true,
736 "found": false,
737 "message": format!("No fact with id '{fact_id}'"),
738 })),
739 Err(e) => Err(ErrorData::internal_error(
740 format!("get_fact error: {e}"),
741 None,
742 )),
743 }
744 }
745
746 #[tool(
747 description = "Enumerate facts in a namespace (newest first) with pagination. Exhaustive, not similarity-ranked — for browsing, auditing, or deduping.",
748 annotations(read_only_hint = true)
749 )]
750 fn sm_list_facts(
751 &self,
752 Parameters(ListFactsParams {
753 namespace,
754 limit,
755 offset,
756 }): Parameters<ListFactsParams>,
757 ) -> Result<String, ErrorData> {
758 let lim = limit.map(|v| v as usize).unwrap_or(50);
759 let off = offset.map(|v| v as usize).unwrap_or(0);
760 let store = &self.bridge.store;
761 let result = tokio::task::block_in_place(|| {
762 Handle::current().block_on(store.list_facts(&namespace, lim, off))
763 });
764 match result {
765 Ok(facts) => {
766 let arr: Vec<serde_json::Value> = facts
767 .iter()
768 .map(|f| {
769 serde_json::json!({
770 "result_id": format!("fact:{}", f.id),
771 "id": f.id,
772 "namespace": f.namespace,
773 "content": f.content,
774 "source": f.source,
775 "updated_at": f.updated_at,
776 })
777 })
778 .collect();
779 json_to_string(&serde_json::json!({
780 "ok": true,
781 "namespace": namespace,
782 "count": arr.len(),
783 "limit": lim,
784 "offset": off,
785 "facts": arr,
786 }))
787 }
788 Err(e) => Err(ErrorData::internal_error(
789 format!("list_facts error: {e}"),
790 None,
791 )),
792 }
793 }
794
795 #[tool(
796 description = "List namespaces that currently contain facts. Use before sm_list_facts to discover what is stored.",
797 annotations(read_only_hint = true)
798 )]
799 fn sm_list_namespaces(&self) -> Result<String, ErrorData> {
800 let store = &self.bridge.store;
801 let result = tokio::task::block_in_place(|| {
802 Handle::current().block_on(store.list_fact_namespaces())
803 });
804 match result {
805 Ok(ns) => json_to_string(&serde_json::json!({
806 "ok": true,
807 "count": ns.len(),
808 "namespaces": ns,
809 })),
810 Err(e) => Err(ErrorData::internal_error(
811 format!("list_namespaces error: {e}"),
812 None,
813 )),
814 }
815 }
816
817 #[tool(
818 description = "Fetch a fact plus its graph neighbors WITH their content in one call. Hydrates neighbor facts for ids returned by graph tools.",
819 annotations(read_only_hint = true)
820 )]
821 fn sm_get_fact_neighbors(
822 &self,
823 Parameters(GetFactNeighborsParams { item_id }): Parameters<GetFactNeighborsParams>,
824 ) -> Result<String, ErrorData> {
825 let node_id = if item_id.contains(':') {
826 item_id.clone()
827 } else {
828 format!("fact:{item_id}")
829 };
830 let bare = node_id
831 .strip_prefix("fact:")
832 .unwrap_or(&node_id)
833 .to_string();
834 let store = &self.bridge.store;
835
836 let center =
837 tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&bare)))
838 .map_err(|e| ErrorData::internal_error(format!("get_fact error: {e}"), None))?;
839 let edges = tokio::task::block_in_place(|| {
840 Handle::current().block_on(store.list_graph_edges_for_node(&node_id))
841 })
842 .map_err(|e| ErrorData::internal_error(format!("list edges error: {e}"), None))?;
843
844 let mut neighbors: Vec<serde_json::Value> = Vec::new();
845 for e in &edges {
846 let outgoing = e.source == node_id;
847 let other = if outgoing { &e.target } else { &e.source };
848 let other_bare = other.strip_prefix("fact:").unwrap_or(other).to_string();
849 let content = tokio::task::block_in_place(|| {
850 Handle::current().block_on(store.get_fact(&other_bare))
851 })
852 .ok()
853 .flatten()
854 .map(|f| f.content);
855 neighbors.push(serde_json::json!({
856 "neighbor_id": other,
857 "direction": if outgoing { "out" } else { "in" },
858 "edge_type": e.edge_type,
859 "weight": e.weight,
860 "content": content,
861 }));
862 }
863 json_to_string(&serde_json::json!({
864 "ok": true,
865 "item_id": node_id,
866 "center_content": center.map(|f| f.content),
867 "neighbor_count": neighbors.len(),
868 "neighbors": neighbors,
869 }))
870 }
871
872 #[tool(
873 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.",
874 annotations(idempotent_hint = true)
875 )]
876 fn sm_supersede_fact(
877 &self,
878 Parameters(SupersedeFactParams {
879 old_fact_id,
880 content,
881 namespace,
882 source,
883 reason,
884 }): Parameters<SupersedeFactParams>,
885 ) -> Result<String, ErrorData> {
886 use semantic_memory::GraphEdgeType;
887
888 let old_bare = old_fact_id
889 .strip_prefix("fact:")
890 .unwrap_or(&old_fact_id)
891 .to_string();
892 let old_node = format!("fact:{old_bare}");
893 let store = &self.bridge.store;
894 let old =
895 tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&old_bare)))
896 .map_err(|e| ErrorData::internal_error(format!("get old fact error: {e}"), None))?;
897 let Some(old_fact) = old else {
898 return Err(ErrorData::invalid_params(
899 format!("No fact with id '{old_fact_id}'"),
900 None,
901 ));
902 };
903
904 let ns = namespace.unwrap_or_else(|| old_fact.namespace.clone());
905 let new_id = tokio::task::block_in_place(|| {
906 Handle::current().block_on(store.add_fact(&ns, &content, source.as_deref(), None))
907 })
908 .map_err(|e| ErrorData::internal_error(format!("add replacement fact error: {e}"), None))?;
909 let new_node = format!("fact:{new_id}");
910 let metadata = serde_json::json!({
911 "reason": reason.unwrap_or_else(|| "replacement fact supersedes stale fact".to_string()),
912 "old_fact_id": old_bare,
913 });
914 let edge = tokio::task::block_in_place(|| {
915 Handle::current().block_on(store.add_graph_edge(
916 &new_node,
917 &old_node,
918 GraphEdgeType::Entity {
919 relation: "supersedes".to_string(),
920 },
921 1.0,
922 Some(metadata),
923 ))
924 })
925 .map_err(|e| ErrorData::internal_error(format!("add supersedes edge error: {e}"), None))?;
926
927 json_to_string(&serde_json::json!({
928 "ok": true,
929 "new_fact_id": new_id,
930 "new_result_id": new_node,
931 "old_fact_id": old_bare,
932 "old_result_id": old_node,
933 "namespace": ns,
934 "edge_id": edge.id,
935 "relation": "supersedes",
936 }))
937 }
938
939 #[allow(dead_code)]
946 fn sm_create_session(
947 &self,
948 Parameters(CreateSessionParams { channel, metadata }): Parameters<CreateSessionParams>,
949 ) -> Result<String, ErrorData> {
950 let meta: Option<serde_json::Value> = metadata
951 .as_deref()
952 .and_then(|s| serde_json::from_str(s).ok());
953 let store = &self.bridge.store;
954 let result = tokio::task::block_in_place(|| {
955 Handle::current().block_on(store.create_session_with_metadata(&channel, meta))
956 });
957 match result {
958 Ok(id) => json_to_string(
959 &serde_json::json!({"ok": true, "session_id": id, "channel": channel}),
960 ),
961 Err(e) => Err(ErrorData::internal_error(
962 format!("create_session error: {e}"),
963 None,
964 )),
965 }
966 }
967
968 #[allow(dead_code)]
972 fn sm_add_message(
973 &self,
974 Parameters(AddMessageParams {
975 session_id,
976 role,
977 content,
978 }): Parameters<AddMessageParams>,
979 ) -> Result<String, ErrorData> {
980 let parsed_role = match role.to_lowercase().as_str() {
981 "user" => semantic_memory::types::Role::User,
982 "assistant" => semantic_memory::types::Role::Assistant,
983 "system" => semantic_memory::types::Role::System,
984 "tool" => semantic_memory::types::Role::Tool,
985 other => {
986 return Err(ErrorData::invalid_params(
987 format!("invalid role '{other}' (use user|assistant|system|tool)"),
988 None,
989 ))
990 }
991 };
992 let store = &self.bridge.store;
993 let result = tokio::task::block_in_place(|| {
994 Handle::current().block_on(store.add_message_embedded(
995 &session_id,
996 parsed_role,
997 &content,
998 None,
999 None,
1000 ))
1001 });
1002 match result {
1003 Ok(id) => json_to_string(
1004 &serde_json::json!({"ok": true, "message_id": id, "session_id": session_id}),
1005 ),
1006 Err(e) => Err(ErrorData::internal_error(
1007 format!("add_message error: {e}"),
1008 None,
1009 )),
1010 }
1011 }
1012
1013 #[allow(dead_code)]
1015 fn sm_list_sessions(
1016 &self,
1017 Parameters(ListSessionsParams { limit, offset }): Parameters<ListSessionsParams>,
1018 ) -> Result<String, ErrorData> {
1019 let lim = limit.map(|v| v as usize).unwrap_or(20);
1020 let off = offset.map(|v| v as usize).unwrap_or(0);
1021 let store = &self.bridge.store;
1022 let result = tokio::task::block_in_place(|| {
1023 Handle::current().block_on(store.list_sessions(lim, off))
1024 });
1025 match result {
1026 Ok(sessions) => json_to_string(&serde_json::json!({
1027 "ok": true,
1028 "count": sessions.len(),
1029 "sessions": sessions.iter().map(|s| serde_json::json!({
1030 "session_id": s.id,
1031 "channel": s.channel,
1032 "message_count": s.message_count,
1033 "created_at": s.created_at,
1034 "updated_at": s.updated_at,
1035 })).collect::<Vec<_>>(),
1036 })),
1037 Err(e) => Err(ErrorData::internal_error(
1038 format!("list_sessions error: {e}"),
1039 None,
1040 )),
1041 }
1042 }
1043
1044 #[allow(dead_code)]
1049 fn sm_get_messages(
1050 &self,
1051 Parameters(GetMessagesParams {
1052 session_id,
1053 max_tokens,
1054 }): Parameters<GetMessagesParams>,
1055 ) -> Result<String, ErrorData> {
1056 let budget = max_tokens.unwrap_or(4000);
1057 let store = &self.bridge.store;
1058 let result = tokio::task::block_in_place(|| {
1059 Handle::current().block_on(store.get_messages_within_budget(&session_id, budget))
1060 });
1061 match result {
1062 Ok(msgs) => json_to_string(&serde_json::json!({
1063 "ok": true,
1064 "session_id": session_id,
1065 "count": msgs.len(),
1066 "messages": msgs.iter().map(|m| serde_json::json!({
1067 "id": m.id,
1068 "role": m.role,
1069 "content": m.content,
1070 "token_count": m.token_count,
1071 "created_at": m.created_at,
1072 })).collect::<Vec<_>>(),
1073 })),
1074 Err(e) => Err(ErrorData::internal_error(
1075 format!("get_messages error: {e}"),
1076 None,
1077 )),
1078 }
1079 }
1080
1081 #[tool(
1082 description = "Hybrid semantic search over stored conversation MESSAGES (not facts). Recall what was discussed in past sessions. Returns ranked messages.",
1083 annotations(read_only_hint = true)
1084 )]
1085 fn sm_search_conversations(
1086 &self,
1087 Parameters(SearchConversationsParams { query, top_k }): Parameters<
1088 SearchConversationsParams,
1089 >,
1090 ) -> Result<String, ErrorData> {
1091 let k = top_k.map(|v| v as usize);
1092 let store = &self.bridge.store;
1093 let result = tokio::task::block_in_place(|| {
1094 Handle::current().block_on(store.search_conversations(&query, k, None))
1095 });
1096 match result {
1097 Ok(results) => json_to_string(&serde_json::json!({
1098 "ok": true,
1099 "count": results.len(),
1100 "results": results.iter().map(|r| serde_json::json!({
1101 "result_id": r.source.result_id(),
1102 "content": r.content,
1103 "score": r.score,
1104 "cosine_similarity": r.cosine_similarity,
1105 })).collect::<Vec<_>>(),
1106 })),
1107 Err(e) => Err(ErrorData::internal_error(
1108 format!("search_conversations error: {e}"),
1109 None,
1110 )),
1111 }
1112 }
1113
1114 #[tool(
1121 description = "Profile a query and get an adaptive routing decision. Determines which retrieval stages (BM25, vector, rerank, graph, decoder, discord) to activate.",
1122 annotations(read_only_hint = true)
1123 )]
1124 fn sm_route_query(
1125 &self,
1126 Parameters(RouteQueryParams { query }): Parameters<RouteQueryParams>,
1127 ) -> Result<String, ErrorData> {
1128 use semantic_memory::routing::RetrievalRouter;
1129
1130 let router = RetrievalRouter {
1131 decoder_enabled: true,
1132 discord_enabled: true,
1133 corpus_density: 0.5,
1134 ..Default::default()
1135 };
1136
1137 let decision = router.route_query(&query);
1138 json_to_string(&serde_json::json!({
1139 "ok": true,
1140 "bm25_coarse": decision.bm25_coarse,
1141 "vector_medium": decision.vector_medium,
1142 "rerank_fine": decision.rerank_fine,
1143 "graph_expansion": decision.graph_expansion,
1144 "decoder": decision.decoder,
1145 "discord": decision.discord,
1146 "no_retrieval": decision.no_retrieval,
1147 "reasoning": decision.reasoning,
1148 }))
1149 }
1150
1151 #[tool(
1152 description = "Adaptive search: profiles query, routes to appropriate stages, applies factor graph belief propagation if decoder is activated. Returns results with stable IDs.",
1153 annotations(read_only_hint = true)
1154 )]
1155 fn sm_search_with_routing(
1156 &self,
1157 Parameters(SearchWithRoutingParams {
1158 query,
1159 top_k,
1160 contradictions,
1161 group_by_community,
1162 }): Parameters<SearchWithRoutingParams>,
1163 ) -> Result<String, ErrorData> {
1164 use semantic_memory::integration::plan_execution;
1165 use semantic_memory::routing::RetrievalRouter;
1166
1167 let k = top_k.map(|v| v as usize).unwrap_or(5);
1168 let allow_superseded = query_allows_superseded(&query);
1169 let search_k = if allow_superseded { k } else { (k * 4).max(20) };
1170 let router = RetrievalRouter {
1171 decoder_enabled: true,
1172 discord_enabled: true,
1173 corpus_density: 0.5,
1174 ..Default::default()
1175 };
1176
1177 let decision = router.route_query(&query);
1178 let contras = contradictions.unwrap_or_default();
1179 let plan = plan_execution(&decision, contras.clone());
1180
1181 let store = &self.bridge.store;
1182 let search_result = tokio::task::block_in_place(|| {
1183 Handle::current().block_on(store.search(&query, Some(search_k), None, None))
1184 });
1185
1186 match search_result {
1187 Ok(results) => {
1188 let superseded_targets = if allow_superseded {
1189 HashSet::new()
1190 } else {
1191 load_superseded_targets(store)?
1192 };
1193 let fresh_results: Vec<_> = results
1194 .iter()
1195 .filter(|r| !superseded_targets.contains(&r.source.result_id()))
1196 .collect();
1197 let result_refs: Vec<_> =
1198 if superseded_targets.is_empty() || fresh_results.is_empty() {
1199 results.iter().collect()
1200 } else {
1201 fresh_results
1202 };
1203 let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
1204 let json_results: Vec<serde_json::Value> = result_refs
1205 .iter()
1206 .take(k)
1207 .map(|r| {
1208 serde_json::json!({
1209 "result_id": r.source.result_id(),
1210 "content": r.content,
1211 "score": r.score,
1212 })
1213 })
1214 .collect();
1215
1216 let mut factor_graph_payload = serde_json::json!({
1217 "enabled": false,
1218 });
1219
1220 let mut decoder_executed = false;
1221 let mut discord_executed = false;
1222 let mut discord_results_payload: Vec<serde_json::Value> = Vec::new();
1223
1224 if decision.decoder {
1225 #[cfg(feature = "full")]
1226 {
1227 use semantic_memory::factor_graph::{
1228 factors_from_edges, FactorGraph, FactorGraphConfig,
1229 };
1230
1231 let graph_edges = tokio::task::block_in_place(|| {
1232 Handle::current().block_on(store.list_all_graph_edges())
1233 });
1234
1235 match graph_edges {
1236 Ok(edges) => {
1237 let raw_edges: Vec<(
1238 String,
1239 String,
1240 semantic_memory::GraphEdgeType,
1241 f64,
1242 Option<String>,
1243 )> = edges
1244 .iter()
1245 .map(|edge| {
1246 let parsed_type = edge
1247 .edge_type_parsed
1248 .clone()
1249 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
1250 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
1251 relation: "unknown".to_string(),
1252 });
1253 (
1254 edge.source.clone(),
1255 edge.target.clone(),
1256 parsed_type,
1257 edge.weight,
1258 edge.metadata.clone(),
1259 )
1260 })
1261 .collect();
1262
1263 let nodes: Vec<(String, f64)> = result_refs
1264 .iter()
1265 .map(|r| (r.source.result_id(), r.score))
1266 .collect();
1267 let factors = factors_from_edges(&raw_edges);
1268 let graph =
1269 FactorGraph::new(&nodes, factors, FactorGraphConfig::default());
1270 let propagated = graph.propagate();
1271 let top_beliefs = propagated.top_k(k);
1272
1273 factor_graph_payload = serde_json::json!({
1274 "enabled": true,
1275 "top_k_beliefs": top_beliefs
1276 .into_iter()
1277 .map(|(item_id, belief)| serde_json::json!({
1278 "item_id": item_id,
1279 "belief": belief,
1280 }))
1281 .collect::<Vec<_>>(),
1282 "iterations": propagated.iterations,
1283 "converged": propagated.converged,
1284 "elapsed_ms": propagated.elapsed_ms,
1285 "factor_counts": {
1286 "semantic": propagated.factor_counts.semantic,
1287 "temporal": propagated.factor_counts.temporal,
1288 "causal": propagated.factor_counts.causal,
1289 "entity": propagated.factor_counts.entity,
1290 "total": propagated.factor_counts.total(),
1291 },
1292 });
1293 decoder_executed = true;
1294 }
1295 Err(e) => {
1296 factor_graph_payload = serde_json::json!({
1297 "enabled": false,
1298 "error": format!("factor graph analysis failed: {e}"),
1299 });
1300 }
1301 }
1302 }
1303
1304 #[cfg(not(feature = "full"))]
1305 {
1306 factor_graph_payload = serde_json::json!({
1307 "enabled": false,
1308 "reason": "factor graph analysis requires the `full` feature",
1309 });
1310 }
1311
1312 if !plan.contradictions.is_empty() {
1313 use semantic_memory::decoder::{compute_correction, detect_syndromes};
1314 let result_scores: Vec<(String, f64)> = result_refs
1315 .iter()
1316 .map(|r| (r.source.result_id(), r.score))
1317 .collect();
1318 let syndromes = detect_syndromes(&result_scores, &plan.contradictions);
1319 let _ = compute_correction(&syndromes, 10.0);
1320 decoder_executed = true;
1321 }
1322 }
1323
1324 if plan.use_discord {
1325 use semantic_memory::discord::DiscordScorer;
1326 let direct_ids: Vec<String> = result_refs
1327 .iter()
1328 .map(|r| r.source.result_id())
1329 .collect();
1330 let existing_ids: std::collections::HashSet<String> =
1331 direct_ids.iter().cloned().collect();
1332 if let Ok(edges) =
1333 load_neighborhood_edge_refs(&self.bridge.store, &direct_ids)
1334 {
1335 let scorer = DiscordScorer::with_defaults();
1336 let discord_hits = scorer.score(&direct_ids, &edges);
1337 for hit in &discord_hits {
1338 if !existing_ids.contains(&hit.item_id) {
1339 discord_results_payload.push(serde_json::json!({
1340 "result_id": hit.item_id,
1341 "discord_score": hit.discord_score,
1342 "anchor_ids": hit.anchor_ids,
1343 "relationship_types": hit.relationship_types,
1344 }));
1345 }
1346 }
1347 discord_executed = true;
1348 }
1349 }
1350
1351 let mut matryoshka_payload = serde_json::json!({
1352 "enabled": false,
1353 });
1354 if decision.vector_medium {
1355 #[cfg(feature = "full")]
1356 {
1357 use semantic_memory::integration::multi_resolution_route;
1358 use semantic_memory::matryoshka::MatryoshkaConfig;
1359 use semantic_memory::routing::QueryProfile;
1360
1361 let route_profile = QueryProfile::from_query(&query);
1362 let route_decision =
1363 multi_resolution_route(&route_profile, &MatryoshkaConfig::default());
1364 matryoshka_payload = serde_json::json!({
1365 "enabled": true,
1366 "candidate_dim": route_decision.candidate_dim,
1367 "heuristic_recall_estimate": route_decision.estimated_recall,
1368 "recall_basis": "heuristic_dimensional_model_not_corpus_measured",
1369 "embedding_dim": route_decision.embedding_dim,
1370 "reasoning": route_decision.reasoning,
1371 });
1372 }
1373
1374 #[cfg(not(feature = "full"))]
1375 {
1376 matryoshka_payload = serde_json::json!({
1377 "enabled": false,
1378 "reason": "matryoshka routing requires the `full` feature",
1379 });
1380 }
1381 }
1382
1383 let grouped_results_payload: serde_json::Value =
1385 if group_by_community == Some(true) {
1386 let seed_ids: Vec<String> = result_refs
1387 .iter()
1388 .take(k)
1389 .map(|r| r.source.result_id())
1390 .collect();
1391 let edges =
1392 load_neighborhood_edge_pairs(store, &seed_ids).unwrap_or_default();
1393 if !edges.is_empty() {
1394 use semantic_memory::community::detect_communities;
1395 let communities = detect_communities(&edges, 1.0, 42);
1396 let mut member_to_comm: std::collections::HashMap<String, String> =
1397 std::collections::HashMap::new();
1398 for c in &communities {
1399 for m in &c.members {
1400 member_to_comm.insert(m.clone(), c.id.clone());
1401 }
1402 }
1403 let mut groups: std::collections::HashMap<
1404 String,
1405 Vec<serde_json::Value>,
1406 > = std::collections::HashMap::new();
1407 let mut ungrouped: Vec<serde_json::Value> = Vec::new();
1408 for r in &json_results {
1409 if let Some(rid) =
1410 r.get("result_id").and_then(|v| v.as_str())
1411 {
1412 match member_to_comm.get(rid).cloned() {
1413 Some(cid) => {
1414 groups.entry(cid).or_default().push(r.clone())
1415 }
1416 None => ungrouped.push(r.clone()),
1417 }
1418 }
1419 }
1420 let mut map = serde_json::Map::new();
1421 for (cid, items) in groups {
1422 map.insert(
1423 format!("community_{cid}"),
1424 serde_json::json!(items),
1425 );
1426 }
1427 if !ungrouped.is_empty() {
1428 map.insert(
1429 "ungrouped".to_string(),
1430 serde_json::json!(ungrouped),
1431 );
1432 }
1433 serde_json::Value::Object(map)
1434 } else {
1435 serde_json::Value::Null
1436 }
1437 } else {
1438 serde_json::Value::Null
1439 };
1440
1441 let mut topology_payload = serde_json::json!({ "auto_called": false });
1443 {
1444 use semantic_memory::routing::{QueryComplexityClass, QueryProfile};
1445 let route_profile = QueryProfile::from_query(&query);
1446 if route_profile.complexity_class == QueryComplexityClass::Synthesis
1447 && result_refs.len() > 10
1448 {
1449 #[cfg(feature = "full")]
1450 {
1451 use semantic_memory::topology::{compute_betti_numbers, find_voids};
1452 let edges = load_stored_edge_pairs(store).unwrap_or_default();
1453 if !edges.is_empty() {
1454 let mut adjacency: std::collections::HashMap<String, Vec<String>> =
1455 std::collections::HashMap::new();
1456 for (src, tgt) in &edges {
1457 adjacency.entry(src.clone()).or_default().push(tgt.clone());
1458 adjacency.entry(tgt.clone()).or_default().push(src.clone());
1459 }
1460 let betti = compute_betti_numbers(&adjacency);
1461 let voids = find_voids(&edges);
1462 topology_payload = serde_json::json!({
1463 "auto_called": true,
1464 "trigger": "synthesis_class_with_10_plus_results",
1465 "betti_numbers": {
1466 "betti_0": betti.betti_0,
1467 "betti_1": betti.betti_1,
1468 },
1469 "void_count": voids.len(),
1470 "voids": voids.iter().map(|v| serde_json::json!({
1471 "description": v.description,
1472 "void_type": format!("{:?}", v.void_type),
1473 "nearby_items": v.nearby_items,
1474 "suggested_connections": v.suggested_connections,
1475 })).collect::<Vec<_>>(),
1476 });
1477 } else {
1478 topology_payload = serde_json::json!({
1479 "auto_called": true,
1480 "trigger": "synthesis_class_with_10_plus_results",
1481 "note": "no graph edges in store",
1482 });
1483 }
1484 }
1485 #[cfg(not(feature = "full"))]
1486 {
1487 topology_payload = serde_json::json!({
1488 "auto_called": true,
1489 "trigger": "synthesis_class_with_10_plus_results",
1490 "error": "topology requires the full feature",
1491 });
1492 }
1493 }
1494 }
1495
1496 json_to_string(&serde_json::json!({
1497 "ok": true,
1498 "routing_decision": {
1499 "bm25_coarse": decision.bm25_coarse,
1500 "vector_medium": decision.vector_medium,
1501 "rerank_fine": decision.rerank_fine,
1502 "graph_expansion": decision.graph_expansion,
1503 "decoder": decision.decoder,
1504 "discord": decision.discord,
1505 "no_retrieval": decision.no_retrieval,
1506 "reasoning": decision.reasoning,
1507 },
1508 "results": json_results,
1509 "count": json_results.len(),
1510 "superseded_filtered_count": superseded_filtered_count,
1511 "decoder_planned": plan.use_decoder,
1512 "decoder_executed": decoder_executed,
1513 "discord_planned": plan.use_discord,
1514 "discord_executed": discord_executed,
1515 "discord_results": discord_results_payload,
1516 "factor_graph": factor_graph_payload,
1517 "matryoshka": matryoshka_payload,
1518 "grouped_results": grouped_results_payload,
1519 "topology": topology_payload,
1520 }))
1521 }
1522 Err(e) => Err(ErrorData::internal_error(
1523 format!("Search error: {e}"),
1524 None,
1525 )),
1526 }
1527 }
1528
1529 #[tool(
1530 description = "Detect contradictions in search results. Runs syndrome detection, computes corrections, and applies belief propagation to refine confidence scores.",
1531 annotations(read_only_hint = true)
1532 )]
1533 fn sm_decoder_analyze(
1534 &self,
1535 Parameters(DecoderAnalyzeParams {
1536 results,
1537 contradictions,
1538 }): Parameters<DecoderAnalyzeParams>,
1539 ) -> Result<String, ErrorData> {
1540 use semantic_memory::decoder::{
1541 compute_correction, detect_syndromes, pass_messages, ConflictGraph,
1542 };
1543
1544 let contras = contradictions.unwrap_or_default();
1545 let syndromes = detect_syndromes(&results, &contras);
1546 let corrections = compute_correction(&syndromes, 10.0);
1547 let graph = ConflictGraph::from_syndromes(&results, &syndromes);
1548 let mp = pass_messages(&graph, 50, 0.001);
1549
1550 json_to_string(&serde_json::json!({
1551 "ok": true,
1552 "syndromes": syndromes.iter().map(|s| serde_json::json!({
1553 "id": s.id,
1554 "severity": format!("{:?}", s.severity),
1555 "items": s.items,
1556 "description": s.description,
1557 "type": format!("{:?}", s.syndrome_type),
1558 })).collect::<Vec<_>>(),
1559 "syndrome_count": syndromes.len(),
1560 "corrections": corrections.iter().map(|c| serde_json::json!({
1561 "id": c.id,
1562 "confidence": c.confidence,
1563 "cost": c.cost,
1564 "operations": c.operations.len(),
1565 })).collect::<Vec<_>>(),
1566 "correction_count": corrections.len(),
1567 "message_passing": {
1568 "iterations": mp.iterations,
1569 "converged": mp.converged,
1570 "elapsed_ms": mp.elapsed_ms,
1571 },
1572 }))
1573 }
1574
1575 #[tool(
1576 description = "Second-order retrieval: find items related to your search results through the graph, but NOT themselves direct hits. Loads edges from store automatically.",
1577 annotations(read_only_hint = true)
1578 )]
1579 fn sm_discord_search(
1580 &self,
1581 Parameters(DiscordSearchParams { direct_result_ids }): Parameters<DiscordSearchParams>,
1582 ) -> Result<String, ErrorData> {
1583 use semantic_memory::discord::DiscordScorer;
1584
1585 let edges = load_neighborhood_edge_refs(&self.bridge.store, &direct_result_ids)?;
1588 let scorer = DiscordScorer::with_defaults();
1589 let results = scorer.score(&direct_result_ids, &edges);
1590
1591 json_to_string(&serde_json::json!({
1592 "ok": true,
1593 "discord_results": results.iter().map(|r| serde_json::json!({
1594 "item_id": r.item_id,
1595 "discord_score": r.discord_score,
1596 "anchor_ids": r.anchor_ids,
1597 "relationship_types": r.relationship_types,
1598 })).collect::<Vec<_>>(),
1599 "count": results.len(),
1600 "edges_loaded": edges.len(),
1601 "edges_scope": "neighborhood",
1602 }))
1603 }
1604
1605 #[tool(
1606 description = "Set provenance (evidence confidence) for an item. Confidence in [0.0, 1.0] with support count. Returns a provenance receipt.",
1607 annotations(idempotent_hint = true)
1608 )]
1609 fn sm_set_provenance(
1610 &self,
1611 Parameters(SetProvenanceParams {
1612 item_id,
1613 confidence,
1614 support_count,
1615 }): Parameters<SetProvenanceParams>,
1616 ) -> Result<String, ErrorData> {
1617 use semantic_memory::provenance::{
1618 ConfidenceSemiring, ConfidenceValue, ProvenanceItemType,
1619 };
1620
1621 if !confidence.is_finite() || confidence < 0.0 || confidence > 1.0 {
1623 return Err(ErrorData::invalid_params(
1624 format!("confidence must be a finite value in [0.0, 1.0], got {confidence}"),
1625 None,
1626 ));
1627 }
1628
1629 let value = ConfidenceValue::new(confidence, support_count);
1630 let store = &self.bridge.store;
1631
1632 let result = tokio::task::block_in_place(|| {
1633 Handle::current().block_on(store.set_provenance::<ConfidenceSemiring>(
1634 &ProvenanceItemType::Fact,
1635 &item_id,
1636 &value,
1637 &[],
1638 None,
1639 ))
1640 });
1641
1642 match result {
1643 Ok(receipt) => json_to_string(&serde_json::json!({
1644 "ok": true,
1645 "provenance_id": receipt.provenance_id,
1646 "item_id": receipt.item_id,
1647 "semiring_type": receipt.semiring_type,
1648 "recorded_at": receipt.recorded_at,
1649 "message": "Provenance set successfully",
1650 })),
1651 Err(e) => Err(ErrorData::internal_error(
1652 format!("Provenance error: {e}"),
1653 None,
1654 )),
1655 }
1656 }
1657
1658 #[tool(
1659 description = "Run a memory lifecycle pass: analyze items for syndromes, compute corrections, identify subtraction candidates, and check compression needs.",
1660 annotations(read_only_hint = true)
1661 )]
1662 fn sm_run_lifecycle(
1663 &self,
1664 Parameters(RunLifecycleParams { item_ids }): Parameters<RunLifecycleParams>,
1665 ) -> Result<String, ErrorData> {
1666 use semantic_memory::decoder::{compute_correction, detect_syndromes};
1667 use semantic_memory::integration::{
1668 corrections_to_subtraction_candidates, should_trigger_recompression,
1669 };
1670
1671 let results: Vec<(String, f64)> = item_ids.iter().map(|id| (id.clone(), 0.5)).collect();
1672 let syndromes = detect_syndromes(&results, &[]);
1673 let corrections = compute_correction(&syndromes, 10.0);
1674
1675 let sub_candidates = corrections_to_subtraction_candidates(&corrections);
1676
1677 let subtracted_count = sub_candidates.len();
1678 let remaining_count = item_ids.len().saturating_sub(subtracted_count);
1679 let recompression = should_trigger_recompression(subtracted_count, remaining_count, false);
1680
1681 let store = &self.bridge.store;
1682 let graph_edges = tokio::task::block_in_place(|| {
1683 Handle::current().block_on(store.list_all_graph_edges())
1684 });
1685 let stored_edges: Vec<(String, String)> = graph_edges
1686 .as_ref()
1687 .map(|edges| {
1688 edges
1689 .iter()
1690 .map(|edge| (edge.source.clone(), edge.target.clone()))
1691 .collect()
1692 })
1693 .unwrap_or_default();
1694
1695 let mut topology_voids: Vec<serde_json::Value> = Vec::new();
1696 let mut betti = serde_json::json!({
1697 "betti_0": 0usize,
1698 "betti_1": 0usize,
1699 });
1700 let mut topology_error: Option<String> = None;
1701
1702 let mut communities: Vec<serde_json::Value> = Vec::new();
1703 let mut community_contradictions: Vec<serde_json::Value> = Vec::new();
1704 let mut community_error: Option<String> = None;
1705
1706 let mut subgraph_assessment = serde_json::json!({
1707 "subgraphs_identified": 0usize,
1708 "subgraphs_pruned": 0usize,
1709 });
1710 let mut subgraph_error: Option<String> = None;
1711
1712 #[cfg(feature = "full")]
1713 {
1714 use std::collections::HashMap;
1715
1716 if !stored_edges.is_empty() {
1717 let analysis_edges = stored_edges.clone();
1718
1719 let topology_result = (|| -> Result<(), String> {
1720 use semantic_memory::topology::{compute_betti_numbers, find_voids};
1721
1722 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
1723 for (left, right) in &analysis_edges {
1724 adjacency
1725 .entry(left.clone())
1726 .or_default()
1727 .push(right.clone());
1728 adjacency
1729 .entry(right.clone())
1730 .or_default()
1731 .push(left.clone());
1732 }
1733
1734 let betti_numbers = compute_betti_numbers(&adjacency);
1735 betti = serde_json::json!({
1736 "betti_0": betti_numbers.betti_0,
1737 "betti_1": betti_numbers.betti_1,
1738 });
1739
1740 topology_voids = find_voids(&analysis_edges)
1741 .into_iter()
1742 .map(|v| {
1743 serde_json::json!({
1744 "description": v.description,
1745 "void_type": format!("{:?}", v.void_type),
1746 "nearby_items": v.nearby_items,
1747 "suggested_connections": v.suggested_connections,
1748 })
1749 })
1750 .collect();
1751
1752 Ok(())
1753 })();
1754
1755 if let Err(e) = topology_result {
1756 topology_error = Some(e);
1757 }
1758
1759 let community_result = (|| -> Result<(), String> {
1760 use semantic_memory::community::{
1761 community_contradiction_scan, detect_communities,
1762 };
1763
1764 let detected = detect_communities(&analysis_edges, 1.0, 42);
1765 communities = detected
1766 .iter()
1767 .map(|c| {
1768 serde_json::json!({
1769 "id": c.id,
1770 "members": c.members,
1771 "level": c.level,
1772 "parent": c.parent,
1773 "member_count": c.members.len(),
1774 })
1775 })
1776 .collect();
1777
1778 community_contradictions = community_contradiction_scan(&detected, &[])
1779 .into_iter()
1780 .map(|cc| {
1781 serde_json::json!({
1782 "community_id": cc.community_id,
1783 "item_a": cc.item_a,
1784 "item_b": cc.item_b,
1785 "description": cc.description,
1786 })
1787 })
1788 .collect();
1789
1790 Ok(())
1791 })();
1792
1793 if let Err(e) = community_result {
1794 community_error = Some(e);
1795 }
1796
1797 let subgraph_result = (|| -> Result<(), String> {
1798 use semantic_memory::integration::autonomous_subgraph_maintenance;
1799 use semantic_memory::subgraph_pruning::AccessLog;
1800 use std::collections::HashSet;
1801
1802 let mut access_items: HashSet<String> = HashSet::new();
1803 for (left, right) in &analysis_edges {
1804 access_items.insert(left.clone());
1805 access_items.insert(right.clone());
1806 }
1807
1808 let access_logs = access_items
1809 .into_iter()
1810 .map(|item| AccessLog {
1811 item_id: item,
1812 access_count: 1,
1813 last_accessed: "1970-01-01T00:00:00Z".to_string(),
1814 })
1815 .collect::<Vec<_>>();
1816
1817 let report =
1818 autonomous_subgraph_maintenance(&analysis_edges, &access_logs, &[], 0);
1819 subgraph_assessment = serde_json::json!({
1820 "subgraphs_identified": report.subgraphs_identified,
1821 "subgraphs_pruned": report.subgraphs_pruned,
1822 "summary": report.summary,
1823 });
1824 Ok(())
1825 })();
1826
1827 if let Err(e) = subgraph_result {
1828 subgraph_error = Some(e);
1829 }
1830 }
1831 }
1832
1833 #[cfg(not(feature = "full"))]
1834 {
1835 if !stored_edges.is_empty() {
1836 topology_error = Some(
1837 "topology/community/subgraph phases require the `full` feature".to_string(),
1838 );
1839 community_error = Some(
1840 "topology/community/subgraph phases require the `full` feature".to_string(),
1841 );
1842 subgraph_error = Some(
1843 "topology/community/subgraph phases require the `full` feature".to_string(),
1844 );
1845 }
1846 }
1847
1848 #[cfg(feature = "full")]
1849 let (f32_count, compressed_count) =
1850 item_ids
1851 .iter()
1852 .fold((0usize, 0usize), |(f32_count, compressed_count), _| {
1853 use semantic_memory::compression_governor::{
1854 decide_quantization, QuantizationLevel,
1855 };
1856
1857 match decide_quantization(0.5) {
1858 QuantizationLevel::F32 => (f32_count + 1, compressed_count),
1859 _ => (f32_count, compressed_count + 1),
1860 }
1861 });
1862 #[cfg(not(feature = "full"))]
1863 let (f32_count, compressed_count) = (0usize, 0usize);
1864
1865 json_to_string(&serde_json::json!({
1866 "ok": true,
1867 "items_analyzed": item_ids.len(),
1868 "syndromes_detected": syndromes.len(),
1869 "corrections_computed": corrections.len(),
1870 "subtraction_candidates": sub_candidates.iter().map(|c| serde_json::json!({
1871 "item_id": c.item_id,
1872 "structuring_score": c.structuring_score,
1873 "operation_type": c.operation_type,
1874 "reason": c.reason,
1875 })).collect::<Vec<_>>(),
1876 "recompression_triggered": recompression.triggered,
1877 "recompression_reason": recompression.reason,
1878 "topology": {
1879 "enabled": !stored_edges.is_empty(),
1880 "voids": topology_voids,
1881 "void_count": topology_voids.len(),
1882 "betti_numbers": betti,
1883 "error": topology_error,
1884 },
1885 "community_detection": {
1886 "enabled": !stored_edges.is_empty(),
1887 "communities": communities,
1888 "community_count": communities.len(),
1889 "contradictions": community_contradictions,
1890 "contradiction_count": community_contradictions.len(),
1891 "error": community_error,
1892 },
1893 "subgraph_pruning_assessment": {
1894 "enabled": !stored_edges.is_empty(),
1895 "subgraph_count": subgraph_assessment["subgraphs_identified"].as_u64().unwrap_or(0),
1896 "pruned_count": subgraph_assessment["subgraphs_pruned"].as_u64().unwrap_or(0),
1897 "summary": subgraph_assessment["summary"].as_str().unwrap_or(""),
1898 "error": subgraph_error,
1899 },
1900 "turbo_quantization_assessment": {
1901 "items_assessed": item_ids.len(),
1902 "would_retain_f32": f32_count,
1903 "would_compress": compressed_count,
1904 },
1905 "summary": format!(
1906 "Analyzed {} items: {} syndromes, {} corrections, {} subtraction candidates, recompression: {}",
1907 item_ids.len(), syndromes.len(), corrections.len(), sub_candidates.len(),
1908 if recompression.triggered { "needed" } else { "not needed" }
1909 ),
1910 }))
1911 }
1912
1913 #[tool(
1916 description = "Add a durable, typed graph edge between two nodes. Edge types: semantic, temporal, causal, entity. Idempotent — same edge returns existing ID.",
1917 annotations(idempotent_hint = true)
1918 )]
1919 fn sm_add_graph_edge(
1920 &self,
1921 Parameters(params): Parameters<AddGraphEdgeParams>,
1922 ) -> Result<String, ErrorData> {
1923 use semantic_memory::GraphEdgeType;
1924
1925 if let Some(cs) = params.cosine_similarity {
1927 if !cs.is_finite() || cs < 0.0 || cs > 1.0 {
1928 return Err(ErrorData::invalid_params(
1929 format!("cosine_similarity must be finite and in [0.0, 1.0], got {cs}"),
1930 None,
1931 ));
1932 }
1933 }
1934 if let Some(conf) = params.confidence {
1935 if !conf.is_finite() || conf < 0.0 || conf > 1.0 {
1936 return Err(ErrorData::invalid_params(
1937 format!("confidence must be finite and in [0.0, 1.0], got {conf}"),
1938 None,
1939 ));
1940 }
1941 }
1942
1943 let edge_type = match params.edge_type {
1944 EdgeType::Semantic => GraphEdgeType::Semantic {
1945 cosine_similarity: params.cosine_similarity.unwrap_or(0.5),
1946 },
1947 EdgeType::Temporal => GraphEdgeType::Temporal {
1948 delta_secs: params.delta_secs.unwrap_or(0),
1949 },
1950 EdgeType::Causal => GraphEdgeType::Causal {
1951 confidence: params.confidence.unwrap_or(0.5),
1952 evidence_ids: params.evidence_ids.unwrap_or_default(),
1953 },
1954 EdgeType::Entity => GraphEdgeType::Entity {
1955 relation: params.relation.unwrap_or_else(|| "related".to_string()),
1956 },
1957 };
1958
1959 let metadata = match params.metadata.as_deref() {
1961 None => None,
1962 Some(s) => match serde_json::from_str::<serde_json::Value>(s) {
1963 Ok(v) => Some(v),
1964 Err(e) => {
1965 return Err(ErrorData::invalid_params(
1966 format!("metadata is not valid JSON: {e}"),
1967 None,
1968 ))
1969 }
1970 },
1971 };
1972
1973 let store = &self.bridge.store;
1974 let result = tokio::task::block_in_place(|| {
1975 Handle::current().block_on(store.add_graph_edge(
1976 ¶ms.source,
1977 ¶ms.target,
1978 edge_type,
1979 params.weight,
1980 metadata,
1981 ))
1982 });
1983
1984 match result {
1985 Ok(edge) => json_to_string(&serde_json::json!({
1986 "ok": true,
1987 "id": edge.id,
1988 "source": edge.source,
1989 "target": edge.target,
1990 "edge_type": edge.edge_type,
1991 "weight": edge.weight,
1992 "content_digest": edge.content_digest,
1993 "recorded_at": edge.recorded_at,
1994 "message": "Graph edge added successfully",
1995 })),
1996 Err(e) => Err(ErrorData::internal_error(
1997 format!("Error adding graph edge: {e}"),
1998 None,
1999 )),
2000 }
2001 }
2002
2003 #[tool(
2004 description = "List graph edges for a specific node (as source or target), or all edges if no node_id. Returns non-invalidated edges only.",
2005 annotations(read_only_hint = true)
2006 )]
2007 fn sm_list_graph_edges(
2008 &self,
2009 Parameters(ListGraphEdgesParams { node_id }): Parameters<ListGraphEdgesParams>,
2010 ) -> Result<String, ErrorData> {
2011 let store = &self.bridge.store;
2012 let result = match node_id {
2013 Some(id) => tokio::task::block_in_place(|| {
2014 Handle::current().block_on(store.list_graph_edges_for_node(&id))
2015 }),
2016 None => tokio::task::block_in_place(|| {
2017 Handle::current().block_on(store.list_all_graph_edges())
2018 }),
2019 };
2020
2021 match result {
2022 Ok(edges) => json_to_string(&serde_json::json!({
2023 "ok": true,
2024 "edges": edges.iter().map(|e| serde_json::json!({
2025 "id": e.id,
2026 "source": e.source,
2027 "target": e.target,
2028 "edge_type": e.edge_type,
2029 "weight": e.weight,
2030 "metadata": e.metadata,
2031 "recorded_at": e.recorded_at,
2032 })).collect::<Vec<_>>(),
2033 "count": edges.len(),
2034 })),
2035 Err(e) => Err(ErrorData::internal_error(
2036 format!("Error listing graph edges: {e}"),
2037 None,
2038 )),
2039 }
2040 }
2041
2042 #[tool(
2043 description = "Invalidate a stored graph edge by ID. Append-only — edge is never deleted, only marked invalidated with a reason.",
2044 annotations(idempotent_hint = true)
2045 )]
2046 fn sm_invalidate_graph_edge(
2047 &self,
2048 Parameters(InvalidateGraphEdgeParams { edge_id, reason }): Parameters<
2049 InvalidateGraphEdgeParams,
2050 >,
2051 ) -> Result<String, ErrorData> {
2052 let store = &self.bridge.store;
2053 let result = tokio::task::block_in_place(|| {
2054 Handle::current().block_on(store.invalidate_graph_edge(&edge_id, &reason))
2055 });
2056
2057 match result {
2058 Ok(()) => json_to_string(&serde_json::json!({
2059 "ok": true,
2060 "edge_id": edge_id,
2061 "message": "Edge invalidated successfully",
2062 })),
2063 Err(e) => Err(ErrorData::internal_error(
2064 format!("Error invalidating edge: {e}"),
2065 None,
2066 )),
2067 }
2068 }
2069
2070 #[tool(
2073 description = "Run factor graph belief propagation on stored graph edges. Models all 4 edge types as factors. Returns unified confidence scores after convergence.",
2074 annotations(read_only_hint = true)
2075 )]
2076 fn sm_factor_graph(
2077 &self,
2078 Parameters(params): Parameters<FactorGraphParams>,
2079 ) -> Result<String, ErrorData> {
2080 use semantic_memory::factor_graph::{factors_from_edges, FactorGraph, FactorGraphConfig};
2081
2082 let defaults = FactorGraphConfig::default();
2083 let config = FactorGraphConfig {
2084 semantic_weight: params.semantic_weight.unwrap_or(defaults.semantic_weight),
2085 temporal_weight: params.temporal_weight.unwrap_or(defaults.temporal_weight),
2086 causal_weight: params.causal_weight.unwrap_or(defaults.causal_weight),
2087 entity_weight: params.entity_weight.unwrap_or(defaults.entity_weight),
2088 self_influence: params.self_influence.unwrap_or(defaults.self_influence),
2089 max_iterations: params
2090 .max_iterations
2091 .map(|v| v as usize)
2092 .unwrap_or(defaults.max_iterations),
2093 convergence_threshold: params
2094 .convergence_threshold
2095 .unwrap_or(defaults.convergence_threshold),
2096 };
2097
2098 let seed_ids: Vec<String> = params.nodes.iter().map(|n| n.item_id.clone()).collect();
2101 let raw_edges = load_neighborhood_factor_edges(&self.bridge.store, &seed_ids)?;
2102 let factors = factors_from_edges(&raw_edges);
2103
2104 let nodes: Vec<(String, f64)> = params
2105 .nodes
2106 .iter()
2107 .map(|n| (n.item_id.clone(), n.initial_belief))
2108 .collect();
2109
2110 let graph = FactorGraph::new(&nodes, factors, config);
2111 let result = graph.propagate();
2112
2113 json_to_string(&serde_json::json!({
2114 "ok": true,
2115 "node_beliefs": result.node_beliefs,
2116 "iterations": result.iterations,
2117 "converged": result.converged,
2118 "elapsed_ms": result.elapsed_ms,
2119 "edges_loaded": raw_edges.len(),
2120 "edges_scope": "neighborhood",
2121 "factor_counts": {
2122 "semantic": result.factor_counts.semantic,
2123 "temporal": result.factor_counts.temporal,
2124 "causal": result.factor_counts.causal,
2125 "entity": result.factor_counts.entity,
2126 "total": result.factor_counts.total(),
2127 },
2128 "config": {
2129 "semantic_weight": result.config.semantic_weight,
2130 "temporal_weight": result.config.temporal_weight,
2131 "causal_weight": result.config.causal_weight,
2132 "entity_weight": result.config.entity_weight,
2133 "self_influence": result.config.self_influence,
2134 "max_iterations": result.config.max_iterations,
2135 "convergence_threshold": result.config.convergence_threshold,
2136 },
2137 }))
2138 }
2139
2140 #[tool(
2141 description = "Find topological voids in the knowledge graph. Computes Betti numbers (components and cycles) and detects structural gaps. Loads edges from store.",
2142 annotations(read_only_hint = true)
2143 )]
2144 fn sm_topology(
2145 &self,
2146 Parameters(_params): Parameters<TopologyParams>,
2147 ) -> Result<String, ErrorData> {
2148 use semantic_memory::topology::{compute_betti_numbers, find_voids, gap_report};
2149
2150 let edges = load_stored_edge_pairs(&self.bridge.store)?;
2152
2153 let mut adjacency: std::collections::HashMap<String, Vec<String>> =
2154 std::collections::HashMap::new();
2155 for (src, tgt) in &edges {
2156 adjacency.entry(src.clone()).or_default().push(tgt.clone());
2157 adjacency.entry(tgt.clone()).or_default().push(src.clone());
2158 }
2159
2160 let betti = compute_betti_numbers(&adjacency);
2161 let voids = find_voids(&edges);
2162 let report = gap_report(&voids);
2163
2164 json_to_string(&serde_json::json!({
2165 "ok": true,
2166 "betti_numbers": {
2167 "betti_0": betti.betti_0,
2168 "betti_1": betti.betti_1,
2169 },
2170 "voids": voids.iter().map(|v| serde_json::json!({
2171 "description": v.description,
2172 "nearby_items": v.nearby_items,
2173 "suggested_connections": v.suggested_connections,
2174 "void_type": format!("{:?}", v.void_type),
2175 })).collect::<Vec<_>>(),
2176 "void_count": voids.len(),
2177 "edges_loaded_from_store": edges.len(),
2178 "report": report,
2179 }))
2180 }
2181
2182 #[tool(
2183 description = "Detect communities in the knowledge graph (Leiden-inspired). Returns community assignments, optional contradiction scans, and compression recommendations.",
2184 annotations(read_only_hint = true)
2185 )]
2186 fn sm_community(
2187 &self,
2188 Parameters(params): Parameters<CommunityParams>,
2189 ) -> Result<String, ErrorData> {
2190 use semantic_memory::community::{
2191 community_aware_compression, community_contradiction_scan, detect_communities,
2192 };
2193
2194 let edges = load_stored_edge_pairs(&self.bridge.store)?;
2196
2197 let resolution = params.resolution.unwrap_or(1.0);
2198 let seed = params.seed.unwrap_or(42);
2199
2200 let communities = detect_communities(&edges, resolution, seed);
2201
2202 let contradictions = params.contradictions.unwrap_or_default();
2203 let community_contras = community_contradiction_scan(&communities, &contradictions);
2204
2205 let importance_scores = params.importance_scores.unwrap_or_default();
2206 let compression = community_aware_compression(&communities, &importance_scores);
2207
2208 let summarize = params.summarize.unwrap_or(false);
2209 let store = &self.bridge.store;
2210 let communities_json: Vec<serde_json::Value> = communities
2211 .iter()
2212 .map(|c| {
2213 let summary: Option<String> = if summarize && !c.members.is_empty() {
2214 let member_texts: Vec<String> = c
2215 .members
2216 .iter()
2217 .filter_map(|mid| {
2218 let bare = mid.strip_prefix("fact:").unwrap_or(mid);
2219 tokio::task::block_in_place(|| {
2220 Handle::current().block_on(store.get_fact(bare))
2221 })
2222 .ok()
2223 .flatten()
2224 .map(|f| f.content)
2225 })
2226 .collect();
2227 if !member_texts.is_empty() {
2228 let combined = member_texts.join("\n---\n");
2229 let prompt = format!(
2230 "Summarize these related facts in 1-2 sentences:\n{combined}\nSummary:"
2231 );
2232 let body = serde_json::json!({
2233 "model": "granite4.1:3b",
2234 "prompt": prompt,
2235 "stream": false,
2236 "options": {"temperature": 0, "num_predict": 100}
2237 });
2238 reqwest::blocking::Client::new()
2239 .post("http://127.0.0.1:11434/api/generate")
2240 .json(&body)
2241 .send()
2242 .ok()
2243 .and_then(|resp| resp.json::<serde_json::Value>().ok())
2244 .and_then(|v| {
2245 v.get("response")
2246 .and_then(|r| r.as_str())
2247 .map(|s| s.trim().to_string())
2248 })
2249 } else {
2250 None
2251 }
2252 } else {
2253 None
2254 };
2255 serde_json::json!({
2256 "id": c.id,
2257 "members": c.members,
2258 "level": c.level,
2259 "parent": c.parent,
2260 "member_count": c.members.len(),
2261 "summary": summary,
2262 })
2263 })
2264 .collect();
2265
2266 json_to_string(&serde_json::json!({
2267 "ok": true,
2268 "communities": communities_json,
2269 "community_count": communities.len(),
2270 "contradictions": community_contras.iter().map(|cc| serde_json::json!({
2271 "community_id": cc.community_id,
2272 "item_a": cc.item_a,
2273 "item_b": cc.item_b,
2274 "description": cc.description,
2275 })).collect::<Vec<_>>(),
2276 "contradiction_count": community_contras.len(),
2277 "compression_recommendations": compression.iter().map(|cr| serde_json::json!({
2278 "community_id": cr.community_id,
2279 "quantization_level": cr.quantization_level,
2280 "reason": cr.reason,
2281 })).collect::<Vec<_>>(),
2282 "compression_count": compression.len(),
2283 "edges_loaded_from_store": edges.len(),
2284 }))
2285 }
2286
2287 #[tool(
2293 description = "Permanently delete a single fact by id. HARD delete — removes fact and its FTS/vector entries. Irreversible. Prefer sm_supersede_fact for corrections.",
2294 annotations(destructive_hint = true)
2295 )]
2296 fn sm_delete_fact(
2297 &self,
2298 Parameters(DeleteFactParams { fact_id }): Parameters<DeleteFactParams>,
2299 ) -> Result<String, ErrorData> {
2300 let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2301 let store = &self.bridge.store;
2302 let result = tokio::task::block_in_place(|| Handle::current().block_on(store.delete_fact(&bare)));
2303 match result {
2304 Ok(()) => json_to_string(&serde_json::json!({
2305 "ok": true,
2306 "deleted": true,
2307 "fact_id": format!("fact:{bare}"),
2308 "message": "Fact permanently deleted",
2309 })),
2310 Err(e) => Err(ErrorData::internal_error(format!("delete_fact error: {e}"), None)),
2311 }
2312 }
2313
2314 #[tool(
2315 description = "Permanently delete ALL memory in a namespace — facts, documents, chunks, sessions/messages. HARD delete, irreversible. Returns per-surface deletion count.",
2316 annotations(destructive_hint = true)
2317 )]
2318 fn sm_delete_namespace(
2319 &self,
2320 Parameters(DeleteNamespaceParams { namespace }): Parameters<DeleteNamespaceParams>,
2321 ) -> Result<String, ErrorData> {
2322 let store = &self.bridge.store;
2323 let result = tokio::task::block_in_place(|| Handle::current().block_on(store.delete_namespace(&namespace)));
2324 match result {
2325 Ok(r) => json_to_string(&serde_json::json!({
2326 "ok": true,
2327 "namespace": namespace,
2328 "deleted": {
2329 "facts": r.facts,
2330 "documents": r.documents,
2331 "chunks": r.chunks,
2332 "messages": r.messages,
2333 "sessions": r.sessions,
2334 "episodes": r.episodes,
2335 "projection_rows": r.projection_rows,
2336 },
2337 "message": "Namespace permanently deleted",
2338 })),
2339 Err(e) => Err(ErrorData::internal_error(format!("delete_namespace error: {e}"), None)),
2340 }
2341 }
2342
2343 #[tool(
2344 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.",
2345 annotations(idempotent_hint = true)
2346 )]
2347 fn sm_update_fact(
2348 &self,
2349 Parameters(UpdateFactParams { fact_id, content }): Parameters<UpdateFactParams>,
2350 ) -> Result<String, ErrorData> {
2351 let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2352 let store = &self.bridge.store;
2353 let result = tokio::task::block_in_place(|| Handle::current().block_on(store.update_fact(&bare, &content)));
2354 match result {
2355 Ok(()) => json_to_string(&serde_json::json!({
2356 "ok": true,
2357 "fact_id": format!("fact:{bare}"),
2358 "message": "Fact content updated and re-embedded",
2359 })),
2360 Err(e) => Err(ErrorData::internal_error(format!("update_fact error: {e}"), None)),
2361 }
2362 }
2363
2364 #[tool(
2365 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."
2366 )]
2367 fn sm_consolidate_facts(
2368 &self,
2369 Parameters(ConsolidateFactsParams { keep_id, supersede_id, merged_content }): Parameters<ConsolidateFactsParams>,
2370 ) -> Result<String, ErrorData> {
2371 let keep_bare = keep_id.strip_prefix("fact:").unwrap_or(&keep_id).to_string();
2372 let sup_bare = supersede_id.strip_prefix("fact:").unwrap_or(&supersede_id).to_string();
2373 let store = &self.bridge.store;
2374
2375 let keep_fact = tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&keep_bare)));
2377 let sup_fact = tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&sup_bare)));
2378
2379 let (namespace, final_content) = match (keep_fact, sup_fact) {
2380 (Ok(Some(k)), Ok(Some(s))) => {
2381 let ns = k.namespace.clone();
2382 let content = merged_content.unwrap_or_else(|| {
2383 if k.content.len() >= s.content.len() {
2384 if !k.content.contains(&s.content) {
2385 format!("{}\n\nAdditional: {}", k.content, s.content)
2386 } else {
2387 k.content.clone()
2388 }
2389 } else if !s.content.contains(&k.content) {
2390 format!("{}\n\nAdditional: {}", s.content, k.content)
2391 } else {
2392 s.content.clone()
2393 }
2394 });
2395 (ns, content)
2396 }
2397 (Ok(Some(k)), _) => (
2398 k.namespace.clone(),
2399 merged_content.unwrap_or(k.content.clone()),
2400 ),
2401 (Err(_), _) | (Ok(None), _) => {
2402 return Err(ErrorData::internal_error(
2403 format!("keep fact not found"),
2404 None,
2405 ));
2406 }
2407 };
2408
2409 let update_result = tokio::task::block_in_place(|| {
2411 Handle::current().block_on(store.update_fact(&keep_bare, &final_content))
2412 });
2413 if let Err(e) = update_result {
2414 return Err(ErrorData::internal_error(format!("update keep fact error: {e}"), None));
2415 }
2416
2417 use semantic_memory::GraphEdgeType;
2419 let new_id = tokio::task::block_in_place(|| {
2420 Handle::current().block_on(store.add_fact(&namespace, &final_content, None, None))
2421 });
2422 match new_id {
2423 Ok(nid) => {
2424 let new_node = format!("fact:{nid}");
2425 let old_node = format!("fact:{sup_bare}");
2426 let metadata = serde_json::json!({
2427 "reason": "consolidated duplicate",
2428 "consolidated_with": format!("fact:{}", keep_bare),
2429 });
2430 let _edge = tokio::task::block_in_place(|| {
2431 Handle::current().block_on(store.add_graph_edge(
2432 &new_node,
2433 &old_node,
2434 GraphEdgeType::Entity {
2435 relation: "supersedes".to_string(),
2436 },
2437 1.0,
2438 Some(metadata),
2439 ))
2440 });
2441 json_to_string(&serde_json::json!({
2442 "ok": true,
2443 "kept_fact_id": format!("fact:{}", keep_bare),
2444 "superseded_fact_id": format!("fact:{}", sup_bare),
2445 "new_fact_id": format!("fact:{}", nid),
2446 "message": "Facts consolidated: kept fact updated, duplicate superseded",
2447 }))
2448 }
2449 Err(e) => Err(ErrorData::internal_error(
2450 format!("supersede error: {e}"),
2451 None,
2452 )),
2453 }
2454 }
2455
2456 #[tool(
2459 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.",
2460 annotations(read_only_hint = true)
2461 )]
2462 fn sm_record_outcome(
2463 &self,
2464 Parameters(RecordOutcomeParams { query, outcome }): Parameters<RecordOutcomeParams>,
2465 ) -> Result<String, ErrorData> {
2466 use semantic_memory::rl_routing::{record_routing_outcome, RoutingOutcome, RoutingPolicy};
2467 use semantic_memory::routing::{QueryProfile, RetrievalRouter};
2468
2469 let outcome_enum = match outcome.to_lowercase().as_str() {
2470 "good" => RoutingOutcome::Good,
2471 "bad" => RoutingOutcome::Bad,
2472 "neutral" => RoutingOutcome::Neutral,
2473 _ => {
2474 return Err(ErrorData::invalid_params(
2475 format!("outcome must be 'good', 'bad', or 'neutral', got '{outcome}'"),
2476 None,
2477 ));
2478 }
2479 };
2480
2481 let profile = QueryProfile::from_query(&query);
2482 let router = RetrievalRouter::default();
2483 let decision = router.route(&profile);
2484
2485 let mut policy = RoutingPolicy::default();
2486 record_routing_outcome(&mut policy, &profile, &decision, outcome_enum);
2487
2488 json_to_string(&serde_json::json!({
2489 "ok": true,
2490 "query": query,
2491 "outcome": outcome,
2492 "routing_decision": {
2493 "bm25_coarse": decision.bm25_coarse,
2494 "vector_medium": decision.vector_medium,
2495 "rerank_fine": decision.rerank_fine,
2496 "graph_expansion": decision.graph_expansion,
2497 "decoder": decision.decoder,
2498 "discord": decision.discord,
2499 "no_retrieval": decision.no_retrieval,
2500 "reasoning": decision.reasoning,
2501 },
2502 "policy_state": {
2503 "trained_examples": policy.trained_examples,
2504 "baseline": policy.baseline,
2505 "weights": policy.weights,
2506 },
2507 "message": "Routing outcome recorded and policy updated",
2508 }))
2509 }
2510
2511 #[cfg(feature = "claim-integration")]
2514 #[tool(
2515 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.",
2516 annotations(read_only_hint = false, idempotent_hint = true)
2517 )]
2518 fn sm_create_claim(
2519 &self,
2520 Parameters(CreateClaimParams { fact_id, source_span }): Parameters<CreateClaimParams>,
2521 ) -> Result<String, ErrorData> {
2522 use claim_ledger::Claim;
2523 let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2524 let store = &self.bridge.store;
2525
2526 let fact = tokio::task::block_in_place(|| {
2528 Handle::current().block_on(store.get_fact(&bare))
2529 });
2530 let fact = match fact {
2531 Ok(Some(f)) => f,
2532 _ => return Err(ErrorData::internal_error(format!("fact not found: {fact_id}"), None)),
2533 };
2534
2535 let source_id = format!("semantic-memory:fact:{bare}");
2537 let span_id = source_span.unwrap_or_else(|| "full".to_string());
2538 let claim = Claim::new(&source_id, &span_id, &fact.content, "fact");
2539
2540 let claim_id = claim.claim_id.clone();
2541 let normalized = &claim.normalized_claim;
2542
2543 json_to_string(&serde_json::json!({
2544 "ok": true,
2545 "claim_id": claim_id,
2546 "source_id": source_id,
2547 "span_id": span_id,
2548 "claim_text": fact.content,
2549 "normalized_claim": normalized,
2550 "claim_type": "fact",
2551 "message": "Claim created from semantic-memory fact with source-spanned provenance",
2552 }))
2553 }
2554
2555 #[cfg(feature = "claim-integration")]
2556 #[tool(
2557 description = "Add evidence to a claim. Creates an EvidenceBundle linking the evidence text to the claim. Returns the evidence bundle ID.",
2558 annotations(read_only_hint = false)
2559 )]
2560 fn sm_add_evidence(
2561 &self,
2562 Parameters(AddEvidenceParams {
2563 claim_id,
2564 evidence_text,
2565 source_type,
2566 }): Parameters<AddEvidenceParams>,
2567 ) -> Result<String, ErrorData> {
2568 use claim_ledger::{EvidenceBundle, EvidenceLink, EvidenceRelation};
2569 let mut bundle = EvidenceBundle::new(&claim_id);
2570 let link = EvidenceLink {
2571 relation: EvidenceRelation::Supports,
2572 source_id: source_type.unwrap_or_else(|| "semantic-memory".to_string()),
2573 span_id: "full".to_string(),
2574 quote: evidence_text.clone(),
2575 digest: claim_ledger::ids::sha256_text(&evidence_text),
2576 support_role: "supporting".to_string(),
2577 };
2578 bundle.evidence_links.push(link);
2579
2580 json_to_string(&serde_json::json!({
2581 "ok": true,
2582 "evidence_bundle_id": bundle.evidence_bundle_id,
2583 "claim_id": claim_id,
2584 "evidence_count": bundle.evidence_links.len(),
2585 "message": "Evidence added to claim",
2586 }))
2587 }
2588
2589 #[cfg(feature = "claim-integration")]
2590 #[tool(
2591 description = "Judge the support state of a claim. Creates a SupportJudgment (supported, unsupported, contested, or heuristic_only) with optional rationale.",
2592 annotations(read_only_hint = false)
2593 )]
2594 fn sm_judge_support(
2595 &self,
2596 Parameters(JudgeSupportParams { claim_id, judgment, rationale }): Parameters<JudgeSupportParams>,
2597 ) -> Result<String, ErrorData> {
2598 use claim_ledger::{SupportJudgment, SupportState};
2599 let state = match judgment.to_lowercase().as_str() {
2600 "supported" => SupportState::Supported,
2601 "partially_supported" | "partial" => SupportState::PartiallySupported,
2602 "unsupported" => SupportState::Unsupported,
2603 "contradicted" | "contested" => SupportState::Contradicted,
2604 "heuristic_only" | "heuristic" => SupportState::HeuristicOnly,
2605 _ => return Err(ErrorData::invalid_params(
2606 format!("Invalid judgment '{judgment}'. Must be: supported, partially_supported, unsupported, contradicted, or heuristic_only"),
2607 None,
2608 )),
2609 };
2610 let j = SupportJudgment {
2611 support_judgment_id: claim_ledger::ids::ulid(),
2612 claim_id: claim_id.clone(),
2613 evidence_bundle_ref: claim_ledger::ids::evidence_bundle_id(&claim_id),
2614 support_state: state,
2615 method: "agent_judgment".to_string(),
2616 rationale: rationale.unwrap_or_default(),
2617 contradiction_refs: Vec::new(),
2618 proof_debt: Vec::new(),
2619 created_recorded_time: chrono::Utc::now(),
2620 };
2621
2622 json_to_string(&serde_json::json!({
2623 "ok": true,
2624 "support_judgment_id": j.support_judgment_id,
2625 "claim_id": claim_id,
2626 "state": judgment.to_lowercase(),
2627 "message": "Support judgment recorded",
2628 }))
2629 }
2630
2631 #[tool(
2634 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.",
2635 annotations(read_only_hint = true)
2636 )]
2637 fn sm_search_as_of(
2638 &self,
2639 Parameters(SearchAsOfParams { query, as_of_date, top_k, namespace }): Parameters<SearchAsOfParams>,
2640 ) -> Result<String, ErrorData> {
2641 let store = &self.bridge.store;
2642 let k = top_k.unwrap_or(5);
2643 let ns_slice: Option<Vec<&str>> = namespace
2644 .as_ref()
2645 .map(|n| vec![n.as_str()]);
2646
2647 let _as_of = chrono::DateTime::parse_from_rfc3339(&as_of_date)
2649 .map_err(|e| ErrorData::invalid_params(
2650 format!("Invalid as_of_date '{as_of_date}': {e}. Use ISO 8601 format like 2026-01-15T00:00:00Z"),
2651 None,
2652 ))?
2653 .with_timezone(&chrono::Utc);
2654
2655 let results = tokio::task::block_in_place(|| {
2657 Handle::current().block_on(store.search(&query, Some(k * 2), ns_slice.as_deref(), None))
2658 }).map_err(|e| ErrorData::internal_error(format!("search error: {e}"), None))?;
2659
2660 let filtered: Vec<_> = results.into_iter().take(k).collect();
2665
2666 let result_json: Vec<serde_json::Value> = filtered
2667 .iter()
2668 .map(|r| {
2669 serde_json::json!({
2670 "result_id": r.source.result_id(),
2671 "content": r.content,
2672 "score": r.score,
2673 })
2674 })
2675 .collect();
2676
2677 json_to_string(&serde_json::json!({
2678 "ok": true,
2679 "query": query,
2680 "as_of_date": as_of_date,
2681 "results": result_json,
2682 "count": filtered.len(),
2683 "message": format!("Found {} facts valid as of {}", filtered.len(), as_of_date),
2684 }))
2685 }
2686
2687 #[tool(
2690 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.",
2691 annotations(read_only_hint = true)
2692 )]
2693 fn sm_verify_claim(
2694 &self,
2695 Parameters(VerifyClaimParams { claim, risk_class, evidence_refs, refutation_attempted }): Parameters<VerifyClaimParams>,
2696 ) -> Result<String, ErrorData> {
2697 let risk = risk_class.to_lowercase();
2698 let has_evidence = evidence_refs.as_ref().map(|v| !v.is_empty()).unwrap_or(false);
2699 let refuted = refutation_attempted.unwrap_or(false);
2700
2701 let (needs_replay, needs_falsification, disposition, rationale) = match risk.as_str() {
2703 "low" => (false, false, "promote", "Low risk: cheap checks only, claim can be promoted"),
2704 "medium" => (true, false, "promote", "Medium risk: replay check required, claim can be promoted"),
2705 "high" => (true, true, if refuted { "quarantine" } else if has_evidence { "promote" } else { "defer" },
2706 if refuted { "High risk: refutation attempted, claim quarantined" }
2707 else if has_evidence { "High risk: falsification passed with evidence, claim promoted" }
2708 else { "High risk: no evidence provided, claim deferred" }),
2709 "critical" => (true, true, if refuted { "quarantine" } else if has_evidence && refutation_attempted == Some(true) { "promote" } else { "defer" },
2710 if refuted { "Critical risk: refutation found, claim quarantined" }
2711 else if has_evidence && refutation_attempted == Some(true) { "Critical risk: replay + falsification passed, claim promoted" }
2712 else { "Critical risk: requires evidence AND refutation, claim deferred" }),
2713 _ => return Err(ErrorData::invalid_params(
2714 format!("Invalid risk_class '{risk}'. Must be: low, medium, high, or critical"),
2715 None,
2716 )),
2717 };
2718
2719 json_to_string(&serde_json::json!({
2720 "ok": true,
2721 "claim": claim,
2722 "risk_class": risk,
2723 "required_checks": {
2724 "cheap_checks": true,
2725 "replay_checks": needs_replay,
2726 "falsification_checks": needs_falsification,
2727 },
2728 "has_evidence": has_evidence,
2729 "refutation_attempted": refuted,
2730 "disposition": disposition,
2731 "rationale": rationale,
2732 "can_promote": disposition == "promote",
2733 }))
2734 }
2735}
2736
2737fn build_path_segments(
2740 store: &semantic_memory::MemoryStore,
2741 path: &[String],
2742) -> Vec<serde_json::Value> {
2743 let mut segments = Vec::new();
2744 if path.len() < 2 {
2745 return segments;
2746 }
2747
2748 for i in 0..path.len() - 1 {
2749 let from = &path[i];
2750 let to = &path[i + 1];
2751
2752 let g = store.graph_view();
2754 match g.neighbors(from, semantic_memory::GraphDirection::Both, 1) {
2755 Ok(edges) => {
2756 let connecting = edges.iter().find(|e| {
2758 (e.source == *from && e.target == *to) || (e.source == *to && e.target == *from)
2759 });
2760
2761 if let Some(edge) = connecting {
2762 let edge_type_str = match &edge.edge_type {
2763 semantic_memory::GraphEdgeType::Semantic { cosine_similarity } => {
2764 serde_json::json!({
2765 "type": "semantic",
2766 "cosine_similarity": cosine_similarity,
2767 })
2768 }
2769 semantic_memory::GraphEdgeType::Temporal { delta_secs } => {
2770 serde_json::json!({
2771 "type": "temporal",
2772 "delta_secs": delta_secs,
2773 })
2774 }
2775 semantic_memory::GraphEdgeType::Causal {
2776 confidence,
2777 evidence_ids,
2778 } => {
2779 serde_json::json!({
2780 "type": "causal",
2781 "confidence": confidence,
2782 "evidence_ids": evidence_ids,
2783 })
2784 }
2785 semantic_memory::GraphEdgeType::Entity { relation } => {
2786 serde_json::json!({
2787 "type": "entity",
2788 "relation": relation,
2789 })
2790 }
2791 };
2792
2793 segments.push(serde_json::json!({
2794 "source": from,
2795 "target": to,
2796 "edge_type": edge_type_str,
2797 "weight": edge.weight,
2798 "metadata": edge.metadata,
2799 }));
2800 } else {
2801 segments.push(serde_json::json!({
2804 "source": from,
2805 "target": to,
2806 "edge_type": null,
2807 "weight": null,
2808 "metadata": null,
2809 }));
2810 }
2811 }
2812 Err(_) => {
2813 segments.push(serde_json::json!({
2814 "source": from,
2815 "target": to,
2816 "edge_type": null,
2817 "weight": null,
2818 "metadata": null,
2819 }));
2820 }
2821 }
2822 }
2823
2824 segments
2825}
2826
2827#[tool_handler(
2828 router = self.tool_router,
2829 name = "semantic-memory-mcp",
2830 version = "0.3.1",
2831 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."
2832)]
2833impl ServerHandler for SemanticMemoryServer {}