1#![forbid(unsafe_code)]
8#![allow(clippy::significant_drop_tightening)]
9
10pub mod embedding_router;
11pub mod expansion;
12pub mod nlu;
13pub mod profiles;
14
15use async_trait::async_trait;
16
17use std::sync::Arc;
18
19use serde_json::{Value, json};
20use wm_cognitive::GanYingBus;
21use wm_core::{
22 Capability, Context, EffectRow, EpisodicCapturePolicy, EpisodicKind, EpisodicRecord, Galaxy,
23 Gana, Provenance, ProvenanceSource, Resource, Tool, ToolStats,
24};
25use wm_dispatch::{DispatchPipeline, ToolRegistry, ToolRegistryBuilder};
26use wm_governance::{DharmaGate, KarmaLedger, ResourceRules};
27use wm_memory::{
28 Association, AssociationStore, ConversationalSearch, Memory, MemoryQuery, MemoryStore,
29 RecallEngine, SearchEngine, VectorStore,
30};
31use wm_substrate::SubstrateMonitor;
32use wm_substrate::anomaly::AnomalyDetector;
33use wm_substrate::homeostatic::HomeostaticLoop;
34use wm_substrate::sensorimotor::{ReflexLoop, SensorimotorBus};
35
36use crate::expansion::common::{
37 bool_prop, fresh_write_galaxies, int_prop, memory_galaxy_reads, memory_galaxy_writes, num_prop,
38 schema, str_array_prop, str_prop,
39};
40
41const NLU_ABSTENTION_THRESHOLD: f64 = 0.15;
46
47fn capture_explicit_memory(
52 store: &MemoryStore,
53 memory: &Memory,
54 kind: EpisodicKind,
55 source: ProvenanceSource,
56 session_id: Option<uuid::Uuid>,
57 sequence: u64,
58) {
59 let record = explicit_memory_record(memory, kind, source, session_id, sequence);
60 if let Err(error) = store
61 .episodic()
62 .append_explicit(&record, EpisodicCapturePolicy::explicit_only())
63 {
64 tracing::warn!(
65 memory_id = %memory.metadata.id,
66 "episodic capture failed after legacy write: {error}"
67 );
68 }
69}
70
71fn explicit_memory_record(
72 memory: &Memory,
73 kind: EpisodicKind,
74 source: ProvenanceSource,
75 session_id: Option<uuid::Uuid>,
76 sequence: u64,
77) -> EpisodicRecord {
78 let resolved_kind = resolve_episodic_kind(memory, kind);
79 EpisodicRecord::new(
80 session_id,
81 sequence,
82 resolved_kind,
83 memory.content.clone(),
84 Provenance::new(source),
85 )
86 .with_id(memory.metadata.id)
87 .with_visibility(memory.metadata.is_private, memory.metadata.model_exclude)
88}
89
90fn resolve_episodic_kind(memory: &Memory, default: EpisodicKind) -> EpisodicKind {
93 let tags = &memory.metadata.tags;
94 if tags.iter().any(|t| t == "user") {
95 EpisodicKind::UserStatement
96 } else if tags.iter().any(|t| t == "assistant") {
97 EpisodicKind::AssistantResponse
98 } else {
99 default
100 }
101}
102
103fn capture_explicit_memories(
104 store: &MemoryStore,
105 memories: &[(Galaxy, Memory)],
106 kind: EpisodicKind,
107 source: ProvenanceSource,
108 session_id: Option<uuid::Uuid>,
109) {
110 if memories.is_empty() {
111 return;
112 }
113 let records: Vec<EpisodicRecord> = memories
114 .iter()
115 .enumerate()
116 .map(|(sequence, (_, memory))| {
117 explicit_memory_record(memory, kind, source, session_id, sequence as u64)
118 })
119 .collect();
120 if let Err(error) = store
121 .episodic()
122 .append_explicit_batch(&records, EpisodicCapturePolicy::explicit_only())
123 {
124 tracing::warn!("episodic batch capture failed after legacy write: {error}");
125 }
126}
127
128fn attestation_agent_id(ctx: &Context) -> String {
135 ctx.session_id
136 .map(|u| u.to_string())
137 .or_else(|| ctx.user_id.clone())
138 .unwrap_or_else(|| "local".to_string())
139}
140
141fn node_attestation_key() -> Option<String> {
145 std::env::var(wm_memory::attestation::ATTESTATION_KEY_ENV)
146 .ok()
147 .filter(|k| !k.trim().is_empty())
148}
149
150fn attest_created_memory(
157 store: &MemoryStore,
158 galaxy: Galaxy,
159 id: uuid::Uuid,
160 record_hash: &str,
161 ctx: &Context,
162 key_hex: Option<&str>,
163) -> (bool, Option<String>) {
164 let key_hex = match key_hex {
165 Some(k) if !k.trim().is_empty() => k,
166 _ => return (false, Some("node key unavailable".to_string())),
167 };
168 let agent_id = attestation_agent_id(ctx);
169 let timestamp = wm_core::time::now_unix_secs();
170 let payload = wm_memory::attestation::attestation_payload(
171 galaxy.db_name(),
172 &id.to_string(),
173 record_hash,
174 &agent_id,
175 timestamp,
176 );
177 let Some((public_key_hex, signature_hex)) =
178 wm_memory::attestation::sign_attestation(&payload, key_hex)
179 else {
180 tracing::warn!("creation attestation skipped for memory {id}: key material invalid");
181 return (false, Some("node key invalid".to_string()));
182 };
183 let entry = wm_memory::attestation::RecordAttestation {
184 domain: wm_memory::attestation::ATTESTATION_DOMAIN.to_string(),
185 galaxy: galaxy.db_name().to_string(),
186 memory_id: id.to_string(),
187 record_hash: record_hash.to_string(),
188 agent_id,
189 timestamp,
190 public_key_hex,
191 signature_hex,
192 };
193 if let Err(e) = store.record_attestation(galaxy, id, &entry) {
194 tracing::warn!("creation attestation write failed for memory {id}: {e}");
195 return (false, Some("attestation store write failed".to_string()));
196 }
197 (true, None)
198}
199
200pub struct MemoryCreateTool {
205 store: Arc<MemoryStore>,
206 search: Option<Arc<SearchEngine>>,
207 recall: Option<Arc<RecallEngine>>,
208 stats: ToolStats,
209 effects: EffectRow,
210 attestation_key: Option<String>,
214}
215
216impl MemoryCreateTool {
217 pub fn new(
218 store: Arc<MemoryStore>,
219 search: Option<Arc<SearchEngine>>,
220 recall: Option<Arc<RecallEngine>>,
221 ) -> Self {
222 Self {
223 store,
224 search,
225 recall,
226 stats: ToolStats::default(),
227 effects: EffectRow {
228 writes: fresh_write_galaxies(),
232 invokes: vec![Capability::MemoryWrite],
233 ..Default::default()
234 },
235 attestation_key: node_attestation_key(),
236 }
237 }
238
239 #[must_use]
242 pub fn with_attestation_key(
243 store: Arc<MemoryStore>,
244 search: Option<Arc<SearchEngine>>,
245 recall: Option<Arc<RecallEngine>>,
246 attestation_key: Option<String>,
247 ) -> Self {
248 let mut tool = Self::new(store, search, recall);
249 tool.attestation_key = attestation_key;
250 tool
251 }
252}
253
254#[async_trait]
255impl Tool for MemoryCreateTool {
256 fn name(&self) -> &str {
257 "memory.create"
258 }
259 fn gana(&self) -> Gana {
260 Gana::Encampment
261 }
262 fn effects(&self) -> &EffectRow {
263 &self.effects
264 }
265 fn input_schema(&self) -> Value {
266 schema(
267 &json!({
268 "content": str_prop("Memory content (text)"),
269 "galaxy": str_prop("Target galaxy (default codex)"),
270 "tags": str_array_prop("Optional tags"),
271 "title": str_prop("Optional human-readable title (envelope v2)"),
272 "topic": str_prop("Optional topic label for subject-scoped retrieval (envelope v2)"),
273 "importance": str_prop("Optional importance 0.0-1.0 (write gate applies class ceilings/floors when the class is recognized)"),
274 "source": str_prop("Authorship claim: user (user-dictated content, trust 1.0) | agent (default, trust 0.7) | other free-form class (trust 0.7)"),
275 }),
276 &["content"],
277 )
278 }
279 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
280 let content = args
281 .get("content")
282 .and_then(|v| v.as_str())
283 .ok_or_else(|| wm_core::CoreError::InvalidArgs("content (string) required".into()))?;
284 let galaxy_str = args
285 .get("galaxy")
286 .and_then(|v| v.as_str())
287 .unwrap_or("codex");
288 let galaxy = parse_galaxy(galaxy_str)?;
289 let tags: Vec<String> = args
290 .get("tags")
291 .and_then(|v| v.as_array())
292 .map(|a| {
293 a.iter()
294 .filter_map(|v| v.as_str().map(String::from))
295 .collect()
296 })
297 .unwrap_or_default();
298
299 if let Some(search) = &self.search {
300 if search.is_readonly() {
301 return Err(wm_core::CoreError::InvalidArgs(
302 "read-only mode: memory.create disabled (another process owns the index)"
303 .into(),
304 ));
305 }
306 }
307 let kinds = wm_memory::credential_shaped_content(content);
311 let warnings: Vec<String> = kinds
312 .iter()
313 .map(|k| {
314 format!(
315 "content looks like a credential ({k}) — {}",
316 wm_memory::CREDENTIAL_ADVICE
317 )
318 })
319 .collect();
320 let mut memory = Memory::new(galaxy, content.to_string());
321 memory.metadata.tags = tags;
322 memory.metadata.title = args
325 .get("title")
326 .and_then(Value::as_str)
327 .map(str::trim)
328 .filter(|s| !s.is_empty())
329 .map(String::from);
330 memory.metadata.topic = args
331 .get("topic")
332 .and_then(Value::as_str)
333 .map(str::trim)
334 .filter(|s| !s.is_empty())
335 .map(String::from);
336 if let Some(importance) = args.get("importance").and_then(Value::as_f64) {
340 memory.metadata.importance = importance as f32;
341 }
342 memory.metadata.class = wm_memory::typology::detect_class(content, &memory.metadata.tags);
343 memory.metadata.tier = memory.metadata.class.map_or(
344 wm_memory::memory::Tier::Working,
345 wm_memory::typology::initial_tier,
346 );
347 let claimed_source = args
353 .get("source")
354 .and_then(Value::as_str)
355 .map(str::trim)
356 .filter(|s| !s.is_empty());
357 let (source, trust) = match claimed_source {
358 Some("user") => ("user", 1.0),
359 Some(other) => (other, 0.7),
360 None => ("agent", 0.7),
361 };
362 memory.metadata.source = source.to_string();
363 memory.metadata.source_trust = trust;
364 let id = memory.metadata.id;
365
366 if let Some(recall) = &self.recall {
369 if let Err(e) = recall.store_with_embedding(galaxy, &memory) {
370 tracing::warn!("RecallEngine store_with_embedding failed for memory {id}: {e}");
371 self.store.put(galaxy, &memory)?;
373 if let Some(search) = &self.search {
374 if let Err(e) = (|| {
375 let mut writer = search.writer()?;
376 search.add_document(
377 &mut writer,
378 &id.to_string(),
379 galaxy.db_name(),
380 content,
381 &memory.metadata.tags,
382 memory.metadata.created_at.timestamp(),
383 )?;
384 search.commit(&mut writer)?;
385 Ok::<(), wm_core::CoreError>(())
386 })() {
387 tracing::warn!("Tantivy indexing failed for memory {id}: {e}");
388 }
389 }
390 }
391 } else {
392 self.store.put(galaxy, &memory)?;
393 if let Some(search) = &self.search {
395 if let Err(e) = (|| {
396 let mut writer = search.writer()?;
397 search.add_document(
398 &mut writer,
399 &id.to_string(),
400 galaxy.db_name(),
401 content,
402 &memory.metadata.tags,
403 memory.metadata.created_at.timestamp(),
404 )?;
405 search.commit(&mut writer)?;
406 Ok::<(), wm_core::CoreError>(())
407 })() {
408 tracing::warn!("Tantivy indexing failed for memory {id}: {e}");
409 }
410 }
411 }
412
413 capture_explicit_memory(
414 &self.store,
415 &memory,
416 EpisodicKind::Observation,
417 if source == "user" {
420 ProvenanceSource::User
421 } else {
422 ProvenanceSource::Agent
423 },
424 ctx.session_id,
425 0,
426 );
427
428 let (attested, attested_reason) = attest_created_memory(
432 &self.store,
433 galaxy,
434 id,
435 &memory.metadata.content_hash,
436 ctx,
437 self.attestation_key.as_deref(),
438 );
439
440 let mut response = json!({
441 "status": "success",
442 "id": id.to_string(),
443 "galaxy": galaxy.db_name(),
444 "content_hash": memory.metadata.content_hash,
445 "source": source,
446 "source_trust": trust,
447 "attested": attested,
448 });
449 if let Some(reason) = attested_reason {
450 response["attested_reason"] = json!(reason);
451 }
452 if !warnings.is_empty() {
453 response["warnings"] = json!(warnings);
454 }
455 Ok(response)
456 }
457 fn stats(&self) -> &ToolStats {
458 &self.stats
459 }
460}
461
462pub struct MemoryBatchCreateTool {
470 store: Arc<MemoryStore>,
471 search: Option<Arc<SearchEngine>>,
472 recall: Option<Arc<RecallEngine>>,
473 stats: ToolStats,
474 effects: EffectRow,
475 attestation_key: Option<String>,
478}
479
480impl MemoryBatchCreateTool {
481 pub fn new(
482 store: Arc<MemoryStore>,
483 search: Option<Arc<SearchEngine>>,
484 recall: Option<Arc<RecallEngine>>,
485 ) -> Self {
486 Self {
487 store,
488 search,
489 recall,
490 stats: ToolStats::default(),
491 effects: EffectRow {
492 writes: fresh_write_galaxies(),
493 invokes: vec![Capability::MemoryWrite],
494 ..Default::default()
495 },
496 attestation_key: node_attestation_key(),
497 }
498 }
499
500 #[must_use]
502 pub fn with_attestation_key(
503 store: Arc<MemoryStore>,
504 search: Option<Arc<SearchEngine>>,
505 recall: Option<Arc<RecallEngine>>,
506 attestation_key: Option<String>,
507 ) -> Self {
508 let mut tool = Self::new(store, search, recall);
509 tool.attestation_key = attestation_key;
510 tool
511 }
512}
513
514#[async_trait]
515impl Tool for MemoryBatchCreateTool {
516 fn name(&self) -> &str {
517 "memory.batch_create"
518 }
519 fn gana(&self) -> Gana {
520 Gana::Encampment
521 }
522 fn effects(&self) -> &EffectRow {
523 &self.effects
524 }
525 fn input_schema(&self) -> Value {
526 schema(
527 &json!({
528 "items": {
529 "type": "array",
530 "description": "Array of {content, galaxy?, tags?} objects",
531 "items": {
532 "type": "object",
533 "properties": {
534 "content": str_prop("Memory content (text)"),
535 "galaxy": str_prop("Target galaxy (default codex)"),
536 "tags": str_array_prop("Optional tags"),
537 "importance": str_prop("Optional importance 0.0-1.0 (write gate applies class ceilings/floors when the class is recognized)"),
538 },
539 "required": ["content"],
540 },
541 },
542 }),
543 &["items"],
544 )
545 }
546 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
547 let items = args
548 .get("items")
549 .and_then(|v| v.as_array())
550 .ok_or_else(|| wm_core::CoreError::InvalidArgs("items (array) required".into()))?;
551
552 if let Some(search) = &self.search {
553 if search.is_readonly() {
554 return Err(wm_core::CoreError::InvalidArgs(
555 "read-only mode: memory.batch_create disabled (another process owns the index)"
556 .into(),
557 ));
558 }
559 }
560
561 let mut ids: Vec<String> = Vec::new();
562 let mut all_items_user_claimed = true;
567 let mut cred_kinds: Vec<&'static str> = Vec::new();
570 let mut writer_guard = if self.recall.is_none() {
574 if let Some(search) = &self.search {
575 Some(search.writer()?)
576 } else {
577 None
578 }
579 } else {
580 None
581 };
582
583 let mut memories: Vec<(Galaxy, Memory)> = Vec::new();
585
586 for item in items {
587 let content = item
588 .get("content")
589 .and_then(|v| v.as_str())
590 .ok_or_else(|| {
591 wm_core::CoreError::InvalidArgs("each item needs content (string)".into())
592 })?;
593 let galaxy_str = item
594 .get("galaxy")
595 .and_then(|v| v.as_str())
596 .unwrap_or("codex");
597 let galaxy = parse_galaxy(galaxy_str)?;
598 let tags: Vec<String> = item
599 .get("tags")
600 .and_then(|v| v.as_array())
601 .map(|a| {
602 a.iter()
603 .filter_map(|v| v.as_str().map(String::from))
604 .collect()
605 })
606 .unwrap_or_default();
607
608 let mut memory = Memory::new(galaxy, content.to_string());
609 memory.metadata.tags = tags;
610 if let Some(importance) = item.get("importance").and_then(Value::as_f64) {
616 memory.metadata.importance = importance as f32;
617 }
618 memory.metadata.class =
619 wm_memory::typology::detect_class(content, &memory.metadata.tags);
620 memory.metadata.tier = memory.metadata.class.map_or(
621 wm_memory::memory::Tier::Working,
622 wm_memory::typology::initial_tier,
623 );
624 let claimed_source = item
628 .get("source")
629 .and_then(Value::as_str)
630 .map(str::trim)
631 .filter(|s| !s.is_empty());
632 let (source, trust) = match claimed_source {
633 Some("user") => ("user", 1.0),
634 Some(other) => (other, 0.7),
635 None => ("agent", 0.7),
636 };
637 if source != "user" {
638 all_items_user_claimed = false;
639 }
640 memory.metadata.source = source.to_string();
641 memory.metadata.source_trust = trust;
642 let id = memory.metadata.id;
643 ids.push(id.to_string());
644 for k in wm_memory::credential_shaped_content(content) {
645 if !cred_kinds.contains(&k) {
646 cred_kinds.push(k);
647 }
648 }
649 memories.push((galaxy, memory));
650 }
651
652 if let Some(recall) = &self.recall {
654 let entries: Vec<(Galaxy, &Memory)> = memories.iter().map(|(g, m)| (*g, m)).collect();
655 match recall.store_batch_with_embedding(&entries) {
656 Ok(n) => {
657 tracing::info!("batch_create: embedded {n} memories in single batch");
658 }
659 Err(e) => {
660 tracing::warn!(
661 "batch_create: store_batch_with_embedding failed ({e}), falling back to per-item"
662 );
663 let mut fallback_writer = if writer_guard.is_none() {
667 if let Some(search) = &self.search {
668 search.writer().ok()
669 } else {
670 None
671 }
672 } else {
673 None
674 };
675 for (galaxy, memory) in &memories {
676 self.store.put(*galaxy, memory)?;
677 let writer_slot = writer_guard.as_mut().or(fallback_writer.as_mut());
678 if let Some(guard) = writer_slot {
679 if let Some(search) = &self.search {
680 if let Err(e) = search.add_document(
681 guard,
682 &memory.metadata.id.to_string(),
683 galaxy.db_name(),
684 &memory.content,
685 &memory.metadata.tags,
686 memory.metadata.created_at.timestamp(),
687 ) {
688 tracing::warn!(
689 "Tantivy indexing failed for memory {}: {e}",
690 memory.metadata.id
691 );
692 }
693 }
694 }
695 }
696 if let Some(mut guard) = fallback_writer {
698 if let Some(search) = &self.search {
699 if let Err(e) = search.commit(&mut guard) {
700 tracing::warn!("Tantivy fallback commit failed: {e}");
701 }
702 }
703 }
704 }
705 }
706 } else {
707 for (galaxy, memory) in &memories {
709 self.store.put(*galaxy, memory)?;
710 if let Some(ref mut guard) = writer_guard {
711 if let Some(search) = &self.search {
712 if let Err(e) = search.add_document(
713 &mut *guard,
714 &memory.metadata.id.to_string(),
715 galaxy.db_name(),
716 &memory.content,
717 &memory.metadata.tags,
718 memory.metadata.created_at.timestamp(),
719 ) {
720 tracing::warn!(
721 "Tantivy indexing failed for memory {}: {e}",
722 memory.metadata.id
723 );
724 }
725 }
726 }
727 }
728 }
729
730 if let Some(ref mut guard) = writer_guard {
732 if let Some(search) = &self.search {
733 if let Err(e) = search.commit(&mut *guard) {
734 tracing::warn!("Tantivy batch commit failed: {e}");
735 }
736 }
737 }
738
739 capture_explicit_memories(
740 &self.store,
741 &memories,
742 EpisodicKind::Observation,
743 if all_items_user_claimed {
746 ProvenanceSource::User
747 } else {
748 ProvenanceSource::Agent
749 },
750 ctx.session_id,
751 );
752
753 let mut attested_count = 0usize;
756 for (galaxy, memory) in &memories {
757 let (ok, _) = attest_created_memory(
758 &self.store,
759 *galaxy,
760 memory.metadata.id,
761 &memory.metadata.content_hash,
762 ctx,
763 self.attestation_key.as_deref(),
764 );
765 attested_count += usize::from(ok);
766 }
767
768 let mut response = json!({
769 "status": "success",
770 "count": ids.len(),
771 "ids": ids,
772 "attested_count": attested_count,
773 });
774 if !cred_kinds.is_empty() {
775 response["warnings"] = json!(
776 cred_kinds
777 .iter()
778 .map(|k| format!(
779 "some items look like credentials ({k}) — {}",
780 wm_memory::CREDENTIAL_ADVICE
781 ))
782 .collect::<Vec<String>>()
783 );
784 }
785 Ok(response)
786 }
787 fn stats(&self) -> &ToolStats {
788 &self.stats
789 }
790}
791
792pub struct MemoryReadTool {
796 store: Arc<MemoryStore>,
797 stats: ToolStats,
798 effects: EffectRow,
799}
800
801impl MemoryReadTool {
802 pub fn new(store: Arc<MemoryStore>) -> Self {
803 Self {
804 store,
805 stats: ToolStats::default(),
806 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
807 }
808 }
809}
810
811#[async_trait]
812impl Tool for MemoryReadTool {
813 fn name(&self) -> &str {
814 "memory.read"
815 }
816 fn gana(&self) -> Gana {
817 Gana::WinnowingBasket
818 }
819 fn effects(&self) -> &EffectRow {
820 &self.effects
821 }
822 fn input_schema(&self) -> Value {
823 schema(
824 &json!({
825 "id": str_prop("Memory UUID"),
826 "galaxy": str_prop("Galaxy containing the memory (default codex)"),
827 }),
828 &["id"],
829 )
830 }
831 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
832 let id_str = args
833 .get("id")
834 .and_then(|v| v.as_str())
835 .ok_or_else(|| wm_core::CoreError::InvalidArgs("id (string) required".into()))?;
836 let id = uuid::Uuid::parse_str(id_str)
837 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid UUID: {e}")))?;
838 let galaxy_str = args
839 .get("galaxy")
840 .and_then(|v| v.as_str())
841 .unwrap_or("codex");
842 let galaxy = parse_galaxy(galaxy_str)?;
843
844 match self.store.get(galaxy, id)? {
845 Some(memory) => {
846 if memory.metadata.is_private {
847 return Ok(json!({
850 "status": "not_found",
851 "id": id_str,
852 "galaxy": galaxy.db_name(),
853 }));
854 }
855 Ok(json!({
856 "status": "success",
857 "id": memory.metadata.id.to_string(),
858 "galaxy": memory.metadata.galaxy.db_name(),
859 "content": memory.content,
860 "tags": memory.metadata.tags,
861 "created_at": memory.metadata.created_at.to_rfc3339(),
862 }))
863 }
864 None => Ok(json!({
865 "status": "not_found",
866 "id": id_str,
867 "galaxy": galaxy.db_name(),
868 })),
869 }
870 }
871 fn stats(&self) -> &ToolStats {
872 &self.stats
873 }
874}
875
876pub struct MemoryListTool {
880 store: Arc<MemoryStore>,
881 stats: ToolStats,
882 effects: EffectRow,
883}
884
885impl MemoryListTool {
886 pub fn new(store: Arc<MemoryStore>) -> Self {
887 Self {
888 store,
889 stats: ToolStats::default(),
890 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
891 }
892 }
893}
894
895#[async_trait]
896impl Tool for MemoryListTool {
897 fn name(&self) -> &str {
898 "memory.list"
899 }
900 fn gana(&self) -> Gana {
901 Gana::WinnowingBasket
902 }
903 fn effects(&self) -> &EffectRow {
904 &self.effects
905 }
906 fn input_schema(&self) -> Value {
907 schema(
908 &json!({
909 "galaxy": str_prop("Galaxy to list (default codex)"),
910 "limit": int_prop("Maximum entries (default 20)"),
911 "offset": int_prop("Skip this many matching entries before returning (default 0)"),
912 "exclude_tags": {
913 "type": "array",
914 "items": {"type": "string"},
915 "description": "Drop memories carrying any of these tags",
916 },
917 }),
918 &[],
919 )
920 }
921 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
922 let galaxy_str = args
923 .get("galaxy")
924 .and_then(|v| v.as_str())
925 .unwrap_or("codex");
926 let limit = args
927 .get("limit")
928 .and_then(serde_json::Value::as_u64)
929 .unwrap_or(20) as usize;
930 let offset = args
931 .get("offset")
932 .and_then(serde_json::Value::as_u64)
933 .unwrap_or(0) as usize;
934 let exclude_tags: Vec<String> = args
935 .get("exclude_tags")
936 .and_then(|v| v.as_array())
937 .map(|arr| {
938 arr.iter()
939 .filter_map(|t| t.as_str().map(String::from))
940 .collect()
941 })
942 .unwrap_or_default();
943 let galaxy = parse_galaxy(galaxy_str)?;
944
945 let memories = self.store.scan(galaxy, 10_000)?;
949 let total = self.store.count(galaxy)?;
950
951 let visible: Vec<&wm_memory::Memory> = memories
952 .iter()
953 .filter(|m| crate::expansion::common::mcp_visible(m))
954 .filter(|m| crate::expansion::common::validity_visible(m))
955 .filter(|m| {
956 !exclude_tags
957 .iter()
958 .any(|t| m.metadata.tags.iter().any(|mt| mt == t))
959 })
960 .collect();
961 let entries: Vec<Value> = visible
962 .iter()
963 .skip(offset)
964 .take(limit)
965 .map(|m| {
966 json!({
967 "id": m.metadata.id.to_string(),
968 "content_preview": m.content.chars().take(80).collect::<String>(),
969 "tags": m.metadata.tags,
970 "created_at": m.metadata.created_at.to_rfc3339(),
971 })
972 })
973 .collect();
974
975 Ok(json!({
976 "status": "success",
977 "galaxy": galaxy.db_name(),
978 "total": total,
979 "matched": visible.len(),
980 "offset": offset,
981 "returned": entries.len(),
982 "memories": entries,
983 }))
984 }
985 fn stats(&self) -> &ToolStats {
986 &self.stats
987 }
988}
989
990pub struct GnosisTool {
994 store: Arc<MemoryStore>,
995 tool_count: usize,
996 stats: ToolStats,
997 effects: EffectRow,
998}
999
1000impl GnosisTool {
1001 pub fn new(store: Arc<MemoryStore>) -> Self {
1002 Self {
1003 store,
1004 tool_count: 0,
1005 stats: ToolStats::default(),
1006 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
1007 }
1008 }
1009
1010 pub fn with_tool_count(store: Arc<MemoryStore>, tool_count: usize) -> Self {
1012 Self {
1013 store,
1014 tool_count,
1015 stats: ToolStats::default(),
1016 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
1017 }
1018 }
1019}
1020
1021#[async_trait]
1022impl Tool for GnosisTool {
1023 fn name(&self) -> &str {
1024 "gnosis"
1025 }
1026 fn gana(&self) -> Gana {
1027 Gana::Root
1028 }
1029 fn effects(&self) -> &EffectRow {
1030 &self.effects
1031 }
1032 async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
1033 let mut galaxy_stats = serde_json::Map::new();
1034 for galaxy in Galaxy::all() {
1035 let count = self.store.count(galaxy).unwrap_or(0);
1036 if count > 0 {
1037 galaxy_stats.insert(galaxy.db_name().to_string(), json!(count));
1038 }
1039 }
1040
1041 Ok(json!({
1042 "status": "success",
1043 "version": env!("CARGO_PKG_VERSION"),
1044 "store_path": self.store.path().display().to_string(),
1045 "brain_wave": format!("{:?}", ctx.brain_wave),
1046 "available_tools": self.tool_count,
1047 "galaxies_with_data": galaxy_stats.len(),
1048 "galaxy_counts": galaxy_stats,
1049 "ganas": Gana::COUNT,
1050 "galaxies": Galaxy::COUNT,
1051 }))
1052 }
1053 fn stats(&self) -> &ToolStats {
1054 &self.stats
1055 }
1056}
1057
1058pub struct ToolsListTool {
1062 registry: Arc<ToolRegistry>,
1063 stats: ToolStats,
1064 effects: EffectRow,
1065}
1066
1067impl ToolsListTool {
1068 #[must_use]
1069 pub fn new(registry: Arc<ToolRegistry>) -> Self {
1070 Self {
1071 registry,
1072 stats: ToolStats::default(),
1073 effects: EffectRow::pure(),
1074 }
1075 }
1076}
1077
1078#[async_trait]
1079impl Tool for ToolsListTool {
1080 fn name(&self) -> &str {
1081 "tools.list"
1082 }
1083 fn gana(&self) -> Gana {
1084 Gana::Ghost
1085 }
1086 fn effects(&self) -> &EffectRow {
1087 &self.effects
1088 }
1089 async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
1090 let available = self.registry.available_in(ctx.brain_wave);
1091 let tools: Vec<Value> = available
1092 .iter()
1093 .map(|t| {
1094 let effects = t.effects();
1097 json!({
1098 "name": t.name(),
1099 "gana": format!("{:?}", t.gana()),
1100 "description": t.description(),
1101 "input_schema": t.input_schema(),
1102 "annotations": {
1103 "readOnlyHint": effects.writes.is_empty(),
1104 "destructiveHint": effects.destructive,
1105 },
1106 })
1107 })
1108 .collect();
1109 Ok(json!({
1110 "status": "success",
1111 "brain_wave": format!("{:?}", ctx.brain_wave),
1112 "total": tools.len(),
1113 "tools": tools,
1114 }))
1115 }
1116 fn stats(&self) -> &ToolStats {
1117 &self.stats
1118 }
1119}
1120
1121pub struct MemoryDeleteTool {
1133 store: Arc<MemoryStore>,
1134 search: Option<Arc<SearchEngine>>,
1135 stats: ToolStats,
1136 effects: EffectRow,
1137}
1138
1139impl MemoryDeleteTool {
1140 pub fn new(store: Arc<MemoryStore>, search: Option<Arc<SearchEngine>>) -> Self {
1141 Self {
1142 store,
1143 search,
1144 stats: ToolStats::default(),
1145 effects: EffectRow {
1146 writes: memory_galaxy_writes(),
1149 reads: memory_galaxy_reads(),
1150 invokes: vec![Capability::MemoryWrite],
1151 destructive: true,
1152 ..Default::default()
1153 },
1154 }
1155 }
1156}
1157
1158#[async_trait]
1159impl Tool for MemoryDeleteTool {
1160 fn name(&self) -> &str {
1161 "memory.delete"
1162 }
1163 fn gana(&self) -> Gana {
1164 Gana::Encampment
1165 }
1166 fn effects(&self) -> &EffectRow {
1167 &self.effects
1168 }
1169 fn input_schema(&self) -> Value {
1170 schema(
1171 &json!({
1172 "id": str_prop("Memory UUID"),
1173 "galaxy": str_prop("Galaxy containing the memory (optional; when omitted the id is resolved across all memory galaxies)"),
1174 "confirm": bool_prop("Required — memory.delete is destructive"),
1175 }),
1176 &["id", "confirm"],
1177 )
1178 }
1179 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1180 let id_str = args
1181 .get("id")
1182 .and_then(|v| v.as_str())
1183 .ok_or_else(|| wm_core::CoreError::InvalidArgs("id (string) required".into()))?;
1184 let id = uuid::Uuid::parse_str(id_str)
1185 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid UUID: {e}")))?;
1186
1187 if let Some(search) = &self.search {
1188 if search.is_readonly() {
1189 return Err(wm_core::CoreError::InvalidArgs(
1190 "read-only mode: memory.delete disabled (another process owns the index)"
1191 .into(),
1192 ));
1193 }
1194 }
1195
1196 let targets: Vec<Galaxy> = match args.get("galaxy").and_then(|v| v.as_str()) {
1197 Some(g) => vec![parse_galaxy(g)?],
1198 None => Galaxy::memory_galaxies().to_vec(),
1199 };
1200
1201 let mut deleted_from: Vec<&str> = Vec::new();
1202 for galaxy in targets {
1203 if self.store.delete(galaxy, id)? {
1204 deleted_from.push(galaxy.db_name());
1205 }
1206 }
1207
1208 if !deleted_from.is_empty() {
1210 if let Some(search) = &self.search {
1211 if let Err(e) = (|| {
1212 let mut writer = search.writer()?;
1213 search.delete_document(&mut writer, id_str)?;
1214 search.commit(&mut writer)?;
1215 Ok::<(), wm_core::CoreError>(())
1216 })() {
1217 tracing::warn!("Tantivy de-indexing failed for memory {id_str}: {e}");
1218 }
1219 }
1220 }
1221
1222 if deleted_from.is_empty() {
1223 return Ok(json!({
1224 "status": "not_found",
1225 "id": id_str,
1226 "hint": "id not found in any memory galaxy; pass an explicit galaxy to target one"
1227 }));
1228 }
1229
1230 let mut body = serde_json::Map::new();
1231 body.insert("status".into(), json!("success"));
1232 body.insert("id".into(), json!(id_str));
1233 if args.get("galaxy").and_then(|v| v.as_str()).is_some() {
1234 body.insert("galaxy".into(), json!(deleted_from[0]));
1235 }
1236 body.insert(
1237 "galaxies".into(),
1238 json!(deleted_from.iter().map(|g| json!(g)).collect::<Vec<_>>()),
1239 );
1240 body.insert("deleted".into(), json!(deleted_from.len()));
1241 Ok(Value::Object(body))
1242 }
1243 fn stats(&self) -> &ToolStats {
1244 &self.stats
1245 }
1246}
1247
1248pub struct MemoryBatchDeleteTool {
1256 store: Arc<MemoryStore>,
1257 search: Option<Arc<SearchEngine>>,
1258 stats: ToolStats,
1259 effects: EffectRow,
1260}
1261
1262impl MemoryBatchDeleteTool {
1263 pub fn new(store: Arc<MemoryStore>, search: Option<Arc<SearchEngine>>) -> Self {
1264 Self {
1265 store,
1266 search,
1267 stats: ToolStats::default(),
1268 effects: EffectRow {
1269 writes: memory_galaxy_writes(),
1270 reads: memory_galaxy_reads(),
1271 invokes: vec![Capability::MemoryWrite],
1272 destructive: true,
1273 ..Default::default()
1274 },
1275 }
1276 }
1277}
1278
1279#[async_trait]
1280impl Tool for MemoryBatchDeleteTool {
1281 fn name(&self) -> &str {
1282 "memory.batch_delete"
1283 }
1284 fn gana(&self) -> Gana {
1285 Gana::Encampment
1286 }
1287 fn effects(&self) -> &EffectRow {
1288 &self.effects
1289 }
1290 fn input_schema(&self) -> Value {
1291 schema(
1292 &json!({
1293 "ids": {"type": "array", "items": {"type": "string"},
1294 "description": "Memory UUIDs to delete (max 200000)"},
1295 "confirm": bool_prop("Required — memory.batch_delete is destructive"),
1296 }),
1297 &["ids", "confirm"],
1298 )
1299 }
1300 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1301 const MAX_IDS: usize = 200_000;
1302 if !args
1303 .get("confirm")
1304 .and_then(serde_json::Value::as_bool)
1305 .unwrap_or(false)
1306 {
1307 return Err(wm_core::CoreError::InvalidArgs(
1308 "confirm (bool) required — memory.batch_delete is destructive".into(),
1309 ));
1310 }
1311 let ids: Vec<String> = args
1312 .get("ids")
1313 .and_then(|v| v.as_array())
1314 .map(|a| {
1315 a.iter()
1316 .filter_map(|v| v.as_str().map(String::from))
1317 .collect()
1318 })
1319 .ok_or_else(|| {
1320 wm_core::CoreError::InvalidArgs("ids (array of UUID strings) required".into())
1321 })?;
1322 if ids.is_empty() {
1323 return Ok(json!({"status": "success", "requested": 0, "deleted": 0, "not_found": 0}));
1324 }
1325 if ids.len() > MAX_IDS {
1326 return Err(wm_core::CoreError::InvalidArgs(format!(
1327 "ids capped at {MAX_IDS}; split the batch"
1328 )));
1329 }
1330
1331 if let Some(search) = &self.search {
1332 if search.is_readonly() {
1333 return Err(wm_core::CoreError::InvalidArgs(
1334 "read-only mode: memory.batch_delete disabled (another process owns the index)"
1335 .into(),
1336 ));
1337 }
1338 }
1339
1340 let targets: Vec<Galaxy> = Galaxy::memory_galaxies().to_vec();
1341 let mut deleted_ids: Vec<(String, Vec<&str>)> = Vec::new();
1342 let mut not_found: usize = 0;
1343 for id_str in &ids {
1344 let Ok(id) = uuid::Uuid::parse_str(id_str) else {
1345 not_found += 1;
1346 continue;
1347 };
1348 let mut deleted_from: Vec<&str> = Vec::new();
1349 for galaxy in targets.iter().copied() {
1350 if self.store.delete(galaxy, id)? {
1351 deleted_from.push(galaxy.db_name());
1352 }
1353 }
1354 if deleted_from.is_empty() {
1355 not_found += 1;
1356 } else {
1357 deleted_ids.push((id_str.clone(), deleted_from));
1358 }
1359 }
1360
1361 if !deleted_ids.is_empty() {
1363 if let Some(search) = &self.search {
1364 if let Err(e) = (|| {
1365 let mut writer = search.writer()?;
1366 for (id_str, _) in &deleted_ids {
1367 search.delete_document(&mut writer, id_str)?;
1368 }
1369 search.commit(&mut writer)?;
1370 Ok::<(), wm_core::CoreError>(())
1371 })() {
1372 tracing::warn!(
1373 "Tantivy batch de-indexing failed ({} ids): {e}",
1374 deleted_ids.len()
1375 );
1376 }
1377 }
1378 }
1379
1380 Ok(json!({
1381 "status": "success",
1382 "requested": ids.len(),
1383 "deleted": deleted_ids.len(),
1384 "not_found": not_found,
1385 }))
1386 }
1387 fn stats(&self) -> &ToolStats {
1388 &self.stats
1389 }
1390}
1391
1392pub struct MemoryQueryTool {
1396 store: Arc<MemoryStore>,
1397 stats: ToolStats,
1398 effects: EffectRow,
1399}
1400
1401impl MemoryQueryTool {
1402 pub fn new(store: Arc<MemoryStore>) -> Self {
1403 Self {
1404 store,
1405 stats: ToolStats::default(),
1406 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
1407 }
1408 }
1409}
1410
1411#[async_trait]
1412impl Tool for MemoryQueryTool {
1413 fn name(&self) -> &str {
1414 "memory.query"
1415 }
1416 fn gana(&self) -> Gana {
1417 Gana::WinnowingBasket
1418 }
1419 fn effects(&self) -> &EffectRow {
1420 &self.effects
1421 }
1422 fn input_schema(&self) -> Value {
1423 schema(
1424 &json!({
1425 "query": str_prop("Case-insensitive substring filter over content (literal match). For tokenized, ranked full-text retrieval use memory.search"),
1426 "galaxy": str_prop("Galaxy to query (default codex)"),
1427 "tags": str_array_prop("Filter: memories with all of these tags"),
1428 "min_importance": num_prop("Filter: minimum importance (0-1)"),
1429 "max_importance": num_prop("Filter: maximum importance (0-1)"),
1430 "created_after": str_prop("Filter: only memories created at or after this RFC 3339 timestamp (e.g. 2026-08-01T00:00:00Z)"),
1431 "created_before": str_prop("Filter: only memories created at or before this RFC 3339 timestamp"),
1432 "limit": int_prop("Maximum entries (default 50)"),
1433 }),
1434 &[],
1435 )
1436 }
1437 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1438 let galaxy_str = args
1439 .get("galaxy")
1440 .and_then(|v| v.as_str())
1441 .unwrap_or("codex");
1442 let galaxy = parse_galaxy(galaxy_str)?;
1443 let limit = args
1444 .get("limit")
1445 .and_then(serde_json::Value::as_u64)
1446 .unwrap_or(50) as usize;
1447 let mut query = MemoryQuery::new().with_limit(limit);
1448 if let Some(text) = args
1449 .get("query")
1450 .and_then(serde_json::Value::as_str)
1451 .map(str::trim)
1452 .filter(|s| !s.is_empty())
1453 {
1454 query = query.with_content_substring(text);
1455 }
1456 if let Some(tags) = args.get("tags").and_then(|v| v.as_array()) {
1457 let tag_list: Vec<String> = tags
1458 .iter()
1459 .filter_map(|v| v.as_str().map(String::from))
1460 .collect();
1461 if !tag_list.is_empty() {
1462 query = query.with_tags(tag_list);
1463 }
1464 }
1465 let parse_bound = |name: &str| -> wm_core::Result<Option<chrono::DateTime<chrono::Utc>>> {
1468 match args.get(name).and_then(|v| v.as_str()) {
1469 Some(s) if !s.trim().is_empty() => chrono::DateTime::parse_from_rfc3339(s.trim())
1470 .map(|t| Some(t.with_timezone(&chrono::Utc)))
1471 .map_err(|_| {
1472 wm_core::CoreError::InvalidArgs(format!(
1473 "{name} must be an RFC 3339 timestamp (e.g. \"2026-08-01T00:00:00Z\"), got: {s}"
1474 ))
1475 }),
1476 _ => Ok(None),
1477 }
1478 };
1479 let created_after = parse_bound("created_after")?;
1480 let created_before = parse_bound("created_before")?;
1481 if let Some(after) = created_after {
1482 query = query.with_created_after(after);
1483 }
1484 if let Some(before) = created_before {
1485 query = query.with_created_before(before);
1486 }
1487
1488 let min_imp = args
1489 .get("min_importance")
1490 .and_then(serde_json::Value::as_f64);
1491 let max_imp = args
1492 .get("max_importance")
1493 .and_then(serde_json::Value::as_f64);
1494 if let (Some(min), Some(max)) = (min_imp, max_imp) {
1495 query = query.with_importance_range(min as f32, max as f32);
1496 } else if let Some(min) = min_imp {
1497 query = query.with_importance_range(min as f32, 1.0);
1498 }
1499
1500 let memories = self.store.query(galaxy, &query)?;
1501
1502 let entries: Vec<Value> = memories
1503 .iter()
1504 .filter(|m| crate::expansion::common::mcp_visible(m))
1505 .filter(|m| crate::expansion::common::validity_visible(m))
1506 .map(|m| {
1507 json!({
1508 "id": m.metadata.id.to_string(),
1509 "content_preview": m.content.chars().take(80).collect::<String>(),
1510 "tags": m.metadata.tags,
1511 "importance": m.metadata.importance,
1512 "created_at": m.metadata.created_at.to_rfc3339(),
1513 })
1514 })
1515 .collect();
1516
1517 let query_applied = args
1524 .get("query")
1525 .and_then(|v| v.as_str())
1526 .is_some_and(|s| !s.trim().is_empty());
1527 let mut response = json!({
1528 "status": "success",
1529 "galaxy": galaxy.db_name(),
1530 "total": entries.len(),
1531 "memories": entries,
1532 });
1533 if query_applied {
1534 response["note"] = json!(
1535 "'query' applied as a literal substring filter over content — \
1536 for tokenized, ranked full-text retrieval use memory.search."
1537 );
1538 }
1539 if created_after.is_some() || created_before.is_some() {
1540 response["time_range"] = json!({
1541 "created_after": created_after
1542 .map(|t| t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
1543 "created_before": created_before
1544 .map(|t| t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
1545 });
1546 }
1547 Ok(response)
1548 }
1549 fn stats(&self) -> &ToolStats {
1550 &self.stats
1551 }
1552}
1553
1554#[allow(dead_code)]
1559pub struct MemorySearchTool {
1560 search: Arc<SearchEngine>,
1561 store: Arc<MemoryStore>,
1562 stats: ToolStats,
1563 effects: EffectRow,
1564}
1565
1566impl MemorySearchTool {
1567 #[must_use]
1568 pub fn new(search: Arc<SearchEngine>, store: Arc<MemoryStore>) -> Self {
1569 Self {
1570 search,
1571 store,
1572 stats: ToolStats::default(),
1573 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
1574 }
1575 }
1576}
1577
1578#[async_trait]
1579impl Tool for MemorySearchTool {
1580 fn name(&self) -> &str {
1581 "memory.search"
1582 }
1583 fn gana(&self) -> Gana {
1584 Gana::WinnowingBasket
1585 }
1586 fn effects(&self) -> &EffectRow {
1587 &self.effects
1588 }
1589 fn input_schema(&self) -> Value {
1590 schema(
1591 &json!({
1592 "query": str_prop("Full-text query"),
1593 "galaxy": str_prop("Galaxy filter (default: all galaxies)"),
1594 "limit": int_prop("Maximum results (default 20)"),
1595 "min_score": num_prop("Absolute BM25 score floor"),
1596 "min_score_ratio": num_prop("Relative floor: reject hits below this fraction of the top score"),
1597 }),
1598 &["query"],
1599 )
1600 }
1601 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1602 let query = args
1603 .get("query")
1604 .and_then(|v| v.as_str())
1605 .ok_or_else(|| wm_core::CoreError::InvalidArgs("query (string) required".into()))?;
1606 let limit = args
1607 .get("limit")
1608 .and_then(serde_json::Value::as_u64)
1609 .unwrap_or(20) as usize;
1610 let min_score = args
1611 .get("min_score")
1612 .and_then(serde_json::Value::as_f64)
1613 .map(|v| v as f32)
1614 .filter(|v| *v > 0.0);
1615 let min_score_ratio = args
1616 .get("min_score_ratio")
1617 .and_then(serde_json::Value::as_f64)
1618 .map(|v| v as f32)
1619 .filter(|v| *v > 0.0 && *v < 1.0);
1620 let galaxy_str = args.get("galaxy").and_then(|v| v.as_str());
1621
1622 let mut opts = wm_memory::SearchOptions {
1623 limit,
1624 min_score,
1625 relative_floor: min_score_ratio,
1626 ..wm_memory::SearchOptions::default()
1627 };
1628 if let Some(g) = galaxy_str {
1629 opts.galaxy = Some(parse_galaxy(g)?);
1630 }
1631 let results = self.search.search_opt(query, &opts)?;
1632
1633 let entries: Vec<Value> = results
1638 .iter()
1639 .filter_map(|r| {
1640 let galaxy = wm_core::Galaxy::from_db_name(&r.galaxy)?;
1641 let id = uuid::Uuid::parse_str(&r.memory_id).ok()?;
1642 let mem = self.store.get(galaxy, id).ok().flatten()?;
1643 if !crate::expansion::common::mcp_visible(&mem) {
1644 return None;
1645 }
1646 if !crate::expansion::common::validity_visible(&mem) {
1647 return None;
1648 }
1649 Some(json!({
1650 "memory_id": r.memory_id,
1651 "galaxy": r.galaxy,
1652 "score": r.score,
1653 "normalized_score": r.normalized_score,
1654 "content_preview": wm_memory::scrub_text(&mem.content).chars().take(120).collect::<String>(),
1655 }))
1656 })
1657 .collect();
1658
1659 Ok(json!({
1660 "status": "success",
1661 "query": query,
1662 "total": entries.len(),
1663 "results": entries,
1664 }))
1665 }
1666 fn stats(&self) -> &ToolStats {
1667 &self.stats
1668 }
1669}
1670
1671pub struct MemoryChatTool {
1677 search: std::sync::Mutex<ConversationalSearch>,
1678 stats: ToolStats,
1679 effects: EffectRow,
1680}
1681
1682impl MemoryChatTool {
1683 #[must_use]
1684 pub fn new(search: ConversationalSearch) -> Self {
1685 Self {
1686 search: std::sync::Mutex::new(search),
1687 stats: ToolStats::default(),
1688 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
1689 }
1690 }
1691}
1692
1693#[async_trait]
1694impl Tool for MemoryChatTool {
1695 fn name(&self) -> &str {
1696 "memory.chat"
1697 }
1698 fn gana(&self) -> Gana {
1699 Gana::WinnowingBasket
1700 }
1701 fn effects(&self) -> &EffectRow {
1702 &self.effects
1703 }
1704 fn input_schema(&self) -> Value {
1705 schema(
1706 &json!({
1707 "query": str_prop("Conversational query"),
1708 "galaxy": str_prop("Optional galaxy filter"),
1709 "limit": int_prop("Maximum results"),
1710 }),
1711 &["query"],
1712 )
1713 }
1714 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1715 let query = args
1716 .get("query")
1717 .and_then(|v| v.as_str())
1718 .ok_or_else(|| wm_core::CoreError::InvalidArgs("query (string) required".into()))?;
1719 let limit = args
1720 .get("limit")
1721 .and_then(serde_json::Value::as_u64)
1722 .map(|n| n as usize);
1723 let galaxy_str = args.get("galaxy").and_then(|v| v.as_str());
1724
1725 let galaxy = match galaxy_str {
1726 Some(g) => Some(parse_galaxy(g)?),
1727 None => None,
1728 };
1729
1730 let (results, metrics) = {
1731 let search = self
1732 .search
1733 .lock()
1734 .map_err(|e| wm_core::CoreError::Tool(format!("search lock: {e}")))?;
1735 let results = search.search_in_galaxy(query, limit, galaxy);
1736 let metrics = search.metrics();
1737 (results, metrics)
1738 };
1739
1740 let entries: Vec<Value> = results
1741 .iter()
1742 .map(|r| {
1743 json!({
1744 "memory_id": r.memory_id,
1745 "galaxy": format!("{:?}", r.galaxy),
1746 "score": r.score,
1747 "snippet": r.snippet,
1748 "from_cache": r.from_cache,
1749 "latency_us": r.latency_us,
1750 })
1751 })
1752 .collect();
1753
1754 Ok(json!({
1755 "status": "success",
1756 "query": query,
1757 "total": entries.len(),
1758 "results": entries,
1759 "metrics": {
1760 "total_queries": metrics.total_queries,
1761 "cache_hits": metrics.cache_hits,
1762 "cache_misses": metrics.cache_misses,
1763 "cache_hit_rate": metrics.cache_hit_rate(),
1764 "avg_latency_ms": metrics.avg_latency_ms(),
1765 "meets_latency_target": metrics.meets_latency_target(),
1766 },
1767 }))
1768 }
1769 fn stats(&self) -> &ToolStats {
1770 &self.stats
1771 }
1772}
1773
1774pub struct MemoryVectorSearchTool {
1782 store: Arc<MemoryStore>,
1783 vector_store: Arc<std::sync::Mutex<VectorStore>>,
1784 stats: ToolStats,
1785 effects: EffectRow,
1786}
1787
1788impl MemoryVectorSearchTool {
1789 #[must_use]
1793 pub fn new(store: Arc<MemoryStore>, vector_store: Arc<std::sync::Mutex<VectorStore>>) -> Self {
1794 Self {
1795 store,
1796 vector_store,
1797 stats: ToolStats::default(),
1798 effects: EffectRow::read_only(vec![Resource::VectorStore]),
1799 }
1800 }
1801
1802 fn ensure_loaded(&self) -> wm_core::Result<()> {
1804 let mut vs = self
1805 .vector_store
1806 .lock()
1807 .map_err(|e| wm_core::CoreError::Tool(format!("vector store lock: {e}")))?;
1808 if !vs.is_loaded() {
1809 vs.load(&self.store)?;
1810 }
1811 drop(vs);
1812 Ok(())
1813 }
1814}
1815
1816#[async_trait]
1817impl Tool for MemoryVectorSearchTool {
1818 fn name(&self) -> &str {
1819 "memory.vector.search"
1820 }
1821 fn gana(&self) -> Gana {
1822 Gana::WinnowingBasket
1823 }
1824 fn effects(&self) -> &EffectRow {
1825 &self.effects
1826 }
1827 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1828 self.ensure_loaded()?;
1829
1830 let limit = args
1831 .get("limit")
1832 .and_then(serde_json::Value::as_u64)
1833 .unwrap_or(10) as usize;
1834 let galaxy_str = args.get("galaxy").and_then(|v| v.as_str());
1835 let galaxy_filter = match galaxy_str {
1836 Some(g) => Some(parse_galaxy(g)?),
1837 None => None,
1838 };
1839
1840 let results = if let Some(id_str) = args.get("memory_id").and_then(|v| v.as_str()) {
1842 let memory_id = uuid::Uuid::parse_str(id_str).map_err(|e| {
1844 wm_core::CoreError::InvalidArgs(format!("Invalid memory_id UUID: {e}"))
1845 })?;
1846
1847 let vs = self
1848 .vector_store
1849 .lock()
1850 .map_err(|e| wm_core::CoreError::Tool(format!("vector store lock: {e}")))?;
1851 vs.search_similar_to(memory_id, limit)
1852 } else if let Some(embedding_arr) = args.get("embedding").and_then(|v| v.as_array()) {
1853 let embedding: Vec<f32> = embedding_arr
1855 .iter()
1856 .filter_map(|v| v.as_f64().map(|f| f as f32))
1857 .collect();
1858
1859 if embedding.is_empty() {
1860 return Err(wm_core::CoreError::InvalidArgs(
1861 "embedding (array of numbers) or memory_id (string) required".into(),
1862 ));
1863 }
1864
1865 let vs = self
1866 .vector_store
1867 .lock()
1868 .map_err(|e| wm_core::CoreError::Tool(format!("vector store lock: {e}")))?;
1869 vs.search(&embedding, limit, galaxy_filter)
1870 } else {
1871 return Err(wm_core::CoreError::InvalidArgs(
1872 "Either 'embedding' (array of floats) or 'memory_id' (UUID string) is required"
1873 .into(),
1874 ));
1875 };
1876
1877 let entries: Vec<Value> = results
1878 .iter()
1879 .filter_map(|r| {
1880 let stored = self.store.get(r.galaxy, r.memory_id).ok().flatten();
1885 if let Some(mem) = &stored {
1886 if !crate::expansion::common::mcp_visible(mem) {
1887 return None;
1888 }
1889 if !crate::expansion::common::validity_visible(mem) {
1890 return None;
1891 }
1892 }
1893 let preview = stored
1894 .map(|m| m.content.chars().take(120).collect::<String>())
1895 .unwrap_or_default();
1896 Some(json!({
1897 "memory_id": r.memory_id.to_string(),
1898 "galaxy": r.galaxy.db_name(),
1899 "score": r.score,
1900 "content_preview": preview,
1901 }))
1902 })
1903 .collect();
1904
1905 Ok(json!({
1906 "status": "success",
1907 "total": entries.len(),
1908 "results": entries,
1909 }))
1910 }
1911 fn stats(&self) -> &ToolStats {
1912 &self.stats
1913 }
1914}
1915
1916pub struct MemoryAssociateTool {
1920 store: Arc<MemoryStore>,
1921 stats: ToolStats,
1922 effects: EffectRow,
1923}
1924
1925impl MemoryAssociateTool {
1926 pub fn new(store: Arc<MemoryStore>) -> Self {
1927 Self {
1928 store,
1929 stats: ToolStats::default(),
1930 effects: EffectRow {
1931 writes: vec![Resource::Galaxy("associations".into())],
1932 invokes: vec![Capability::MemoryWrite],
1933 ..Default::default()
1934 },
1935 }
1936 }
1937}
1938
1939#[async_trait]
1940impl Tool for MemoryAssociateTool {
1941 fn name(&self) -> &str {
1942 "memory.associate"
1943 }
1944 fn gana(&self) -> Gana {
1945 Gana::Net
1946 }
1947 fn effects(&self) -> &EffectRow {
1948 &self.effects
1949 }
1950 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1951 let source_str = args.get("source").and_then(|v| v.as_str()).ok_or_else(|| {
1952 wm_core::CoreError::InvalidArgs("source (UUID string) required".into())
1953 })?;
1954 let target_str = args.get("target").and_then(|v| v.as_str()).ok_or_else(|| {
1955 wm_core::CoreError::InvalidArgs("target (UUID string) required".into())
1956 })?;
1957 let source = uuid::Uuid::parse_str(source_str)
1958 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid source UUID: {e}")))?;
1959 let target = uuid::Uuid::parse_str(target_str)
1960 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid target UUID: {e}")))?;
1961 let weight = args
1962 .get("weight")
1963 .and_then(serde_json::Value::as_f64)
1964 .unwrap_or(1.0) as f32;
1965 let assoc_type = args
1966 .get("type")
1967 .and_then(|v| v.as_str())
1968 .unwrap_or("related");
1969 let link_type = wm_memory::LinkType::from_str_lossy(assoc_type);
1970
1971 let assoc = Association::new(source, target, link_type, weight);
1972 let assoc_store = AssociationStore::open(self.store.env())?;
1973 assoc_store.put(self.store.env(), &assoc)?;
1974
1975 Ok(json!({
1976 "status": "success",
1977 "source": source_str,
1978 "target": target_str,
1979 "weight": weight,
1980 }))
1981 }
1982 fn stats(&self) -> &ToolStats {
1983 &self.stats
1984 }
1985}
1986
1987pub struct MemoryAssociationsTool {
1991 store: Arc<MemoryStore>,
1992 stats: ToolStats,
1993 effects: EffectRow,
1994}
1995
1996impl MemoryAssociationsTool {
1997 pub fn new(store: Arc<MemoryStore>) -> Self {
1998 Self {
1999 store,
2000 stats: ToolStats::default(),
2001 effects: EffectRow::read_only(vec![Resource::Galaxy("associations".into())]),
2002 }
2003 }
2004}
2005
2006#[async_trait]
2007impl Tool for MemoryAssociationsTool {
2008 fn name(&self) -> &str {
2009 "memory.associations"
2010 }
2011 fn gana(&self) -> Gana {
2012 Gana::Net
2013 }
2014 fn effects(&self) -> &EffectRow {
2015 &self.effects
2016 }
2017 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2018 let id_str = args
2019 .get("id")
2020 .and_then(|v| v.as_str())
2021 .ok_or_else(|| wm_core::CoreError::InvalidArgs("id (UUID string) required".into()))?;
2022 let id = uuid::Uuid::parse_str(id_str)
2023 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid UUID: {e}")))?;
2024 let direction = args
2025 .get("direction")
2026 .and_then(|v| v.as_str())
2027 .unwrap_or("both");
2028
2029 let assoc_store = AssociationStore::open(self.store.env())?;
2030
2031 let mut entries = Vec::new();
2032
2033 if direction == "from" || direction == "both" {
2034 for a in assoc_store.find_from(self.store.env(), id)? {
2035 entries.push(json!({
2036 "source": a.source.to_string(),
2037 "target": a.target.to_string(),
2038 "weight": a.weight,
2039 "link_type": a.link_type.as_str(),
2040 "co_activation_count": a.co_activation_count,
2041 "direction": "outgoing",
2042 }));
2043 }
2044 }
2045 if direction == "to" || direction == "both" {
2046 for a in assoc_store.find_to(self.store.env(), id)? {
2047 entries.push(json!({
2048 "source": a.source.to_string(),
2049 "target": a.target.to_string(),
2050 "weight": a.weight,
2051 "link_type": a.link_type.as_str(),
2052 "co_activation_count": a.co_activation_count,
2053 "direction": "incoming",
2054 }));
2055 }
2056 }
2057
2058 let total = assoc_store.count(self.store.env())?;
2059
2060 Ok(json!({
2061 "status": "success",
2062 "id": id_str,
2063 "direction": direction,
2064 "associations": entries,
2065 "returned": entries.len(),
2066 "total_in_store": total,
2067 }))
2068 }
2069 fn stats(&self) -> &ToolStats {
2070 &self.stats
2071 }
2072}
2073
2074pub struct KarmaReportTool {
2078 ledger: Arc<KarmaLedger>,
2079 stats: ToolStats,
2080 effects: EffectRow,
2081}
2082
2083impl KarmaReportTool {
2084 pub fn new(ledger: Arc<KarmaLedger>) -> Self {
2085 Self {
2086 ledger,
2087 stats: ToolStats::default(),
2088 effects: EffectRow::read_only(vec![Resource::Galaxy("karma".into())]),
2089 }
2090 }
2091}
2092
2093#[async_trait]
2094impl Tool for KarmaReportTool {
2095 fn name(&self) -> &str {
2096 "karma.report"
2097 }
2098 fn gana(&self) -> Gana {
2099 Gana::Willow
2100 }
2101 fn effects(&self) -> &EffectRow {
2102 &self.effects
2103 }
2104 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2105 let recent_count = args
2106 .get("limit")
2107 .and_then(serde_json::Value::as_u64)
2108 .unwrap_or(10) as usize;
2109
2110 let recent = self.ledger.recent(recent_count)?;
2111 let tool_debt = self.ledger.tool_debt()?;
2112
2113 let recent_entries: Vec<Value> = recent
2114 .iter()
2115 .map(|e| {
2116 json!({
2117 "id": e.id,
2118 "tool": e.tool,
2119 "success": e.success,
2120 "mismatch": e.mismatch,
2121 "debt_delta": e.debt_delta,
2122 "guna": format!("{:?}", e.guna),
2123 "total_debt": e.total_debt,
2124 })
2125 })
2126 .collect();
2127
2128 let tool_debt_entries: Vec<Value> = tool_debt
2129 .iter()
2130 .map(|(tool, debt)| {
2131 json!({
2132 "tool": tool,
2133 "debt": debt,
2134 })
2135 })
2136 .collect();
2137
2138 Ok(json!({
2139 "status": "success",
2140 "total_debt": self.ledger.total_debt(),
2141 "chain_head": self.ledger.chain_head(),
2142 "entry_count": self.ledger.next_id(),
2143 "recent_entries": recent_entries,
2144 "per_tool_debt": tool_debt_entries,
2145 }))
2146 }
2147 fn stats(&self) -> &ToolStats {
2148 &self.stats
2149 }
2150}
2151
2152pub struct DharmaStatusTool {
2156 gate: Arc<DharmaGate>,
2157 stats: ToolStats,
2158 effects: EffectRow,
2159}
2160
2161impl DharmaStatusTool {
2162 pub fn new(gate: Arc<DharmaGate>) -> Self {
2163 Self {
2164 gate,
2165 stats: ToolStats::default(),
2166 effects: EffectRow::pure(),
2167 }
2168 }
2169}
2170
2171#[async_trait]
2172impl Tool for DharmaStatusTool {
2173 fn name(&self) -> &str {
2174 "dharma.status"
2175 }
2176 fn gana(&self) -> Gana {
2177 Gana::ExtendedNet
2178 }
2179 fn effects(&self) -> &EffectRow {
2180 &self.effects
2181 }
2182 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
2183 let homeostasis = self.gate.homeostasis();
2184 let health = homeostasis.health_score();
2185
2186 Ok(json!({
2187 "status": "success",
2188 "homeostasis": {
2189 "cpu_load": homeostasis.cpu_load,
2190 "memory_pressure": homeostasis.memory_pressure,
2191 "active": homeostasis.active,
2192 "health_score": health,
2193 "stressed": homeostasis.is_stressed(),
2194 },
2195 "sutras": {
2196 "ahimsa": "Non-harm — destructive actions blocked in strict mode",
2197 "satya": "Truth — memory fabrication always forbidden",
2198 },
2199 }))
2200 }
2201 fn stats(&self) -> &ToolStats {
2202 &self.stats
2203 }
2204}
2205
2206pub struct HarmonyVectorTool {
2210 monitor: Arc<SubstrateMonitor>,
2211 stats: ToolStats,
2212 effects: EffectRow,
2213}
2214
2215impl HarmonyVectorTool {
2216 pub fn new(monitor: Arc<SubstrateMonitor>) -> Self {
2217 Self {
2218 monitor,
2219 stats: ToolStats::default(),
2220 effects: EffectRow::pure(),
2221 }
2222 }
2223}
2224
2225#[async_trait]
2226impl Tool for HarmonyVectorTool {
2227 fn name(&self) -> &str {
2228 "harmony.vector"
2229 }
2230 fn gana(&self) -> Gana {
2231 Gana::Dipper
2232 }
2233 fn effects(&self) -> &EffectRow {
2234 &self.effects
2235 }
2236 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
2237 let hv = self.monitor.sample();
2238 Ok(json!({
2239 "status": "success",
2240 "harmony_vector": hv.to_json(),
2241 }))
2242 }
2243 fn stats(&self) -> &ToolStats {
2244 &self.stats
2245 }
2246}
2247
2248pub struct HarmonyHistoryTool {
2252 monitor: Arc<SubstrateMonitor>,
2253 stats: ToolStats,
2254 effects: EffectRow,
2255}
2256
2257impl HarmonyHistoryTool {
2258 pub fn new(monitor: Arc<SubstrateMonitor>) -> Self {
2259 Self {
2260 monitor,
2261 stats: ToolStats::default(),
2262 effects: EffectRow::pure(),
2263 }
2264 }
2265}
2266
2267#[async_trait]
2268impl Tool for HarmonyHistoryTool {
2269 fn name(&self) -> &str {
2270 "harmony.history"
2271 }
2272 fn gana(&self) -> Gana {
2273 Gana::Dipper
2274 }
2275 fn effects(&self) -> &EffectRow {
2276 &self.effects
2277 }
2278 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2279 let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(20) as usize;
2280 let samples: Vec<Value> = self
2281 .monitor
2282 .history(limit)
2283 .iter()
2284 .map(wm_substrate::HarmonyVector::to_json)
2285 .collect();
2286 Ok(json!({
2287 "status": "success",
2288 "count": samples.len(),
2289 "samples": samples,
2290 }))
2291 }
2292 fn stats(&self) -> &ToolStats {
2293 &self.stats
2294 }
2295}
2296
2297pub struct GnosisStatusTool {
2305 dharma_gate: Arc<DharmaGate>,
2306 resource_rules: Arc<ResourceRules>,
2307 substrate: Arc<SubstrateMonitor>,
2308 stats: ToolStats,
2309 effects: EffectRow,
2310}
2311
2312impl GnosisStatusTool {
2313 pub fn new(
2314 dharma_gate: Arc<DharmaGate>,
2315 resource_rules: Arc<ResourceRules>,
2316 substrate: Arc<SubstrateMonitor>,
2317 ) -> Self {
2318 Self {
2319 dharma_gate,
2320 resource_rules,
2321 substrate,
2322 stats: ToolStats::default(),
2323 effects: EffectRow::pure(),
2324 }
2325 }
2326}
2327
2328#[async_trait]
2329impl Tool for GnosisStatusTool {
2330 fn name(&self) -> &str {
2331 "gnosis.status"
2332 }
2333 fn gana(&self) -> Gana {
2334 Gana::ThreeStars
2335 }
2336 fn effects(&self) -> &EffectRow {
2337 &self.effects
2338 }
2339 async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
2340 let homeostasis = self.dharma_gate.homeostasis();
2341 let health = homeostasis.health_score();
2342 let budget_usage = self.resource_rules.budget_usage();
2343 let human_approved = self.resource_rules.human_approved();
2344 let last_hv = self.substrate.last_sample();
2345
2346 Ok(json!({
2347 "status": "success",
2348 "brain_wave": format!("{:?}", ctx.brain_wave),
2349 "homeostasis": {
2350 "cpu_load": homeostasis.cpu_load,
2351 "memory_pressure": homeostasis.memory_pressure,
2352 "active": homeostasis.active,
2353 "health_score": health,
2354 "stressed": homeostasis.is_stressed(),
2355 },
2356 "resource_rules": {
2357 "writes_last_minute": budget_usage.writes_last_minute,
2358 "spawns_last_minute": budget_usage.spawns_last_minute,
2359 "network_last_minute": budget_usage.network_last_minute,
2360 "novelty_entries": budget_usage.novelty_entries,
2361 "human_approved": human_approved,
2362 "require_human_review": true,
2363 },
2364 "substrate": last_hv.as_ref().map(wm_substrate::HarmonyVector::to_json),
2365 "governance_layers": {
2366 "lakshmi": "Harmony Vector — hardware awareness (active)",
2367 "tiferet": "Resource Gating — brain-wave transitions gated by health (active)",
2368 "yama": "Dharma Resource Rules — budgets, novelty, purpose, human review (active)",
2369 "gnosis": "Transparency Portals — this tool (active)",
2370 },
2371 }))
2372 }
2373 fn stats(&self) -> &ToolStats {
2374 &self.stats
2375 }
2376}
2377
2378pub struct GnosisHistoryTool {
2382 substrate: Arc<SubstrateMonitor>,
2383 stats: ToolStats,
2384 effects: EffectRow,
2385}
2386
2387impl GnosisHistoryTool {
2388 pub fn new(substrate: Arc<SubstrateMonitor>) -> Self {
2389 Self {
2390 substrate,
2391 stats: ToolStats::default(),
2392 effects: EffectRow::pure(),
2393 }
2394 }
2395}
2396
2397#[async_trait]
2398impl Tool for GnosisHistoryTool {
2399 fn name(&self) -> &str {
2400 "gnosis.history"
2401 }
2402 fn gana(&self) -> Gana {
2403 Gana::ThreeStars
2404 }
2405 fn effects(&self) -> &EffectRow {
2406 &self.effects
2407 }
2408 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2409 let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(20) as usize;
2410 let history = self.substrate.history(limit);
2411 let samples: Vec<Value> = history
2412 .iter()
2413 .map(wm_substrate::HarmonyVector::to_json)
2414 .collect();
2415
2416 let avg_cpu = if samples.is_empty() {
2418 0.0
2419 } else {
2420 samples
2421 .iter()
2422 .filter_map(|s| s["cpu_load"].as_f64())
2423 .sum::<f64>()
2424 / samples.len() as f64
2425 };
2426 let avg_mem = if samples.is_empty() {
2427 0.0
2428 } else {
2429 samples
2430 .iter()
2431 .filter_map(|s| s["memory_pressure"].as_f64())
2432 .sum::<f64>()
2433 / samples.len() as f64
2434 };
2435 let avg_health = if samples.is_empty() {
2436 0.0
2437 } else {
2438 samples
2439 .iter()
2440 .filter_map(|s| s["health_score"].as_f64())
2441 .sum::<f64>()
2442 / samples.len() as f64
2443 };
2444
2445 Ok(json!({
2446 "status": "success",
2447 "count": samples.len(),
2448 "summary": {
2449 "avg_cpu_load": avg_cpu,
2450 "avg_memory_pressure": avg_mem,
2451 "avg_health_score": avg_health,
2452 },
2453 "samples": samples,
2454 }))
2455 }
2456 fn stats(&self) -> &ToolStats {
2457 &self.stats
2458 }
2459}
2460
2461pub struct GnosisExplainTool {
2469 dharma_gate: Arc<DharmaGate>,
2470 resource_rules: Arc<ResourceRules>,
2471 stats: ToolStats,
2472 effects: EffectRow,
2473}
2474
2475impl GnosisExplainTool {
2476 pub fn new(dharma_gate: Arc<DharmaGate>, resource_rules: Arc<ResourceRules>) -> Self {
2477 Self {
2478 dharma_gate,
2479 resource_rules,
2480 stats: ToolStats::default(),
2481 effects: EffectRow::pure(),
2482 }
2483 }
2484}
2485
2486#[async_trait]
2487impl Tool for GnosisExplainTool {
2488 fn name(&self) -> &str {
2489 "gnosis.explain"
2490 }
2491 fn gana(&self) -> Gana {
2492 Gana::ThreeStars
2493 }
2494 fn effects(&self) -> &EffectRow {
2495 &self.effects
2496 }
2497 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2498 let tool_name = args
2499 .get("tool_name")
2500 .and_then(Value::as_str)
2501 .unwrap_or("unknown");
2502 let is_write = args
2503 .get("is_write")
2504 .and_then(Value::as_bool)
2505 .unwrap_or(false);
2506 let is_spawn = args
2507 .get("is_spawn")
2508 .and_then(Value::as_bool)
2509 .unwrap_or(false);
2510 let is_network = args
2511 .get("is_network")
2512 .and_then(Value::as_bool)
2513 .unwrap_or(false);
2514 let has_purpose = args
2515 .get("has_purpose")
2516 .and_then(Value::as_bool)
2517 .unwrap_or(true);
2518 let args_hash = args.get("args_hash").and_then(Value::as_u64).unwrap_or(0);
2519
2520 let homeostasis = self.dharma_gate.homeostasis();
2521
2522 let dummy_effects = if is_write {
2524 EffectRow {
2525 writes: vec![Resource::Filesystem],
2526 ..Default::default()
2527 }
2528 } else {
2529 EffectRow::pure()
2530 };
2531 let dharma_verdict = self.dharma_gate.evaluate(&dummy_effects, ctx);
2532
2533 let resource_verdict = self.resource_rules.evaluate(
2535 tool_name,
2536 args_hash,
2537 is_write,
2538 is_spawn,
2539 is_network,
2540 has_purpose,
2541 &homeostasis,
2542 ctx.brain_wave,
2543 );
2544
2545 Ok(json!({
2546 "status": "success",
2547 "tool_name": tool_name,
2548 "brain_wave": format!("{:?}", ctx.brain_wave),
2549 "homeostasis": {
2550 "cpu_load": homeostasis.cpu_load,
2551 "memory_pressure": homeostasis.memory_pressure,
2552 "health_score": homeostasis.health_score(),
2553 "stressed": homeostasis.is_stressed(),
2554 },
2555 "dharma_verdict": {
2556 "verdict": format!("{:?}", dharma_verdict),
2557 "blocks": dharma_verdict.blocks(),
2558 "reason": dharma_verdict.reason(),
2559 },
2560 "resource_verdict": {
2561 "verdict": format!("{:?}", resource_verdict),
2562 "blocks": resource_verdict.blocks(),
2563 "reason": resource_verdict.reason(),
2564 },
2565 "would_block": dharma_verdict.blocks() || resource_verdict.blocks(),
2566 "explanation": format!(
2567 "Tool '{}' under {:?} brain-wave with health {:.2}: Dharma says '{}', Resources say '{}'. {}",
2568 tool_name,
2569 ctx.brain_wave,
2570 homeostasis.health_score(),
2571 dharma_verdict.reason(),
2572 resource_verdict.reason(),
2573 if dharma_verdict.blocks() || resource_verdict.blocks() {
2574 "Action would be BLOCKED."
2575 } else {
2576 "Action would be ALLOWED."
2577 }
2578 ),
2579 }))
2580 }
2581 fn stats(&self) -> &ToolStats {
2582 &self.stats
2583 }
2584}
2585
2586pub struct WmMetaTool {
2590 registry: Arc<ToolRegistry>,
2591 stats: ToolStats,
2592 effects: EffectRow,
2593 embedding_router: Option<Arc<embedding_router::EmbeddingRouter>>,
2596 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
2598 pipeline: Option<Arc<DispatchPipeline>>,
2603}
2604
2605impl WmMetaTool {
2606 #[must_use]
2607 pub fn new(registry: Arc<ToolRegistry>) -> Self {
2608 Self {
2609 registry,
2610 stats: ToolStats::default(),
2611 effects: EffectRow::pure(),
2612 embedding_router: None,
2613 shadow_stats: Arc::new(std::sync::RwLock::new(
2614 embedding_router::ShadowModeStats::default(),
2615 )),
2616 pipeline: None,
2617 }
2618 }
2619
2620 #[must_use]
2625 pub fn with_embedder(
2626 registry: Arc<ToolRegistry>,
2627 embedder: Box<dyn wm_memory::Embedder>,
2628 ) -> Self {
2629 let embedding_router = Self::build_embedding_router(®istry, embedder).map(Arc::new);
2630 Self {
2631 registry,
2632 stats: ToolStats::default(),
2633 effects: EffectRow::pure(),
2634 embedding_router,
2635 shadow_stats: Arc::new(std::sync::RwLock::new(
2636 embedding_router::ShadowModeStats::default(),
2637 )),
2638 pipeline: None,
2639 }
2640 }
2641
2642 #[must_use]
2647 pub fn with_embedder_and_shadow_stats(
2648 registry: Arc<ToolRegistry>,
2649 embedder: Box<dyn wm_memory::Embedder>,
2650 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
2651 ) -> Self {
2652 let embedding_router = Self::build_embedding_router(®istry, embedder).map(Arc::new);
2653 Self {
2654 registry,
2655 stats: ToolStats::default(),
2656 effects: EffectRow::pure(),
2657 embedding_router,
2658 shadow_stats,
2659 pipeline: None,
2660 }
2661 }
2662
2663 #[must_use]
2666 pub fn with_router_shadow_stats_and_pipeline(
2667 registry: Arc<ToolRegistry>,
2668 embedder: Box<dyn wm_memory::Embedder>,
2669 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
2670 pipeline: Option<Arc<DispatchPipeline>>,
2671 ) -> Self {
2672 let embedding_router = Self::build_embedding_router(®istry, embedder).map(Arc::new);
2673 Self {
2674 registry,
2675 stats: ToolStats::default(),
2676 effects: EffectRow::pure(),
2677 embedding_router,
2678 shadow_stats,
2679 pipeline,
2680 }
2681 }
2682
2683 fn build_embedding_router(
2691 registry: &ToolRegistry,
2692 embedder: Box<dyn wm_memory::Embedder>,
2693 ) -> Option<embedding_router::EmbeddingRouter> {
2694 let tools = registry.all_ref();
2695 if tools.is_empty() {
2696 return embedding_router::EmbeddingRouter::new(embedder);
2697 }
2698 let descriptions = embedding_router::anchored_descriptions(tools);
2699 embedding_router::EmbeddingRouter::with_descriptions(embedder, descriptions)
2700 }
2701
2702 fn classify(text: &str) -> (&'static str, f64) {
2708 nlu::classify(text)
2709 }
2710
2711 fn classify_with_router_inner(
2720 router: &embedding_router::EmbeddingRouter,
2721 shadow_stats: &std::sync::RwLock<embedding_router::ShadowModeStats>,
2722 text: &str,
2723 ) -> (String, f64, Option<Vec<f32>>) {
2724 let (emb_tool, emb_conf, margin, query_emb) =
2725 match router.route_with_margin_and_embedding(text) {
2726 Some(t) => t,
2727 None => ("gnosis".into(), 0.0, 0.0, Vec::new()),
2728 };
2729
2730 let (tfidf_tool, tfidf_conf) = nlu::classify(text);
2732 if emb_tool != tfidf_tool {
2733 tracing::debug!(
2734 query = text.chars().take(100).collect::<String>(),
2735 embedding_tool = %emb_tool,
2736 embedding_conf = emb_conf,
2737 margin = margin,
2738 tfidf_tool = %tfidf_tool,
2739 tfidf_conf = tfidf_conf,
2740 "shadow mode disagreement: embedding vs TF-IDF"
2741 );
2742 }
2743
2744 if let Ok(mut stats) = shadow_stats.write() {
2746 stats.record(text, &emb_tool, emb_conf, tfidf_tool, tfidf_conf);
2747 }
2748
2749 let selected = if margin < embedding_router::MIN_MARGIN {
2754 (tfidf_tool.to_string(), tfidf_conf)
2755 } else {
2756 (emb_tool, emb_conf)
2757 };
2758 let query_emb = (!query_emb.is_empty()).then_some(query_emb);
2759 (selected.0, selected.1, query_emb)
2760 }
2761
2762 async fn classify_async(&self, text: &str) -> (String, f64, Option<Vec<f32>>) {
2770 let Some(router) = self.embedding_router.clone() else {
2771 let (tool, conf) = Self::classify(text);
2772 return (tool.to_string(), conf, None);
2773 };
2774 let shadow_stats = Arc::clone(&self.shadow_stats);
2775 let text_owned = text.to_string();
2776 let fallback_text = text_owned.clone();
2777 match tokio::task::spawn_blocking(move || {
2778 Self::classify_with_router_inner(&router, &shadow_stats, &text_owned)
2779 })
2780 .await
2781 {
2782 Ok(result) => result,
2783 Err(join_err) => {
2784 tracing::warn!(
2785 error = %join_err,
2786 "NLU blocking classifier task failed — falling back to TF-IDF"
2787 );
2788 let (tool, conf) = Self::classify(&fallback_text);
2789 (tool.to_string(), conf, None)
2790 }
2791 }
2792 }
2793
2794 #[must_use]
2796 pub const fn shadow_stats(&self) -> &Arc<std::sync::RwLock<embedding_router::ShadowModeStats>> {
2797 &self.shadow_stats
2798 }
2799
2800 #[must_use]
2802 pub const fn embedding_router(&self) -> Option<&Arc<embedding_router::EmbeddingRouter>> {
2803 self.embedding_router.as_ref()
2804 }
2805
2806 fn required_arg(tool_name: &str) -> Option<&'static str> {
2809 match tool_name {
2810 "memory.create" => Some("content"),
2811 "memory.batch_create" => Some("items"),
2812 "memory.read" => Some("id"),
2813 "memory.delete" => Some("id"),
2814 "memory.search" => Some("query"),
2815 "memory.episodic_search" => Some("query"),
2816 "memory.query" => Some("query"),
2817 "memory.associate" => Some("source"),
2818 "memory.associations" => Some("id"),
2819 "memory.update" => Some("id"),
2820 "memory.revisions" => Some("id"),
2821 "memory.tag" => Some("id"),
2822 "memory.batch_read" => Some("ids"),
2823 "memory.nearby" => Some("query"),
2824 "session.end" => Some("session_id"),
2825 "agent.register" => Some("name"),
2826 "agent.trust" => Some("agent_id"),
2827 "agent.descriptions" => Some("agent_id"),
2828 "agent.capabilities" => Some("agent_id"),
2829 "agent.heartbeat.history" => Some("agent_id"),
2830 "agent.deregister" => Some("agent_id"),
2831 "galaxy.purge" => Some("galaxy"),
2832 "memory.deduplicate" => Some("galaxy"),
2833 "task.distribute" => Some("task"),
2834 "code.claim" => Some("scope"),
2835 "code.check" => Some("scope"),
2836 "code.release" => Some("scope"),
2837 _ => None,
2838 }
2839 }
2840
2841 fn missing_arg_hint(tool_name: &str, missing: &str) -> String {
2843 match (tool_name, missing) {
2844 ("memory.create", "content") => "Provide the content to store, e.g. wm(thought='remember that rust is fast')".into(),
2845 ("memory.read", "id") => "Provide a memory UUID, e.g. wm(thought='recall <uuid>') or wm(route='memory.read', args={\"id\": \"<uuid>\"}). To list memories instead, use wm(route='memory.list', args={\"galaxy\": \"codex\", \"limit\": 10})".into(),
2846 ("memory.delete", "id") => "Provide a memory UUID, e.g. wm(thought='delete memory <uuid>')".into(),
2847 ("memory.search", "query") => "Provide a search query, e.g. wm(thought='search for rust')".into(),
2848 ("memory.query", "query") => "Provide a literal substring to match against memory content, e.g. wm(route='memory.query', args={\"query\": \"rust\", \"tags\": [\"project:myapp\"]}). Note: substring match, not ranked full-text — use memory.search for that.".into(),
2849 ("memory.vector.search", "memory_id") => "Provide a memory UUID for similarity search, e.g. wm(thought='find similar to <uuid>')".into(),
2850 ("memory.update", "id") => "Provide a memory UUID to update, e.g. wm(route='memory.update', args={\"id\": \"<uuid>\", \"tags\": [\"new\"]})".into(),
2851 ("memory.revisions", "id") => "Provide a memory UUID to inspect, e.g. wm(route='memory.revisions', args={\"id\": \"<uuid>\", \"action\": \"verify\"}) — actions: list (default) | verify".into(),
2852 ("memory.tag", "id") => "Provide a memory UUID to tag, e.g. wm(route='memory.tag', args={\"id\": \"<uuid>\", \"tags\": [\"rust\"]})".into(),
2853 _ => format!("Missing required argument: '{missing}' for tool '{tool_name}'"),
2854 }
2855 }
2856
2857 fn extract_payload(thought: &str, tool_name: &str) -> Option<(String, String)> {
2859 let lower = thought.to_lowercase();
2860 match tool_name {
2861 "memory.create" => {
2862 for prefix in &[
2863 "remember that ",
2864 "remember ",
2865 "store ",
2866 "save ",
2867 "note that ",
2868 "note ",
2869 ] {
2870 if lower.starts_with(prefix) {
2871 let content = thought[prefix.len()..].to_string();
2872 if !content.is_empty() {
2873 return Some(("content".into(), content));
2874 }
2875 }
2876 }
2877 if !thought.is_empty() {
2878 return Some(("content".into(), thought.to_string()));
2879 }
2880 }
2881 "memory.read" => {
2882 for prefix in &["recall ", "read memory ", "fetch memory ", "get memory "] {
2883 if lower.starts_with(prefix) {
2884 let id = thought[prefix.len()..].trim().to_string();
2885 if !id.is_empty() {
2886 return Some(("id".into(), id));
2887 }
2888 }
2889 }
2890 }
2891 "memory.list" => {
2892 for prefix in &[
2893 "list memories",
2894 "show memories",
2895 "search memories",
2896 "search for",
2897 ] {
2898 if lower.contains(prefix) {
2899 let after = &thought[lower.find(prefix).unwrap() + prefix.len()..];
2900 let query = after.trim().trim_start_matches("in ").trim();
2901 if !query.is_empty() {
2902 return Some(("galaxy".into(), query.to_string()));
2903 }
2904 }
2905 }
2906 }
2907 "memory.delete" => {
2908 for prefix in &["delete memory ", "remove memory ", "forget memory "] {
2909 if lower.starts_with(prefix) {
2910 let id = thought[prefix.len()..].trim().to_string();
2911 if !id.is_empty() {
2912 return Some(("id".into(), id));
2913 }
2914 }
2915 }
2916 }
2917 "memory.search" => {
2918 for prefix in &["search for ", "search "] {
2919 if lower.starts_with(prefix) {
2920 let query = thought[prefix.len()..].trim().to_string();
2921 if !query.is_empty() {
2922 return Some(("query".into(), query));
2923 }
2924 }
2925 }
2926 }
2927 "memory.chat" => {
2928 for prefix in &[
2929 "chat about ",
2930 "chat ",
2931 "ask about ",
2932 "ask ",
2933 "discuss ",
2934 "explore ",
2935 "converse about ",
2936 ] {
2937 if lower.starts_with(prefix) {
2938 let query = thought[prefix.len()..].trim().to_string();
2939 if !query.is_empty() {
2940 return Some(("query".into(), query));
2941 }
2942 }
2943 }
2944 if !thought.is_empty() {
2945 return Some(("query".into(), thought.to_string()));
2946 }
2947 }
2948 "memory.vector.search" => {
2949 for prefix in &[
2950 "find similar to ",
2951 "similar to memory ",
2952 "vector search ",
2953 "semantic search ",
2954 "embedding search ",
2955 ] {
2956 if lower.starts_with(prefix) {
2957 let id = thought[prefix.len()..].trim().to_string();
2958 if !id.is_empty() {
2959 return Some(("memory_id".into(), id));
2960 }
2961 }
2962 }
2963 }
2964 "memory.count" => {
2965 for prefix in &[
2966 "count memories in ",
2967 "how many memories in ",
2968 "memory count ",
2969 ] {
2970 if lower.starts_with(prefix) {
2971 let galaxy = thought[prefix.len()..].trim().to_string();
2972 if !galaxy.is_empty() {
2973 return Some(("galaxy".into(), galaxy));
2974 }
2975 }
2976 }
2977 }
2978 "session.start" => {
2979 for prefix in &["start session ", "new session ", "begin session "] {
2980 if lower.starts_with(prefix) {
2981 let title = thought[prefix.len()..].trim().to_string();
2982 if !title.is_empty() {
2983 return Some(("title".into(), title));
2987 }
2988 }
2989 }
2990 }
2991 "session.end" => {
2992 for prefix in &["end session ", "close session ", "stop session "] {
2993 if lower.starts_with(prefix) {
2994 let id = thought[prefix.len()..].trim().to_string();
2995 if !id.is_empty() {
2996 return Some(("session_id".into(), id));
2997 }
2998 }
2999 }
3000 }
3001 "agent.register" => {
3002 for prefix in &[
3003 "register agent ",
3004 "new agent ",
3005 "create agent ",
3006 "add agent ",
3007 ] {
3008 if lower.starts_with(prefix) {
3009 let name = thought[prefix.len()..].trim().to_string();
3010 if !name.is_empty() {
3011 return Some(("name".into(), name));
3012 }
3013 }
3014 }
3015 }
3016 "agent.trust"
3017 | "agent.descriptions"
3018 | "agent.capabilities"
3019 | "agent.heartbeat.history"
3020 | "agent.deregister" => {
3021 for prefix in &[
3022 "trust agent ",
3023 "describe agent ",
3024 "capabilities agent ",
3025 "heartbeat history agent ",
3026 "deregister agent ",
3027 "unregister agent ",
3028 "remove agent ",
3029 ] {
3030 if lower.starts_with(prefix) {
3031 let id = thought[prefix.len()..].trim().to_string();
3032 if !id.is_empty() {
3033 return Some(("agent_id".into(), id));
3034 }
3035 }
3036 }
3037 }
3038 "galaxy.purge" => {
3039 for prefix in &["purge galaxy ", "wipe galaxy ", "clear galaxy "] {
3040 if lower.starts_with(prefix) {
3041 let galaxy = thought[prefix.len()..].trim().to_string();
3042 if !galaxy.is_empty() {
3043 return Some(("galaxy".into(), galaxy));
3044 }
3045 }
3046 }
3047 }
3048 "task.distribute" => {
3049 for prefix in &["distribute task ", "assign task ", "dispatch task "] {
3050 if lower.starts_with(prefix) {
3051 let task = thought[prefix.len()..].trim().to_string();
3052 if !task.is_empty() {
3053 return Some(("task".into(), task));
3054 }
3055 }
3056 }
3057 }
3058 "memory.sort" => {
3059 for prefix in &["sort memories ", "sort memory ", "order memories "] {
3060 if lower.starts_with(prefix) {
3061 let galaxy = thought[prefix.len()..].trim().to_string();
3062 if !galaxy.is_empty() {
3063 return Some(("galaxy".into(), galaxy));
3064 }
3065 }
3066 }
3067 }
3068 "memory.filter" => {
3069 for prefix in &["filter memories ", "filter memory "] {
3070 if lower.starts_with(prefix) {
3071 let galaxy = thought[prefix.len()..].trim().to_string();
3072 if !galaxy.is_empty() {
3073 return Some(("galaxy".into(), galaxy));
3074 }
3075 }
3076 }
3077 }
3078 "memory.deduplicate" => {
3079 for prefix in &[
3080 "deduplicate memories ",
3081 "deduplicate memory ",
3082 "dedup memories ",
3083 ] {
3084 if lower.starts_with(prefix) {
3085 let galaxy = thought[prefix.len()..].trim().to_string();
3086 if !galaxy.is_empty() {
3087 return Some(("galaxy".into(), galaxy));
3088 }
3089 }
3090 }
3091 }
3092 "memory.export" => {
3093 for prefix in &["export memories ", "export memory "] {
3094 if lower.starts_with(prefix) {
3095 let galaxy = thought[prefix.len()..].trim().to_string();
3096 if !galaxy.is_empty() {
3097 return Some(("galaxy".into(), galaxy));
3098 }
3099 }
3100 }
3101 }
3102 "speculative.decode" => {
3103 for prefix in &[
3104 "speculative decode ",
3105 "speculative ",
3106 "decode ",
3107 "draft and verify ",
3108 "accelerate inference ",
3109 ] {
3110 if lower.starts_with(prefix) {
3111 let prompt = thought[prefix.len()..].trim().to_string();
3112 if !prompt.is_empty() {
3113 return Some(("prompt".into(), prompt));
3114 }
3115 }
3116 }
3117 }
3118 "meta.enhance" => {
3119 for prefix in &[
3120 "enhance ",
3121 "enhance prompt ",
3122 "grounded inference ",
3123 "self-correct ",
3124 "meta enhance ",
3125 "cognitive enhance ",
3126 "augment ",
3127 ] {
3128 if lower.starts_with(prefix) {
3129 let prompt = thought[prefix.len()..].trim().to_string();
3130 if !prompt.is_empty() {
3131 return Some(("prompt".into(), prompt));
3132 }
3133 }
3134 }
3135 }
3136 "dense.encode" => {
3137 for prefix in &["dense encode ", "compress ", "encode ", "compact "] {
3138 if lower.starts_with(prefix) {
3139 let text = thought[prefix.len()..].trim().to_string();
3140 if !text.is_empty() {
3141 return Some(("text".into(), text));
3142 }
3143 }
3144 }
3145 }
3146 "dream.trigger" => {
3147 for prefix in &[
3148 "dream trigger ",
3149 "trigger dream ",
3150 "start dream ",
3151 "force dream ",
3152 "initiate dream ",
3153 ] {
3154 if lower.starts_with(prefix) {
3155 let rest = thought[prefix.len()..].trim();
3156 if !rest.is_empty() {
3157 return Some(("force".into(), rest.to_string()));
3158 }
3159 }
3160 }
3161 }
3162 _ => {}
3163 }
3164 None
3165 }
3166}
3167
3168#[async_trait]
3169impl Tool for WmMetaTool {
3170 fn name(&self) -> &str {
3171 "wm"
3172 }
3173 fn gana(&self) -> Gana {
3174 Gana::Horn
3175 }
3176 fn effects(&self) -> &EffectRow {
3177 &self.effects
3178 }
3179 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
3180 let thought = args.get("thought").and_then(|v| v.as_str()).unwrap_or("");
3181 let route = args.get("route").and_then(|v| v.as_str());
3182 let passthrough_args = args.get("args").cloned().unwrap_or(Value::Null);
3183
3184 if thought.is_empty() && route.is_none() {
3185 let received: Vec<String> = args
3191 .as_object()
3192 .map(|o| o.keys().cloned().collect())
3193 .unwrap_or_default();
3194 let detail = if received.is_empty() {
3195 String::new()
3196 } else {
3197 format!("; received argument keys: {received:?}")
3198 };
3199 return Ok(json!({
3200 "status": "error",
3201 "message": format!(
3202 "Either 'thought' (natural language) or 'route' (explicit) is required{detail}"
3203 ),
3204 "hint": "wm(thought='remember that X is Y') or wm(route='memory.create', args={\"content\": \"...\"})"
3205 }));
3206 }
3207
3208 let (tool_name, confidence, query_emb) = if let Some(r) = route {
3210 (r.to_string(), 1.0, None)
3211 } else {
3212 self.classify_async(thought).await
3213 };
3214
3215 if route.is_none() && tool_name == "gnosis" && confidence < NLU_ABSTENTION_THRESHOLD {
3220 return Ok(json!({
3221 "status": "error",
3222 "message": "Could not confidently match your request to a tool.",
3223 "confidence": confidence,
3224 "hint": "Use explicit routing: wm(route='tool.name', args={...}). Use wm(route='tools.list') to see available tools.",
3225 "_wm_route": { "tool": tool_name, "confidence": confidence, "abstained": true }
3226 }));
3227 }
3228
3229 let mut tool_args = if passthrough_args.is_object() {
3231 let mut args = passthrough_args;
3235 if let Some(obj) = args.as_object_mut() {
3236 obj.remove("_meta");
3237 }
3238 args
3239 } else {
3240 Value::Null
3241 };
3242
3243 if route.is_none() && !thought.is_empty() && tool_args.is_null() {
3245 if let Some((param, value)) = Self::extract_payload(thought, &tool_name) {
3246 tool_args = json!({ param: value });
3247 }
3248 }
3249
3250 let tool = self.registry.get(&tool_name);
3252 match tool {
3253 Some(t) => {
3254 if route.is_none() && t.effects().destructive {
3261 return Ok(json!({
3262 "status": "error",
3263 "message": format!(
3264 "tool '{tool_name}' is destructive and cannot be reached via natural language — use wm(route='{tool_name}', args={{...}}) with \"confirm\": true"
3265 ),
3266 "_wm_route": { "tool": tool_name, "confidence": confidence },
3267 }));
3268 }
3269
3270 if let Some(required) = Self::required_arg(&tool_name) {
3272 let has_arg = tool_args.is_object()
3273 && tool_args.get(required).is_some()
3274 && !tool_args
3275 .get(required)
3276 .is_some_and(serde_json::Value::is_null);
3277 if !has_arg {
3278 return Ok(json!({
3279 "status": "error",
3280 "message": format!("Missing required argument: '{required}' for tool '{tool_name}'"),
3281 "hint": Self::missing_arg_hint(&tool_name, required),
3282 "_wm_route": { "tool": tool_name, "confidence": confidence },
3283 }));
3284 }
3285 }
3286
3287 let result = match &self.pipeline {
3293 Some(p) => p.dispatch(t.as_ref(), ctx, tool_args).await,
3294 None => t.call(ctx, tool_args).await,
3295 };
3296 if let Some(ref router) = self.embedding_router {
3303 let success = result.is_ok();
3304 if let Some(emb) = &query_emb {
3305 router.record_outcome_with_embedding(&tool_name, thought, success, emb);
3306 } else {
3307 let router = Arc::clone(router);
3308 let tool_name_owned = tool_name.clone();
3309 let thought_owned = thought.to_string();
3310 tokio::task::spawn_blocking(move || {
3311 router.record_outcome(&tool_name_owned, &thought_owned, success);
3312 });
3313 }
3314 }
3315 match result {
3316 Ok(mut output) => {
3317 if let Value::Object(ref mut map) = output {
3319 map.insert(
3320 "_wm_route".into(),
3321 json!({
3322 "input": thought.chars().take(200).collect::<String>(),
3323 "tool": tool_name,
3324 "confidence": confidence,
3325 }),
3326 );
3327 }
3328 Ok(output)
3329 }
3330 Err(e) => Ok(json!({
3331 "status": "error",
3332 "error": e.to_string(),
3333 "_wm_route": { "tool": tool_name, "confidence": confidence },
3334 })),
3335 }
3336 }
3337 None => Ok(json!({
3338 "status": "error",
3339 "message": format!("Unknown tool: '{tool_name}'"),
3340 "_wm_route": { "tool": tool_name, "confidence": confidence },
3341 })),
3342 }
3343 }
3344 fn stats(&self) -> &ToolStats {
3345 &self.stats
3346 }
3347}
3348
3349fn parse_galaxy(s: &str) -> wm_core::Result<Galaxy> {
3353 expansion::common::parse_galaxy(s)
3354}
3355
3356#[allow(clippy::too_many_arguments)]
3363pub fn register_all(
3364 registry: &ToolRegistry,
3365 store: &Arc<MemoryStore>,
3366 search: Option<Arc<SearchEngine>>,
3367 karma: Option<Arc<KarmaLedger>>,
3368 dharma: &Option<Arc<DharmaGate>>,
3369 substrate: Option<Arc<SubstrateMonitor>>,
3370 resource_rules: &Option<Arc<ResourceRules>>,
3371 associations: Arc<AssociationStore>,
3372 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
3373 vector_store: Arc<std::sync::Mutex<VectorStore>>,
3374 conversational: Option<ConversationalSearch>,
3375 recall: Option<Arc<RecallEngine>>,
3376 homeostatic_loop: Option<Arc<std::sync::Mutex<HomeostaticLoop>>>,
3377 anomaly_detector: Option<Arc<std::sync::Mutex<AnomalyDetector>>>,
3378 sensorimotor_bus: Option<Arc<std::sync::Mutex<SensorimotorBus>>>,
3379 reflex_loop: Option<Arc<std::sync::Mutex<ReflexLoop>>>,
3380 gan_ying_bus: Option<&Arc<std::sync::Mutex<GanYingBus>>>,
3381 transaction_state: expansion::TransactionState,
3382 escalation_queue: Option<&Arc<std::sync::Mutex<wm_governance::EscalationQueue>>>,
3383 firewall: Option<&Arc<expansion::firewall::TxFirewall>>,
3384 code_graph: Option<&Arc<std::sync::Mutex<expansion::code::CodeGraph>>>,
3385) -> ToolRegistry {
3386 let mut reg = registry
3387 .register(Arc::new(MemoryCreateTool::new(
3388 store.clone(),
3389 search.clone(),
3390 recall.clone(),
3391 )))
3392 .register(Arc::new(MemoryBatchCreateTool::new(
3393 store.clone(),
3394 search.clone(),
3395 recall.clone(),
3396 )))
3397 .register(Arc::new(MemoryReadTool::new(store.clone())))
3398 .register(Arc::new(MemoryListTool::new(store.clone())))
3399 .register(Arc::new(MemoryDeleteTool::new(
3400 store.clone(),
3401 search.clone(),
3402 )))
3403 .register(Arc::new(MemoryBatchDeleteTool::new(
3404 store.clone(),
3405 search.clone(),
3406 )))
3407 .register(Arc::new(MemoryQueryTool::new(store.clone())))
3408 .register(Arc::new(MemoryAssociateTool::new(store.clone())))
3409 .register(Arc::new(MemoryAssociationsTool::new(store.clone())))
3410 .register(Arc::new(MemoryVectorSearchTool::new(
3411 store.clone(),
3412 vector_store,
3413 )))
3414 .register(Arc::new(GnosisTool::new(store.clone())));
3415
3416 if let Some(conv) = conversational {
3417 reg = reg.register(Arc::new(MemoryChatTool::new(conv)));
3418 }
3419
3420 if let Some(s) = search {
3421 reg = reg.register(Arc::new(
3425 expansion::MemoryHybridRecallTool::as_search(
3426 store.clone(),
3427 Some(s.clone()),
3428 recall.clone(),
3429 )
3430 .with_associations(Some(associations.clone())),
3431 ));
3432 reg = expansion::register_expansion(
3434 ®,
3435 store,
3436 Some(s),
3437 recall,
3438 associations,
3439 spiral_tracker,
3440 karma.clone(),
3441 substrate.clone(),
3442 homeostatic_loop,
3443 anomaly_detector,
3444 sensorimotor_bus,
3445 reflex_loop,
3446 gan_ying_bus,
3447 transaction_state,
3448 resource_rules.as_ref(),
3449 escalation_queue,
3450 dharma.as_ref(),
3451 firewall,
3452 code_graph,
3453 );
3454 } else {
3455 reg = expansion::register_expansion(
3456 ®,
3457 store,
3458 None,
3459 recall,
3460 associations,
3461 spiral_tracker,
3462 karma.clone(),
3463 substrate.clone(),
3464 homeostatic_loop,
3465 anomaly_detector,
3466 sensorimotor_bus,
3467 reflex_loop,
3468 gan_ying_bus,
3469 transaction_state,
3470 resource_rules.as_ref(),
3471 escalation_queue,
3472 dharma.as_ref(),
3473 firewall,
3474 code_graph,
3475 );
3476 }
3477 if let Some(k) = karma {
3478 reg = reg.register(Arc::new(KarmaReportTool::new(k)));
3479 }
3480 if let Some(d) = dharma {
3481 reg = reg.register(Arc::new(DharmaStatusTool::new(d.clone())));
3482 }
3483 if let Some(s) = substrate {
3484 reg = reg
3485 .register(Arc::new(HarmonyVectorTool::new(s.clone())))
3486 .register(Arc::new(HarmonyHistoryTool::new(s.clone())));
3487 if let Some(d) = dharma {
3488 if let Some(r) = resource_rules {
3489 reg = reg
3490 .register(Arc::new(GnosisStatusTool::new(
3491 d.clone(),
3492 r.clone(),
3493 s.clone(),
3494 )))
3495 .register(Arc::new(GnosisHistoryTool::new(s)))
3496 .register(Arc::new(GnosisExplainTool::new(d.clone(), r.clone())));
3497 }
3498 }
3499 }
3500
3501 reg
3502}
3503
3504pub fn register_meta_tools(
3510 registry: &ToolRegistry,
3511 store: &Arc<MemoryStore>,
3512 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3513) -> ToolRegistry {
3514 register_meta_tools_with_router(registry, store, shadow_stats, None).0
3515}
3516
3517#[must_use]
3527pub fn register_meta_tools_with_router(
3528 registry: &ToolRegistry,
3529 store: &Arc<MemoryStore>,
3530 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3531 pipeline: Option<Arc<DispatchPipeline>>,
3532) -> (ToolRegistry, Option<Arc<embedding_router::EmbeddingRouter>>) {
3533 let base_snapshot: Vec<Arc<dyn Tool>> = registry.all();
3534 let tool_count = base_snapshot.len();
3536
3537 let non_gnosis: Vec<Arc<dyn Tool>> = base_snapshot
3538 .iter()
3539 .filter(|t| t.name() != "gnosis")
3540 .cloned()
3541 .collect();
3542
3543 let mut list_builder = ToolRegistryBuilder::new();
3545 for tool in &non_gnosis {
3546 list_builder.register(tool.clone());
3547 }
3548 let list_registry = Arc::new(list_builder.build());
3549 let tools_list = Arc::new(ToolsListTool::new(Arc::clone(&list_registry)));
3550
3551 let usage_report = Arc::new(expansion::ToolsUsageReportTool::new(list_registry));
3555
3556 let gnosis = Arc::new(GnosisTool::with_tool_count(Arc::clone(store), tool_count));
3558 let mut wm_builder = ToolRegistryBuilder::new();
3559 for tool in &non_gnosis {
3560 wm_builder.register(tool.clone());
3561 }
3562 wm_builder.register(tools_list.clone());
3563 wm_builder.register(usage_report.clone());
3564 wm_builder.register(gnosis.clone());
3565
3566 let shadow_report = Arc::new(expansion::NluShadowReportTool::new(Arc::clone(
3571 &shadow_stats,
3572 )));
3573 wm_builder.register(shadow_report.clone());
3574 let wm = Arc::new(WmMetaTool::with_router_shadow_stats_and_pipeline(
3575 Arc::new(wm_builder.build()),
3576 wm_memory::create_embedder(),
3577 shadow_stats,
3578 pipeline,
3579 ));
3580 let router = wm.embedding_router().cloned();
3581
3582 let mut final_builder = ToolRegistryBuilder::new();
3584 for tool in non_gnosis {
3585 final_builder.register(tool);
3586 }
3587 final_builder.register(tools_list);
3588 final_builder.register(usage_report);
3589 final_builder.register(wm);
3590 final_builder.register(gnosis);
3591 final_builder.register(shadow_report);
3592 (final_builder.build(), router)
3593}
3594
3595#[cfg(test)]
3596mod tests {
3597 use super::*;
3598 use wm_core::BrainWave;
3599
3600 fn test_store() -> Arc<MemoryStore> {
3601 let tmp = tempfile::tempdir().unwrap();
3602 Arc::new(MemoryStore::open_default(tmp.path()).unwrap())
3603 }
3604
3605 #[tokio::test]
3606 async fn memory_create_warns_on_credential_shaped_content() {
3607 let store = test_store();
3608 let tool = MemoryCreateTool::new(store, None, None);
3609 let mut ctx = Context::default();
3610
3611 let clean = tool
3612 .call(
3613 &mut ctx,
3614 json!({"content": "the password policy requires rotation"}),
3615 )
3616 .await
3617 .unwrap();
3618 assert!(clean.get("warnings").is_none(), "clean content: {clean}");
3619
3620 let flagged = tool
3621 .call(
3622 &mut ctx,
3623 json!({"content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEow\n-----END RSA PRIVATE KEY-----"}),
3624 )
3625 .await
3626 .unwrap();
3627 assert_eq!(
3628 flagged["status"], "success",
3629 "warning, not refusal: {flagged}"
3630 );
3631 let warnings = flagged["warnings"].as_array().unwrap();
3632 assert!(
3633 warnings[0].as_str().unwrap().contains("private_key_pem"),
3634 "got: {warnings:?}"
3635 );
3636 assert!(warnings[0].as_str().unwrap().contains("keyring"));
3637 }
3638
3639 #[tokio::test]
3640 async fn memory_batch_create_aggregates_credential_warnings() {
3641 let store = test_store();
3642 let tool = MemoryBatchCreateTool::new(store, None, None);
3643 let mut ctx = Context::default();
3644 let r = tool
3645 .call(
3646 &mut ctx,
3647 json!({"items": [
3648 {"content": "ordinary note"},
3649 {"content": "AKIAIOSFODNN7EXAMPLE"},
3650 ]}),
3651 )
3652 .await
3653 .unwrap();
3654 assert_eq!(r["count"], 2);
3655 let warnings = r["warnings"].as_array().unwrap();
3656 assert!(warnings[0].as_str().unwrap().contains("aws_access_key_id"));
3657 }
3658
3659 fn test_registry_with(store: &Arc<MemoryStore>) -> ToolRegistry {
3660 let registry = ToolRegistry::new();
3661 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
3662 let spiral_tracker =
3663 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
3664 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
3665 register_all(
3666 ®istry,
3667 store,
3668 None,
3669 None,
3670 &None,
3671 None,
3672 &None,
3673 associations,
3674 spiral_tracker,
3675 vector_store,
3676 None,
3677 None,
3678 None,
3679 None,
3680 None,
3681 None,
3682 None,
3683 std::sync::Arc::new(std::sync::Mutex::new(None)),
3684 None,
3685 None,
3686 None,
3687 )
3688 }
3689
3690 #[tokio::test]
3691 async fn memory_create_and_read() {
3692 let store = test_store();
3693 let tool = MemoryCreateTool::new(store.clone(), None, None);
3694 let mut ctx = Context::new(BrainWave::Gamma);
3695
3696 let args = json!({"content": "test memory content", "galaxy": "codex"});
3697 let result = tool.call(&mut ctx, args).await.unwrap();
3698 assert_eq!(result["status"], "success");
3699 let id = result["id"].as_str().unwrap();
3700
3701 let read_tool = MemoryReadTool::new(store.clone());
3702 let result = read_tool.call(&mut ctx, json!({"id": id})).await.unwrap();
3703 assert_eq!(result["status"], "success");
3704 assert_eq!(result["content"], "test memory content");
3705
3706 let episodic = store
3707 .episodic()
3708 .get(uuid::Uuid::parse_str(id).unwrap())
3709 .unwrap()
3710 .expect("explicit memory writes mirror into episodic storage");
3711 assert_eq!(episodic.content, "test memory content");
3712 }
3713
3714 #[tokio::test]
3719 async fn memory_create_attestation_disclosure() {
3720 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
3721 let mut ctx = Context::new(BrainWave::Gamma);
3722
3723 let store = test_store();
3725 let tool = MemoryCreateTool::with_attestation_key(store.clone(), None, None, None);
3726 let result = tool
3727 .call(
3728 &mut ctx,
3729 json!({"content": "unattested create", "galaxy": "codex"}),
3730 )
3731 .await
3732 .unwrap();
3733 assert_eq!(result["status"], "success");
3734 assert_eq!(result["attested"], false);
3735 assert_eq!(result["attested_reason"], "node key unavailable");
3736
3737 let tool = MemoryCreateTool::with_attestation_key(
3739 store.clone(),
3740 None,
3741 None,
3742 Some("not-hex".to_string()),
3743 );
3744 let result = tool
3745 .call(
3746 &mut ctx,
3747 json!({"content": "bad key create", "galaxy": "codex"}),
3748 )
3749 .await
3750 .unwrap();
3751 assert_eq!(result["attested"], false);
3752 assert_eq!(result["attested_reason"], "node key invalid");
3753
3754 let tool = MemoryCreateTool::with_attestation_key(
3756 store.clone(),
3757 None,
3758 None,
3759 Some(TEST_KEY.to_string()),
3760 );
3761 let result = tool
3762 .call(
3763 &mut ctx,
3764 json!({"content": "attested create", "galaxy": "codex"}),
3765 )
3766 .await
3767 .unwrap();
3768 assert_eq!(result["attested"], true);
3769 assert!(result.get("attested_reason").is_none());
3770 let id = uuid::Uuid::parse_str(result["id"].as_str().unwrap()).unwrap();
3771 let report = store.verify_attestation(Galaxy::Codex, id).unwrap();
3772 assert!(report.attested, "{:?}", report.breaks);
3773 assert!(report.signature_valid, "{:?}", report.breaks);
3774 assert!(report.matches_head, "{:?}", report.breaks);
3775 assert!(report.memory_present);
3776 assert!(report.breaks.is_empty());
3777
3778 let mut memory = store.get(Galaxy::Codex, id).unwrap().unwrap();
3782 memory.content = "edited after attestation".to_string();
3783 memory.metadata.content_hash = wm_memory::content_hash(&memory.content);
3784 store.put(Galaxy::Codex, &memory).unwrap();
3785 let stale = store.verify_attestation(Galaxy::Codex, id).unwrap();
3786 assert!(stale.attested);
3787 assert!(stale.signature_valid);
3788 assert!(!stale.matches_head);
3789
3790 let scanned = store.scan_attestations().unwrap();
3792 assert_eq!(scanned.len(), 1);
3793 assert_eq!(scanned[0].memory_id, id.to_string());
3794 }
3795
3796 #[tokio::test]
3797 async fn memory_batch_create_attests_each_item() {
3798 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
3799 let store = test_store();
3800 let tool = MemoryBatchCreateTool::with_attestation_key(
3801 store.clone(),
3802 None,
3803 None,
3804 Some(TEST_KEY.to_string()),
3805 );
3806 let mut ctx = Context::new(BrainWave::Gamma);
3807 let result = tool
3808 .call(
3809 &mut ctx,
3810 json!({"items": [{"content": "batch one"}, {"content": "batch two"}]}),
3811 )
3812 .await
3813 .unwrap();
3814 assert_eq!(result["attested_count"], 2);
3815 assert_eq!(store.scan_attestations().unwrap().len(), 2);
3816
3817 let tool = MemoryBatchCreateTool::with_attestation_key(store.clone(), None, None, None);
3819 let result = tool
3820 .call(&mut ctx, json!({"items": [{"content": "batch three"}]}))
3821 .await
3822 .unwrap();
3823 assert_eq!(result["attested_count"], 0);
3824 assert_eq!(result["count"], 1);
3825 }
3826
3827 #[tokio::test]
3828 async fn memory_batch_create_mirrors_into_episodic_lane() {
3829 let store = test_store();
3830 let tool = MemoryBatchCreateTool::new(store.clone(), None, None);
3831 let mut ctx = Context::new(BrainWave::Gamma);
3832 let result = tool
3833 .call(
3834 &mut ctx,
3835 json!({
3836 "items": [
3837 {"content": "batch rust retrieval"},
3838 {"content": "batch grocery list"}
3839 ]
3840 }),
3841 )
3842 .await
3843 .unwrap();
3844 assert_eq!(result["status"], "success");
3845 let ids = result["ids"].as_array().unwrap();
3846 let first = uuid::Uuid::parse_str(ids[0].as_str().unwrap()).unwrap();
3847 let hits = store
3848 .episodic()
3849 .search("rust retrieval", 10, false)
3850 .unwrap();
3851 assert_eq!(hits.len(), 1);
3852 assert_eq!(hits[0].record.id, first);
3853 }
3854
3855 #[tokio::test]
3856 async fn memory_list_returns_entries() {
3857 let store = test_store();
3858 let create = MemoryCreateTool::new(store.clone(), None, None);
3859 let mut ctx = Context::new(BrainWave::Gamma);
3860
3861 for i in 0..3 {
3862 create
3863 .call(&mut ctx, json!({"content": format!("item-{i}")}))
3864 .await
3865 .unwrap();
3866 }
3867
3868 let list = MemoryListTool::new(store);
3869 let result = list.call(&mut ctx, json!({"limit": 10})).await.unwrap();
3870 assert_eq!(result["status"], "success");
3871 assert_eq!(result["total"], 3);
3872 assert_eq!(result["returned"], 3);
3873 }
3874
3875 #[tokio::test]
3879 async fn memory_list_offset_and_exclude_tags_page_visible_surface() {
3880 let store = test_store();
3881 let mut ctx = Context::new(BrainWave::Gamma);
3882
3883 for i in 0..5 {
3884 let mut m = wm_memory::Memory::new(wm_core::Galaxy::Codex, format!("page note {i}"));
3885 if i == 1 {
3886 m.metadata.tags = vec!["noise".into()];
3887 }
3888 if i == 3 {
3889 m.metadata.is_private = true;
3890 }
3891 store.put(wm_core::Galaxy::Codex, &m).unwrap();
3892 }
3893
3894 let list = MemoryListTool::new(store);
3895
3896 let all = list
3899 .call(
3900 &mut ctx,
3901 json!({"galaxy": "codex", "limit": 50, "exclude_tags": ["noise"]}),
3902 )
3903 .await
3904 .unwrap();
3905 assert_eq!(all["total"], 5, "total counts the whole galaxy");
3906 assert_eq!(all["matched"], 3, "private + excluded are invisible");
3907 assert_eq!(all["returned"], 3);
3908 assert_eq!(all["offset"], 0);
3909
3910 let page1 = list
3912 .call(
3913 &mut ctx,
3914 json!({"galaxy": "codex", "limit": 2, "offset": 0, "exclude_tags": ["noise"]}),
3915 )
3916 .await
3917 .unwrap();
3918 assert_eq!(page1["returned"], 2);
3919 let page2 = list
3920 .call(
3921 &mut ctx,
3922 json!({"galaxy": "codex", "limit": 2, "offset": 2, "exclude_tags": ["noise"]}),
3923 )
3924 .await
3925 .unwrap();
3926 assert_eq!(
3927 page2["returned"], 1,
3928 "matched is 3 — the tail page is short"
3929 );
3930 assert_eq!(page2["offset"], 2);
3931
3932 let ids_of = |v: &Value| -> Vec<String> {
3933 v["memories"]
3934 .as_array()
3935 .unwrap()
3936 .iter()
3937 .filter_map(|m| m["id"].as_str().map(String::from))
3938 .collect()
3939 };
3940 let (p1, p2, everything) = (ids_of(&page1), ids_of(&page2), ids_of(&all));
3941 assert_eq!(p1.len(), 2);
3942 let mut union = p1;
3943 union.extend(p2);
3944 let mut sorted_union = union.clone();
3945 sorted_union.sort();
3946 let mut sorted_all = everything;
3947 sorted_all.sort();
3948 assert_eq!(sorted_union, sorted_all, "pages must partition the surface");
3949 }
3950
3951 #[tokio::test]
3956 async fn memory_create_stamps_provenance_by_claim() {
3957 let store = test_store();
3958 let create = MemoryCreateTool::new(store.clone(), None, None);
3959 let mut ctx = Context::new(BrainWave::Gamma);
3960
3961 let silent = create
3962 .call(&mut ctx, json!({"content": "no claim"}))
3963 .await
3964 .unwrap();
3965 assert_eq!(silent["source"], "agent");
3966 assert!((silent["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
3967
3968 let claimed = create
3969 .call(
3970 &mut ctx,
3971 json!({"content": "user dictated this", "source": "user"}),
3972 )
3973 .await
3974 .unwrap();
3975 assert_eq!(claimed["source"], "user");
3976 assert!((claimed["source_trust"].as_f64().unwrap() - 1.0).abs() < 1e-5);
3977
3978 let custom = create
3979 .call(&mut ctx, json!({"content": "web import", "source": "web"}))
3980 .await
3981 .unwrap();
3982 assert_eq!(custom["source"], "web");
3983 assert!((custom["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
3984
3985 let fetch = |id: &str| {
3986 store
3987 .get(wm_core::Galaxy::Codex, uuid::Uuid::parse_str(id).unwrap())
3988 .expect("stored")
3989 .expect("present")
3990 };
3991 assert_eq!(
3992 fetch(silent["id"].as_str().unwrap()).metadata.source,
3993 "agent"
3994 );
3995 assert_eq!(
3996 fetch(claimed["id"].as_str().unwrap()).metadata.source,
3997 "user"
3998 );
3999 }
4000
4001 #[tokio::test]
4002 async fn gnosis_returns_system_info() {
4003 let store = test_store();
4004 let tool = GnosisTool::new(store);
4005 let mut ctx = Context::new(BrainWave::Gamma);
4006 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4007 assert_eq!(result["status"], "success");
4008 assert!(result["version"].is_string());
4009 }
4010
4011 #[tokio::test]
4012 async fn memory_delete_removes_entry() {
4013 let store = test_store();
4014 let create = MemoryCreateTool::new(store.clone(), None, None);
4015 let mut ctx = Context::new(BrainWave::Gamma);
4016
4017 let result = create
4018 .call(&mut ctx, json!({"content": "to be deleted"}))
4019 .await
4020 .unwrap();
4021 let id = result["id"].as_str().unwrap();
4022
4023 let delete = MemoryDeleteTool::new(store.clone(), None);
4024 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4025 assert_eq!(result["status"], "success");
4026
4027 let read = MemoryReadTool::new(store);
4028 let result = read.call(&mut ctx, json!({"id": id})).await.unwrap();
4029 assert_eq!(result["status"], "not_found");
4030 }
4031
4032 #[tokio::test]
4033 async fn memory_delete_without_galaxy_resolves_across_memory_galaxies() {
4034 let store = test_store();
4035 let create = MemoryCreateTool::new(store.clone(), None, None);
4036 let mut ctx = Context::new(BrainWave::Gamma);
4037
4038 let result = create
4040 .call(
4041 &mut ctx,
4042 json!({"content": "session decision", "galaxy": "sessions"}),
4043 )
4044 .await
4045 .unwrap();
4046 let id = result["id"].as_str().unwrap();
4047
4048 let delete = MemoryDeleteTool::new(store.clone(), None);
4050 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4051 assert_eq!(result["status"], "success");
4052 assert!(
4053 result["galaxies"]
4054 .as_array()
4055 .unwrap()
4056 .contains(&json!("sessions"))
4057 );
4058
4059 let read = MemoryReadTool::new(store.clone());
4060 let result = read
4061 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4062 .await
4063 .unwrap();
4064 assert_eq!(result["status"], "not_found");
4065 }
4066
4067 #[tokio::test]
4068 async fn memory_delete_explicit_galaxy_does_not_miss_other_galaxies() {
4069 let store = test_store();
4070 let create = MemoryCreateTool::new(store.clone(), None, None);
4071 let mut ctx = Context::new(BrainWave::Gamma);
4072
4073 let result = create
4074 .call(
4075 &mut ctx,
4076 json!({"content": "in sessions", "galaxy": "sessions"}),
4077 )
4078 .await
4079 .unwrap();
4080 let id = result["id"].as_str().unwrap();
4081
4082 let delete = MemoryDeleteTool::new(store.clone(), None);
4084 let result = delete
4085 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4086 .await
4087 .unwrap();
4088 assert_eq!(result["status"], "not_found");
4089 assert!(result["hint"].is_string());
4090
4091 let read = MemoryReadTool::new(store.clone());
4092 let result = read
4093 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4094 .await
4095 .unwrap();
4096 assert_eq!(result["status"], "success");
4097 }
4098
4099 #[tokio::test]
4100 async fn memory_query_filters_by_tags() {
4101 let store = test_store();
4102 let create = MemoryCreateTool::new(store.clone(), None, None);
4103 let mut ctx = Context::new(BrainWave::Gamma);
4104
4105 create
4106 .call(&mut ctx, json!({"content": "tagged", "tags": ["rust"]}))
4107 .await
4108 .unwrap();
4109 create
4110 .call(&mut ctx, json!({"content": "untagged"}))
4111 .await
4112 .unwrap();
4113
4114 let query = MemoryQueryTool::new(store);
4115 let result = query
4116 .call(&mut ctx, json!({"tags": ["rust"]}))
4117 .await
4118 .unwrap();
4119 assert_eq!(result["status"], "success");
4120 assert_eq!(result["total"], 1);
4121 }
4122
4123 #[tokio::test]
4126 async fn memory_query_time_range_passthrough() {
4127 let store = test_store();
4128 let mut ctx = Context::new(BrainWave::Gamma);
4129
4130 let mut old = wm_memory::Memory::new(wm_core::Galaxy::Codex, "old relic".into());
4131 old.metadata.created_at = chrono::Utc::now() - chrono::Duration::days(60);
4132 store.put(wm_core::Galaxy::Codex, &old).unwrap();
4133 let mut recent = wm_memory::Memory::new(wm_core::Galaxy::Codex, "recent note".into());
4134 recent.metadata.created_at = chrono::Utc::now() - chrono::Duration::hours(1);
4135 store.put(wm_core::Galaxy::Codex, &recent).unwrap();
4136
4137 let query = MemoryQueryTool::new(store);
4138 let cutoff = (chrono::Utc::now() - chrono::Duration::days(1))
4139 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
4140
4141 let only_recent = query
4142 .call(&mut ctx, json!({"created_after": cutoff}))
4143 .await
4144 .unwrap();
4145 assert_eq!(only_recent["total"], 1);
4146 assert_eq!(only_recent["memories"][0]["content_preview"], "recent note");
4147 assert_eq!(
4148 only_recent["time_range"]["created_after"], cutoff,
4149 "the applied time range must be disclosed"
4150 );
4151
4152 let only_old = query
4153 .call(&mut ctx, json!({"created_before": cutoff}))
4154 .await
4155 .unwrap();
4156 assert_eq!(only_old["total"], 1);
4157 assert_eq!(only_old["memories"][0]["content_preview"], "old relic");
4158
4159 let both = query
4161 .call(
4162 &mut ctx,
4163 json!({
4164 "created_after": (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
4165 "created_before": cutoff,
4166 }),
4167 )
4168 .await
4169 .unwrap();
4170 assert_eq!(both["total"], 1);
4171 assert_eq!(both["memories"][0]["content_preview"], "old relic");
4172
4173 let bad = query
4175 .call(&mut ctx, json!({"created_after": "not-a-timestamp"}))
4176 .await;
4177 assert!(bad.is_err(), "invalid RFC 3339 must be refused");
4178 }
4179
4180 #[tokio::test]
4181 async fn memory_vector_search_by_embedding() {
4182 let store = test_store();
4183 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4184
4185 {
4187 let mut vs = vector_store.lock().unwrap();
4188 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![1.0, 0.0, 0.0]);
4189 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![0.9, 0.1, 0.0]);
4190 vs.add(uuid::Uuid::new_v4(), Galaxy::Research, vec![0.0, 1.0, 0.0]);
4191 }
4192
4193 let tool = MemoryVectorSearchTool::new(store, vector_store);
4194 let mut ctx = Context::new(BrainWave::Gamma);
4195
4196 let result = tool
4198 .call(&mut ctx, json!({"embedding": [1.0, 0.0, 0.0], "limit": 2}))
4199 .await
4200 .unwrap();
4201 assert_eq!(result["status"], "success");
4202 assert_eq!(result["total"], 2);
4203 }
4204
4205 #[tokio::test]
4206 async fn memory_vector_search_missing_args() {
4207 let store = test_store();
4208 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4209
4210 let tool = MemoryVectorSearchTool::new(store, vector_store);
4211 let mut ctx = Context::new(BrainWave::Gamma);
4212
4213 let result = tool.call(&mut ctx, json!({"limit": 5})).await;
4214 assert!(result.is_err());
4215 }
4216
4217 #[tokio::test]
4218 async fn wm_routes_vector_search_to_memory_vector_search() {
4219 let store = test_store();
4220 let registry = test_registry_with(&store);
4221 let registry = register_meta_tools(
4222 ®istry,
4223 &store,
4224 std::sync::Arc::new(std::sync::RwLock::new(
4225 embedding_router::ShadowModeStats::default(),
4226 )),
4227 );
4228
4229 let wm = registry.get("wm").unwrap();
4230 let mut ctx = Context::new(BrainWave::Gamma);
4231 let result = wm
4232 .call(
4233 &mut ctx,
4234 json!({"route": "memory.vector.search", "args": {"embedding": [1.0, 0.0, 0.0]}}),
4235 )
4236 .await
4237 .unwrap();
4238
4239 assert_eq!(result["status"], "success");
4240 assert_eq!(result["_wm_route"]["tool"], "memory.vector.search");
4241 }
4242
4243 #[tokio::test]
4244 async fn wm_routes_shadow_report_inside_meta_tool() {
4245 let store = test_store();
4249 let registry = test_registry_with(&store);
4250 let registry = register_meta_tools(
4251 ®istry,
4252 &store,
4253 std::sync::Arc::new(std::sync::RwLock::new(
4254 embedding_router::ShadowModeStats::default(),
4255 )),
4256 );
4257
4258 let wm = registry.get("wm").unwrap();
4259 let mut ctx = Context::new(BrainWave::Gamma);
4260 let result = wm
4261 .call(&mut ctx, json!({"route": "nlu.shadow_report"}))
4262 .await
4263 .unwrap();
4264
4265 assert_eq!(result["_wm_route"]["tool"], "nlu.shadow_report");
4266 assert!(
4267 result.get("total_queries").is_some(),
4268 "expected shadow report payload"
4269 );
4270 }
4271
4272 #[tokio::test]
4273 async fn memory_associate_and_find() {
4274 let store = test_store();
4275 let create = MemoryCreateTool::new(store.clone(), None, None);
4276 let mut ctx = Context::new(BrainWave::Gamma);
4277
4278 let r1 = create
4279 .call(&mut ctx, json!({"content": "source mem"}))
4280 .await
4281 .unwrap();
4282 let r2 = create
4283 .call(&mut ctx, json!({"content": "target mem"}))
4284 .await
4285 .unwrap();
4286 let id1 = r1["id"].as_str().unwrap();
4287 let id2 = r2["id"].as_str().unwrap();
4288
4289 let assoc = MemoryAssociateTool::new(store.clone());
4290 let result = assoc
4291 .call(
4292 &mut ctx,
4293 json!({"source": id1, "target": id2, "weight": 0.8}),
4294 )
4295 .await
4296 .unwrap();
4297 assert_eq!(result["status"], "success");
4298
4299 let find = MemoryAssociationsTool::new(store);
4300 let result = find
4301 .call(&mut ctx, json!({"id": id1, "direction": "from"}))
4302 .await
4303 .unwrap();
4304 assert_eq!(result["status"], "success");
4305 assert_eq!(result["returned"], 1);
4306 }
4307
4308 #[tokio::test]
4309 async fn karma_report_shows_entries() {
4310 let tmp = tempfile::tempdir().unwrap();
4311 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
4312 let ledger = Arc::new(KarmaLedger::new(store).unwrap());
4313
4314 ledger.record("test_tool", false, 0, true).unwrap();
4316 ledger.record("wasteful_tool", true, 0, true).unwrap();
4317
4318 let tool = KarmaReportTool::new(ledger);
4319 let mut ctx = Context::new(BrainWave::Gamma);
4320 let result = tool.call(&mut ctx, json!({"limit": 5})).await.unwrap();
4321 assert_eq!(result["status"], "success");
4322 assert_eq!(result["entry_count"], 2);
4323 assert_eq!(result["recent_entries"].as_array().unwrap().len(), 2);
4324 }
4325
4326 #[tokio::test]
4327 async fn dharma_status_returns_homeostasis() {
4328 let gate = Arc::new(DharmaGate::default());
4329 let tool = DharmaStatusTool::new(gate);
4330 let mut ctx = Context::new(BrainWave::Gamma);
4331 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4332 assert_eq!(result["status"], "success");
4333 assert!(result["homeostasis"]["health_score"].is_f64());
4334 assert!(result["sutras"]["ahimsa"].is_string());
4335 }
4336
4337 #[tokio::test]
4338 async fn wm_routes_remember_to_memory_create() {
4339 let store = test_store();
4340 let registry = test_registry_with(&store);
4341 let registry = register_meta_tools(
4342 ®istry,
4343 &store,
4344 std::sync::Arc::new(std::sync::RwLock::new(
4345 embedding_router::ShadowModeStats::default(),
4346 )),
4347 );
4348
4349 let wm = registry.get("wm").unwrap();
4350 let mut ctx = Context::new(BrainWave::Gamma);
4351 let result = wm
4352 .call(
4353 &mut ctx,
4354 json!({"thought": "remember that the API uses X-User-Id headers"}),
4355 )
4356 .await
4357 .unwrap();
4358
4359 assert_eq!(result["status"], "success");
4360 assert_eq!(result["_wm_route"]["tool"], "memory.create");
4361 assert!(result["id"].is_string());
4362 }
4363
4364 #[tokio::test]
4365 async fn wm_explicit_route() {
4366 let store = test_store();
4367 let registry = test_registry_with(&store);
4368 let registry = register_meta_tools(
4369 ®istry,
4370 &store,
4371 std::sync::Arc::new(std::sync::RwLock::new(
4372 embedding_router::ShadowModeStats::default(),
4373 )),
4374 );
4375
4376 let wm = registry.get("wm").unwrap();
4377 let mut ctx = Context::new(BrainWave::Gamma);
4378 let result = wm
4379 .call(
4380 &mut ctx,
4381 json!({
4382 "route": "gnosis"
4383 }),
4384 )
4385 .await
4386 .unwrap();
4387
4388 assert_eq!(result["status"], "success");
4389 assert_eq!(result["_wm_route"]["tool"], "gnosis");
4390 }
4391
4392 #[tokio::test]
4393 async fn wm_no_input_returns_error() {
4394 let store = test_store();
4395 let registry = test_registry_with(&store);
4396 let registry = register_meta_tools(
4397 ®istry,
4398 &store,
4399 std::sync::Arc::new(std::sync::RwLock::new(
4400 embedding_router::ShadowModeStats::default(),
4401 )),
4402 );
4403
4404 let wm = registry.get("wm").unwrap();
4405 let mut ctx = Context::new(BrainWave::Gamma);
4406 let result = wm.call(&mut ctx, json!({})).await.unwrap();
4407
4408 assert_eq!(result["status"], "error");
4409 }
4410
4411 #[tokio::test]
4412 async fn wm_missing_route_echoes_received_keys() {
4413 let store = test_store();
4418 let registry = test_registry_with(&store);
4419 let registry = register_meta_tools(
4420 ®istry,
4421 &store,
4422 std::sync::Arc::new(std::sync::RwLock::new(
4423 embedding_router::ShadowModeStats::default(),
4424 )),
4425 );
4426
4427 let wm = registry.get("wm").unwrap();
4428 let mut ctx = Context::new(BrainWave::Gamma);
4429 let result = wm
4430 .call(
4431 &mut ctx,
4432 json!({"content": "x", "turn_type": "summary", "importance": 0.5}),
4433 )
4434 .await
4435 .unwrap();
4436
4437 assert_eq!(result["status"], "error");
4438 let message = result["message"].as_str().unwrap();
4439 assert!(
4440 message.contains("received argument keys"),
4441 "error must disclose received keys, got: {message}"
4442 );
4443 for key in ["content", "turn_type", "importance"] {
4444 assert!(
4445 message.contains(key),
4446 "error must list received key '{key}', got: {message}"
4447 );
4448 }
4449 let empty = wm.call(&mut ctx, json!({})).await.unwrap();
4451 assert!(
4452 !empty["message"]
4453 .as_str()
4454 .unwrap()
4455 .contains("received argument keys: ["),
4456 "empty input must not list keys, got: {}",
4457 empty["message"]
4458 );
4459 }
4460
4461 #[tokio::test]
4462 async fn wm_unknown_tool_returns_error() {
4463 let store = test_store();
4464 let registry = test_registry_with(&store);
4465 let registry = register_meta_tools(
4466 ®istry,
4467 &store,
4468 std::sync::Arc::new(std::sync::RwLock::new(
4469 embedding_router::ShadowModeStats::default(),
4470 )),
4471 );
4472
4473 let wm = registry.get("wm").unwrap();
4474 let mut ctx = Context::new(BrainWave::Gamma);
4475 let result = wm
4476 .call(&mut ctx, json!({"route": "nonexistent.tool"}))
4477 .await
4478 .unwrap();
4479
4480 assert_eq!(result["status"], "error");
4481 assert!(result["message"].as_str().unwrap().contains("Unknown tool"));
4482 }
4483
4484 #[tokio::test]
4485 async fn wm_missing_arg_returns_hint() {
4486 let store = test_store();
4487 let registry = test_registry_with(&store);
4488 let registry = register_meta_tools(
4489 ®istry,
4490 &store,
4491 std::sync::Arc::new(std::sync::RwLock::new(
4492 embedding_router::ShadowModeStats::default(),
4493 )),
4494 );
4495
4496 let wm = registry.get("wm").unwrap();
4497 let mut ctx = Context::new(BrainWave::Gamma);
4498
4499 let result = wm
4501 .call(&mut ctx, json!({"route": "memory.read"}))
4502 .await
4503 .unwrap();
4504
4505 assert_eq!(result["status"], "error");
4506 assert!(
4507 result["message"]
4508 .as_str()
4509 .unwrap()
4510 .contains("Missing required argument")
4511 );
4512 assert!(result["hint"].as_str().unwrap().contains("uuid"));
4513 }
4514
4515 #[tokio::test]
4516 async fn wm_auto_route_missing_arg_returns_hint() {
4517 let store = test_store();
4518 let registry = test_registry_with(&store);
4519 let registry = register_meta_tools(
4520 ®istry,
4521 &store,
4522 std::sync::Arc::new(std::sync::RwLock::new(
4523 embedding_router::ShadowModeStats::default(),
4524 )),
4525 );
4526
4527 let wm = registry.get("wm").unwrap();
4528 let mut ctx = Context::new(BrainWave::Gamma);
4529
4530 let result = wm
4532 .call(&mut ctx, json!({"thought": "recall"}))
4533 .await
4534 .unwrap();
4535
4536 assert_eq!(result["status"], "error");
4537 assert!(result["hint"].as_str().unwrap().contains("uuid"));
4538 }
4539
4540 #[tokio::test]
4541 async fn wm_routes_karma_to_karma_report() {
4542 let tmp = tempfile::tempdir().unwrap();
4543 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
4544 let ledger = Arc::new(KarmaLedger::new(store.clone()).unwrap());
4545 let gate = Arc::new(DharmaGate::default());
4546
4547 let registry = ToolRegistry::new();
4548 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
4549 let spiral_tracker =
4550 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
4551 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4552 let registry = register_all(
4553 ®istry,
4554 &store,
4555 None,
4556 Some(ledger),
4557 &Some(gate),
4558 None,
4559 &None,
4560 associations,
4561 spiral_tracker,
4562 vector_store,
4563 None,
4564 None,
4565 None,
4566 None,
4567 None,
4568 None,
4569 None,
4570 std::sync::Arc::new(std::sync::Mutex::new(None)),
4571 None,
4572 None,
4573 None,
4574 );
4575 let registry = register_meta_tools(
4576 ®istry,
4577 &store,
4578 std::sync::Arc::new(std::sync::RwLock::new(
4579 embedding_router::ShadowModeStats::default(),
4580 )),
4581 );
4582
4583 let wm = registry.get("wm").unwrap();
4584 let mut ctx = Context::new(BrainWave::Gamma);
4585 let result = wm
4586 .call(&mut ctx, json!({"thought": "show me the karma report"}))
4587 .await
4588 .unwrap();
4589
4590 assert_eq!(result["status"], "success");
4591 assert_eq!(result["_wm_route"]["tool"], "karma.report");
4592 }
4593
4594 fn test_registry_with_pipeline(
4597 store: &Arc<MemoryStore>,
4598 ) -> (ToolRegistry, Arc<DispatchPipeline>) {
4599 let registry = test_registry_with(store);
4600 let pipeline = Arc::new(DispatchPipeline::with_defaults());
4601 let (registry, _router) = register_meta_tools_with_router(
4602 ®istry,
4603 store,
4604 std::sync::Arc::new(std::sync::RwLock::new(
4605 embedding_router::ShadowModeStats::default(),
4606 )),
4607 Some(pipeline.clone()),
4608 );
4609 (registry, pipeline)
4610 }
4611
4612 #[tokio::test]
4613 async fn wm_route_destructive_without_confirm_blocked_by_pipeline() {
4614 let store = test_store();
4615 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4616
4617 let wm = registry.get("wm").unwrap();
4618 let mut ctx = Context::new(BrainWave::Gamma);
4619 let result = wm
4620 .call(
4621 &mut ctx,
4622 json!({"route": "memory.delete", "args": {"id": "00000000-0000-0000-0000-000000000001"}}),
4623 )
4624 .await
4625 .unwrap();
4626
4627 assert_eq!(result["status"], "error");
4628 assert!(
4629 result["error"].as_str().unwrap().contains("destructive"),
4630 "expected destructive-gate message, got: {result}"
4631 );
4632 assert!(result["error"].as_str().unwrap().contains("confirm"));
4633 }
4634
4635 #[tokio::test]
4636 async fn wm_route_destructive_with_confirm_proceeds() {
4637 let store = test_store();
4638 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4639
4640 let memory = Memory::new(Galaxy::Codex, "delete me via wm route".into());
4642 let id = memory.metadata.id;
4643 store.put(Galaxy::Codex, &memory).unwrap();
4644
4645 let wm = registry.get("wm").unwrap();
4646 let mut ctx = Context::new(BrainWave::Gamma);
4647 let result = wm
4648 .call(
4649 &mut ctx,
4650 json!({"route": "memory.delete", "args": {"id": id.to_string(), "galaxy": "codex", "confirm": true}}),
4651 )
4652 .await
4653 .unwrap();
4654
4655 assert_eq!(result["status"], "success");
4656 assert_eq!(result["_wm_route"]["tool"], "memory.delete");
4657 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
4658 }
4659
4660 #[tokio::test]
4661 async fn wm_thought_cannot_reach_destructive_tool() {
4662 let store = test_store();
4663 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4664
4665 let wm = registry.get("wm").unwrap();
4666 let mut ctx = Context::new(BrainWave::Gamma);
4667 let result = wm
4670 .call(
4671 &mut ctx,
4672 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001"}),
4673 )
4674 .await
4675 .unwrap();
4676
4677 assert_eq!(result["status"], "error");
4678 assert!(
4679 result["message"]
4680 .as_str()
4681 .unwrap()
4682 .contains("cannot be reached via natural language"),
4683 "expected NLU hard-block message, got: {result}"
4684 );
4685 }
4686
4687 #[tokio::test]
4688 async fn wm_thought_cannot_reach_destructive_tool_even_with_confirm() {
4689 let store = test_store();
4690 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4691
4692 let wm = registry.get("wm").unwrap();
4693 let mut ctx = Context::new(BrainWave::Gamma);
4694 let result = wm
4698 .call(
4699 &mut ctx,
4700 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001", "args": {"confirm": true, "id": "00000000-0000-0000-0000-000000000001"}}),
4701 )
4702 .await
4703 .unwrap();
4704
4705 assert_eq!(result["status"], "error");
4706 assert!(
4707 result["message"]
4708 .as_str()
4709 .unwrap()
4710 .contains("cannot be reached via natural language")
4711 );
4712 }
4713
4714 #[tokio::test]
4719 async fn nlu_cannot_reach_any_destructive_tool() {
4720 let store = test_store();
4721 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4722 let wm = registry.get("wm").unwrap();
4723
4724 let destructive_tools: Vec<String> = registry
4727 .all_ref()
4728 .iter()
4729 .filter(|t| t.effects().destructive)
4730 .map(|t| t.name().to_string())
4731 .collect();
4732
4733 assert!(
4734 !destructive_tools.is_empty(),
4735 "registry must contain at least one destructive tool for this test to be meaningful"
4736 );
4737
4738 let mut ctx = Context::new(BrainWave::Gamma);
4739 for tool_name in &destructive_tools {
4740 let result = wm
4744 .call(
4745 &mut ctx,
4746 json!({
4747 "thought": tool_name,
4748 "args": {"confirm": true}
4749 }),
4750 )
4751 .await
4752 .unwrap();
4753
4754 let routed_tool = result
4759 .get("_wm_route")
4760 .and_then(|r| r.get("tool"))
4761 .and_then(|t| t.as_str())
4762 .unwrap_or("");
4763 let resolved_destructive = registry
4764 .get(routed_tool)
4765 .is_some_and(|t| t.effects().destructive);
4766 assert!(
4767 result["status"] != "success" || !resolved_destructive,
4768 "destructive tool '{tool_name}' executed via NLU (resolved as '{routed_tool}') — structural gate failed"
4769 );
4770
4771 if routed_tool == tool_name {
4774 assert!(
4775 result
4776 .get("message")
4777 .and_then(|m| m.as_str())
4778 .is_some_and(|m| m.contains("cannot be reached via natural language")),
4779 "destructive tool '{tool_name}' was routed to but gate message missing: {result}"
4780 );
4781 }
4782
4783 let nl_phrase = match tool_name.as_str() {
4788 "memory.delete" => "delete memory 00000000-0000-0000-0000-000000000001",
4789 "transaction.rollback" => "rollback the transaction",
4790 "galaxy.purge" => "purge galaxy codex",
4791 "galaxy.transfer" => "transfer galaxy codex to archive",
4792 "galaxy.restore" => "restore galaxy codex from snapshot",
4793 "memory.consolidate" => "consolidate memories in codex",
4794 "memory.deduplicate" => "deduplicate memories in codex",
4795 "karma.purge" => "purge karma ledger",
4796 "system.flush" => "flush low importance memories",
4797 "galaxy.cold_rotate" => "rotate telemetry noise to cold storage",
4798 _ => tool_name.as_str(),
4799 };
4800 let result2 = wm
4801 .call(&mut ctx, json!({"thought": nl_phrase}))
4802 .await
4803 .unwrap();
4804
4805 let routed_tool2 = result2
4806 .get("_wm_route")
4807 .and_then(|r| r.get("tool"))
4808 .and_then(|t| t.as_str())
4809 .unwrap_or("");
4810 let resolved_destructive2 = registry
4811 .get(routed_tool2)
4812 .is_some_and(|t| t.effects().destructive);
4813 assert!(
4814 result2["status"] != "success" || !resolved_destructive2,
4815 "destructive tool '{tool_name}' executed via NLU phrase '{nl_phrase}' (resolved as '{routed_tool2}') — structural gate failed"
4816 );
4817 if routed_tool2 == tool_name {
4818 assert!(
4819 result2
4820 .get("message")
4821 .and_then(|m| m.as_str())
4822 .is_some_and(|m| m.contains("cannot be reached via natural language")),
4823 "destructive tool '{tool_name}' was routed to via '{nl_phrase}' but gate message missing: {result2}"
4824 );
4825 }
4826 }
4827 }
4828
4829 #[tokio::test]
4830 async fn nlu_abstention_returns_error_for_unmatched_query() {
4831 let store = test_store();
4832 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4833 let wm = registry.get("wm").unwrap();
4834 let mut ctx = Context::new(BrainWave::Gamma);
4835
4836 let result = wm
4839 .call(&mut ctx, json!({"thought": "xyzzy quux blargh frobnicate"}))
4840 .await
4841 .unwrap();
4842
4843 assert_eq!(result["status"], "error");
4844 assert!(
4845 result
4846 .get("_wm_route")
4847 .and_then(|r| r.get("abstained"))
4848 .and_then(serde_json::Value::as_bool)
4849 .unwrap_or(false),
4850 "expected abstained=true, got: {result}"
4851 );
4852 assert!(
4853 result["message"]
4854 .as_str()
4855 .unwrap()
4856 .contains("Could not confidently match"),
4857 "expected abstention message, got: {result}"
4858 );
4859 }
4860
4861 #[tokio::test]
4862 async fn nlu_abstention_does_not_fire_for_explicit_route() {
4863 let store = test_store();
4864 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4865 let wm = registry.get("wm").unwrap();
4866 let mut ctx = Context::new(BrainWave::Gamma);
4867
4868 let result = wm.call(&mut ctx, json!({"route": "gnosis"})).await.unwrap();
4871
4872 assert_eq!(result["status"], "success");
4873 assert!(
4874 !result
4875 .get("_wm_route")
4876 .and_then(|r| r.get("abstained"))
4877 .and_then(serde_json::Value::as_bool)
4878 .unwrap_or(false),
4879 "explicit route should not abstain, got: {result}"
4880 );
4881 }
4882
4883 struct FakeVecEmbedder;
4886
4887 impl wm_memory::Embedder for FakeVecEmbedder {
4888 fn embed_batch(&self, texts: &[&str]) -> wm_core::Result<Vec<Vec<f32>>> {
4889 Ok(texts
4890 .iter()
4891 .map(|t| {
4892 let mut v = vec![0.0_f32; 16];
4893 for (i, b) in t.bytes().take(16).enumerate() {
4894 v[i] = f32::from(b) / 255.0;
4895 }
4896 v
4897 })
4898 .collect())
4899 }
4900 fn dimension(&self) -> usize {
4901 16
4902 }
4903 fn is_available(&self) -> bool {
4904 true
4905 }
4906 fn backend_name(&self) -> &'static str {
4907 "fake"
4908 }
4909 }
4910
4911 #[tokio::test]
4912 async fn wm_classify_async_routes_off_thread_with_embedding_router() {
4913 let store = test_store();
4914 let registry = test_registry_with(&store);
4915 let shadow = std::sync::Arc::new(std::sync::RwLock::new(
4916 embedding_router::ShadowModeStats::default(),
4917 ));
4918 let router = embedding_router::EmbeddingRouter::with_descriptions(
4919 Box::new(FakeVecEmbedder),
4920 embedding_router::tool_descriptions(),
4921 )
4922 .expect("fake-embedder router should build");
4923 let mut meta = WmMetaTool::with_router_shadow_stats_and_pipeline(
4924 std::sync::Arc::new(registry),
4925 wm_memory::create_embedder(),
4926 shadow,
4927 None,
4928 );
4929 meta.embedding_router = Some(std::sync::Arc::new(router));
4930
4931 let (tool, conf, emb) = meta.classify_async("remember the meeting notes").await;
4934 assert!(!tool.is_empty());
4935 assert!(conf >= 0.0);
4936 assert!(
4937 emb.is_some(),
4938 "query embedding should be returned for OATS reuse"
4939 );
4940 }
4941
4942 #[tokio::test]
4943 async fn tools_list_shows_all() {
4944 let store = test_store();
4945 let registry = test_registry_with(&store);
4946 let registry = register_meta_tools(
4947 ®istry,
4948 &store,
4949 std::sync::Arc::new(std::sync::RwLock::new(
4950 embedding_router::ShadowModeStats::default(),
4951 )),
4952 );
4953
4954 let list = registry.get("tools.list").unwrap();
4955 let mut ctx = Context::new(BrainWave::Gamma);
4956 let result = list.call(&mut ctx, json!({})).await.unwrap();
4957
4958 assert_eq!(result["status"], "success");
4959 assert!(result["total"].as_u64().unwrap() >= 7);
4960 }
4961
4962 #[tokio::test]
4963 async fn tools_list_exposes_curated_argument_schemas() {
4964 let store = test_store();
4965 let registry = test_registry_with(&store);
4966 let registry = register_meta_tools(
4967 ®istry,
4968 &store,
4969 std::sync::Arc::new(std::sync::RwLock::new(
4970 embedding_router::ShadowModeStats::default(),
4971 )),
4972 );
4973
4974 let list = registry.get("tools.list").unwrap();
4975 let mut ctx = Context::new(BrainWave::Gamma);
4976 let result = list.call(&mut ctx, json!({})).await.unwrap();
4977
4978 let tools = result["tools"].as_array().unwrap();
4979 let create = tools
4980 .iter()
4981 .find(|t| t["name"] == "memory.create")
4982 .expect("tools.list must include memory.create");
4983 let schema = &create["input_schema"];
4984 assert_eq!(schema["type"], "object");
4985 assert!(
4986 schema["properties"].get("content").is_some(),
4987 "memory.create schema must describe content, got: {schema}"
4988 );
4989 assert!(
4990 schema["required"]
4991 .as_array()
4992 .unwrap()
4993 .iter()
4994 .any(|r| r == "content"),
4995 "memory.create schema must require content"
4996 );
4997
4998 let rollback = tools
4999 .iter()
5000 .find(|t| t["name"] == "transaction.rollback")
5001 .expect("tools.list must include transaction.rollback");
5002 assert!(
5003 rollback["input_schema"]["required"]
5004 .as_array()
5005 .unwrap()
5006 .iter()
5007 .any(|r| r == "confirm"),
5008 "transaction.rollback schema must require confirm"
5009 );
5010
5011 let annotations = &create["annotations"];
5013 assert_eq!(annotations["readOnlyHint"], false, "memory.create writes");
5014 assert_eq!(annotations["destructiveHint"], false);
5015 assert_eq!(
5016 rollback["annotations"]["destructiveHint"], true,
5017 "transaction.rollback is destructive"
5018 );
5019 let list_tool = tools
5020 .iter()
5021 .find(|t| t["name"] == "memory.list")
5022 .expect("tools.list must include memory.list");
5023 assert_eq!(
5024 list_tool["annotations"]["readOnlyHint"], true,
5025 "memory.list is read-only"
5026 );
5027 }
5028
5029 #[tokio::test]
5030 async fn tools_list_filters_by_brain_wave() {
5031 let store = test_store();
5032 let registry = test_registry_with(&store);
5033 let registry = register_meta_tools(
5034 ®istry,
5035 &store,
5036 std::sync::Arc::new(std::sync::RwLock::new(
5037 embedding_router::ShadowModeStats::default(),
5038 )),
5039 );
5040
5041 let list = registry.get("tools.list").unwrap();
5042
5043 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5045 let result_gamma = list.call(&mut ctx_gamma, json!({})).await.unwrap();
5046 let gamma_count = result_gamma["total"].as_u64().unwrap();
5047 assert!(gamma_count >= 7);
5048
5049 let mut ctx_alpha = Context::new(BrainWave::Alpha);
5051 let result_alpha = list.call(&mut ctx_alpha, json!({})).await.unwrap();
5052 let alpha_count = result_alpha["total"].as_u64().unwrap();
5053 assert!(alpha_count < gamma_count);
5054 assert!(alpha_count > 0);
5055
5056 let mut ctx_delta = Context::new(BrainWave::Delta);
5058 let result_delta = list.call(&mut ctx_delta, json!({})).await.unwrap();
5059 assert_eq!(result_delta["total"], 0);
5060 }
5061
5062 #[tokio::test]
5063 async fn gnosis_includes_brain_wave_and_tool_count() {
5064 let store = test_store();
5065 let registry = test_registry_with(&store);
5066 let registry = register_meta_tools(
5067 ®istry,
5068 &store,
5069 std::sync::Arc::new(std::sync::RwLock::new(
5070 embedding_router::ShadowModeStats::default(),
5071 )),
5072 );
5073
5074 let gnosis = registry.get("gnosis").unwrap();
5075 let mut ctx = Context::new(BrainWave::Gamma);
5076 let result = gnosis.call(&mut ctx, json!({})).await.unwrap();
5077
5078 assert_eq!(result["status"], "success");
5079 assert_eq!(result["brain_wave"], "Gamma");
5080 assert!(result["available_tools"].as_u64().unwrap() >= 9);
5081 }
5082
5083 #[tokio::test]
5084 async fn gnosis_available_tools_is_total_registered() {
5085 let store = test_store();
5086 let registry = test_registry_with(&store);
5087 let registry = register_meta_tools(
5088 ®istry,
5089 &store,
5090 std::sync::Arc::new(std::sync::RwLock::new(
5091 embedding_router::ShadowModeStats::default(),
5092 )),
5093 );
5094
5095 let gnosis = registry.get("gnosis").unwrap();
5096
5097 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5100 let result_gamma = gnosis.call(&mut ctx_gamma, json!({})).await.unwrap();
5101 let gamma_tools = result_gamma["available_tools"].as_u64().unwrap();
5102
5103 let mut ctx_delta = Context::new(BrainWave::Delta);
5104 let result_delta = gnosis.call(&mut ctx_delta, json!({})).await.unwrap();
5105 let delta_tools = result_delta["available_tools"].as_u64().unwrap();
5106
5107 assert_eq!(gamma_tools, delta_tools);
5108 assert!(
5109 gamma_tools >= 9,
5110 "expected at least 9 registered tools, got {gamma_tools}"
5111 );
5112 }
5113
5114 #[tokio::test]
5115 async fn expansion_brings_tool_count_to_50() {
5116 let store = test_store();
5117 let registry = test_registry_with(&store);
5118 let registry = register_meta_tools(
5119 ®istry,
5120 &store,
5121 std::sync::Arc::new(std::sync::RwLock::new(
5122 embedding_router::ShadowModeStats::default(),
5123 )),
5124 );
5125
5126 let list = registry.get("tools.list").unwrap();
5127 let mut ctx = Context::new(BrainWave::Gamma);
5128 let result = list.call(&mut ctx, json!({})).await.unwrap();
5129
5130 let total = result["total"].as_u64().unwrap();
5131 assert!(
5132 total >= 50,
5133 "Expected 50+ tools after expansion, got {total}"
5134 );
5135 }
5136
5137 #[tokio::test]
5140 async fn nlu_routes_consolidate() {
5141 let (tool, conf) = WmMetaTool::classify("consolidate memories in codex");
5142 assert_eq!(tool, "memory.consolidate");
5143 assert!(conf > 0.0);
5144 }
5145
5146 #[tokio::test]
5147 async fn nlu_routes_decay() {
5148 let (tool, conf) = WmMetaTool::classify("decay old memories");
5149 assert_eq!(tool, "memory.decay");
5150 assert!(conf > 0.0);
5151 }
5152
5153 #[tokio::test]
5154 async fn nlu_routes_batch_read() {
5155 let (tool, conf) = WmMetaTool::classify("batch read these memories");
5156 assert_eq!(tool, "memory.batch_read");
5157 assert!(conf > 0.0);
5158 }
5159
5160 #[tokio::test]
5161 async fn nlu_routes_update() {
5162 let (tool, conf) = WmMetaTool::classify("update memory tags");
5163 assert_eq!(tool, "memory.update");
5164 assert!(conf > 0.0);
5165 }
5166
5167 #[tokio::test]
5168 async fn nlu_routes_tag() {
5169 let (tool, conf) = WmMetaTool::classify("add tag to memory");
5170 assert_eq!(tool, "memory.tag");
5171 assert!(conf > 0.0);
5172 }
5173
5174 #[tokio::test]
5175 async fn nlu_routes_memory_stats() {
5176 let (tool, conf) = WmMetaTool::classify("memory stats for codex");
5177 assert_eq!(tool, "memory.stats");
5178 assert!(conf > 0.0);
5179 }
5180
5181 #[tokio::test]
5182 async fn nlu_routes_hybrid_recall() {
5183 let (tool, conf) = WmMetaTool::classify("hybrid recall for rust");
5184 assert_eq!(tool, "memory.hybrid_recall");
5185 assert!(conf > 0.0);
5186 }
5187
5188 #[tokio::test]
5189 async fn nlu_routes_count() {
5190 let (tool, conf) = WmMetaTool::classify("count memories in codex");
5191 assert_eq!(tool, "memory.count");
5192 assert!(conf > 0.0);
5193 }
5194
5195 #[tokio::test]
5196 async fn nlu_routes_tags() {
5197 let (tool, conf) = WmMetaTool::classify("list tags in codex");
5198 assert_eq!(tool, "memory.tags");
5199 assert!(conf > 0.0);
5200 }
5201
5202 #[tokio::test]
5203 async fn nlu_routes_associate_mine() {
5204 let (tool, conf) = WmMetaTool::classify("mine associations in codex");
5205 assert_eq!(tool, "memory.associate_mine");
5206 assert!(conf > 0.0);
5207 }
5208
5209 #[tokio::test]
5210 async fn nlu_routes_session_start() {
5211 let (tool, conf) = WmMetaTool::classify("start session research");
5212 assert_eq!(tool, "session.start");
5213 assert!(conf > 0.0);
5214 }
5215
5216 #[tokio::test]
5217 async fn nlu_routes_session_end() {
5218 let (tool, conf) = WmMetaTool::classify("end session 12345");
5219 assert_eq!(tool, "session.end");
5220 assert!(conf > 0.0);
5221 }
5222
5223 #[tokio::test]
5224 async fn nlu_routes_session_list() {
5225 let (tool, conf) = WmMetaTool::classify("list sessions");
5226 assert_eq!(tool, "session.list");
5227 assert!(conf > 0.0);
5228 }
5229
5230 #[tokio::test]
5231 async fn nlu_routes_citta_status() {
5232 let (tool, conf) = WmMetaTool::classify("citta status");
5233 assert_eq!(tool, "citta.status");
5234 assert!(conf > 0.0);
5235 }
5236
5237 #[tokio::test]
5238 async fn nlu_routes_citta_reflect() {
5239 let (tool, conf) = WmMetaTool::classify("reflect on recent events");
5240 assert_eq!(tool, "citta.reflect");
5241 assert!(conf > 0.0);
5242 }
5243
5244 #[tokio::test]
5245 async fn nlu_routes_coherence() {
5246 let (tool, conf) = WmMetaTool::classify("check coherence");
5247 assert_eq!(tool, "citta.coherence");
5248 assert!(conf > 0.0);
5249 }
5250
5251 #[tokio::test]
5252 async fn nlu_routes_dream_status() {
5253 let (tool, conf) = WmMetaTool::classify("dream cycle status");
5254 assert_eq!(tool, "dream.status");
5255 assert!(conf > 0.0);
5256 }
5257
5258 #[tokio::test]
5259 async fn nlu_routes_dream_trigger() {
5260 let (tool, conf) = WmMetaTool::classify("trigger dream cycle");
5261 assert_eq!(tool, "dream.trigger");
5262 assert!(conf > 0.0);
5263 }
5264
5265 #[tokio::test]
5266 async fn nlu_routes_effectiveness() {
5267 let (tool, conf) = WmMetaTool::classify("tool effectiveness report");
5268 assert_eq!(tool, "tools.effectiveness_report");
5269 assert!(conf > 0.0);
5270 }
5271
5272 #[tokio::test]
5273 async fn nlu_routes_retire() {
5274 let (tool, conf) = WmMetaTool::classify("retire tool memory.old");
5275 assert_eq!(tool, "tools.retire");
5276 assert!(conf > 0.0);
5277 }
5278
5279 #[tokio::test]
5280 async fn nlu_routes_pattern_search() {
5281 let (tool, conf) = WmMetaTool::classify("pattern search for rust");
5282 assert_eq!(tool, "pattern.search");
5283 assert!(conf > 0.0);
5284 }
5285
5286 #[tokio::test]
5287 async fn nlu_routes_salience() {
5288 let (tool, conf) = WmMetaTool::classify("salience spotlight");
5289 assert_eq!(tool, "salience.spotlight");
5290 assert!(conf > 0.0);
5291 }
5292
5293 #[tokio::test]
5294 async fn nlu_routes_serendipity() {
5295 let (tool, conf) = WmMetaTool::classify("serendipity surface");
5296 assert_eq!(tool, "serendipity.surface");
5297 assert!(conf > 0.0);
5298 }
5299
5300 #[tokio::test]
5301 async fn nlu_routes_constellation_detect() {
5302 let (tool, conf) = WmMetaTool::classify("detect clusters");
5303 assert_eq!(tool, "constellation.detect");
5304 assert!(conf > 0.0);
5305 }
5306
5307 #[tokio::test]
5308 async fn nlu_routes_constellation_list() {
5309 let (tool, conf) = WmMetaTool::classify("list constellations");
5310 assert_eq!(tool, "constellation.list");
5311 assert!(conf > 0.0);
5312 }
5313
5314 #[tokio::test]
5315 async fn nlu_routes_galaxy_stats() {
5316 let (tool, conf) = WmMetaTool::classify("galaxy stats");
5317 assert_eq!(tool, "galaxy.stats");
5318 assert!(conf > 0.0);
5319 }
5320
5321 #[tokio::test]
5322 async fn nlu_routes_galaxy_export() {
5323 let (tool, conf) = WmMetaTool::classify("export galaxy codex");
5324 assert_eq!(tool, "galaxy.export");
5325 assert!(conf > 0.0);
5326 }
5327
5328 #[tokio::test]
5329 async fn nlu_routes_galaxy_import() {
5330 let (tool, conf) = WmMetaTool::classify("import galaxy codex");
5331 assert_eq!(tool, "galaxy.import");
5332 assert!(conf > 0.0);
5333 }
5334
5335 #[tokio::test]
5336 async fn nlu_routes_karma_history() {
5337 let (tool, conf) = WmMetaTool::classify("karma history");
5338 assert_eq!(tool, "karma.history");
5339 assert!(conf > 0.0);
5340 }
5341
5342 #[tokio::test]
5343 async fn nlu_routes_karma_clear() {
5344 let (tool, conf) = WmMetaTool::classify("clear karma");
5345 assert_eq!(tool, "karma.clear");
5346 assert!(conf > 0.0);
5347 }
5348
5349 #[tokio::test]
5350 async fn nlu_routes_dharma_rules() {
5351 let (tool, conf) = WmMetaTool::classify("dharma rules");
5352 assert_eq!(tool, "dharma.rules");
5353 assert!(conf > 0.0);
5354 }
5355
5356 #[tokio::test]
5357 async fn nlu_routes_dharma_audit() {
5358 let (tool, conf) = WmMetaTool::classify("dharma audit");
5359 assert_eq!(tool, "dharma.audit");
5360 assert!(conf > 0.0);
5361 }
5362
5363 #[tokio::test]
5364 async fn nlu_routes_dharma_profiles() {
5365 let (tool, conf) = WmMetaTool::classify("dharma profiles");
5366 assert_eq!(tool, "dharma.profiles");
5367 assert!(conf > 0.0);
5368 }
5369
5370 #[tokio::test]
5371 async fn nlu_routes_agent_register() {
5372 let (tool, conf) = WmMetaTool::classify("register agent worker-1");
5373 assert_eq!(tool, "agent.register");
5374 assert!(conf > 0.0);
5375 }
5376
5377 #[tokio::test]
5378 async fn nlu_routes_agent_list() {
5379 let (tool, conf) = WmMetaTool::classify("list agents");
5380 assert_eq!(tool, "agent.list");
5381 assert!(conf > 0.0);
5382 }
5383
5384 #[tokio::test]
5385 async fn nlu_routes_agent_heartbeat() {
5386 let (tool, conf) = WmMetaTool::classify("heartbeat for agent");
5387 assert_eq!(tool, "agent.heartbeat");
5388 assert!(conf > 0.0);
5389 }
5390
5391 #[tokio::test]
5392 async fn nlu_routes_task_distribute() {
5393 let (tool, conf) = WmMetaTool::classify("distribute task analyze data");
5394 assert_eq!(tool, "task.distribute");
5395 assert!(conf > 0.0);
5396 }
5397
5398 #[tokio::test]
5399 async fn nlu_routes_task_status() {
5400 let (tool, conf) = WmMetaTool::classify("task status");
5401 assert_eq!(tool, "task.status");
5402 assert!(conf > 0.0);
5403 }
5404
5405 #[tokio::test]
5406 async fn nlu_routes_system_health() {
5407 let (tool, conf) = WmMetaTool::classify("system health check");
5408 assert_eq!(tool, "system.health");
5409 assert!(conf > 0.0);
5410 }
5411
5412 #[tokio::test]
5413 async fn nlu_routes_system_config() {
5414 let (tool, conf) = WmMetaTool::classify("system config");
5415 assert_eq!(tool, "system.config");
5416 assert!(conf > 0.0);
5417 }
5418
5419 #[tokio::test]
5420 async fn nlu_routes_system_flush() {
5421 let (tool, conf) = WmMetaTool::classify("flush old memories");
5422 assert_eq!(tool, "system.flush");
5423 assert!(conf > 0.0);
5424 }
5425
5426 #[tokio::test]
5427 async fn nlu_routes_memory_nearby() {
5428 let (tool, conf) = WmMetaTool::classify("nearby memories in codex");
5429 assert_eq!(tool, "memory.nearby");
5430 assert!(conf > 0.0);
5431 }
5432
5433 #[tokio::test]
5434 async fn nlu_routes_empty_to_gnosis() {
5435 let (tool, conf) = WmMetaTool::classify("");
5436 assert_eq!(tool, "gnosis");
5437 assert_eq!(conf, 0.0);
5438 }
5439
5440 #[tokio::test]
5441 async fn nlu_routes_unknown_to_gnosis() {
5442 let (tool, conf) = WmMetaTool::classify("xyzzy frobnicate");
5443 assert_eq!(tool, "gnosis");
5444 assert_eq!(conf, 0.0);
5445 }
5446
5447 #[tokio::test]
5448 async fn nlu_extract_payload_memory_search() {
5449 let (param, value) =
5450 WmMetaTool::extract_payload("search for rust patterns", "memory.search").unwrap();
5451 assert_eq!(param, "query");
5452 assert_eq!(value, "rust patterns");
5453 }
5454
5455 #[tokio::test]
5456 async fn nlu_extract_payload_session_start() {
5457 let (param, value) =
5461 WmMetaTool::extract_payload("start session research", "session.start").unwrap();
5462 assert_eq!(param, "title");
5463 assert_eq!(value, "research");
5464 }
5465
5466 #[tokio::test]
5467 async fn nlu_extract_payload_agent_register() {
5468 let (param, value) =
5469 WmMetaTool::extract_payload("register agent worker-1", "agent.register").unwrap();
5470 assert_eq!(param, "name");
5471 assert_eq!(value, "worker-1");
5472 }
5473
5474 #[tokio::test]
5475 async fn nlu_extract_payload_task_distribute() {
5476 let (param, value) =
5477 WmMetaTool::extract_payload("distribute task analyze data", "task.distribute").unwrap();
5478 assert_eq!(param, "task");
5479 assert_eq!(value, "analyze data");
5480 }
5481
5482 #[tokio::test]
5483 async fn nlu_count_unique_patterns() {
5484 let inputs = [
5486 "remember",
5487 "recall",
5488 "list memories",
5489 "delete memory",
5490 "search",
5491 "query",
5492 "associate",
5493 "associations",
5494 "consolidate",
5495 "decay",
5496 "batch read",
5497 "update memory",
5498 "tag memory",
5499 "memory stats",
5500 "hybrid recall",
5501 "count memories",
5502 "list tags",
5503 "mine associations",
5504 "start session",
5505 "checkpoint",
5506 "recall session",
5507 "end session",
5508 "list sessions",
5509 "citta status",
5510 "reflect",
5511 "coherence",
5512 "dream status",
5513 "trigger dream",
5514 "effectiveness",
5515 "retire tool",
5516 "pattern search",
5517 "salience",
5518 "serendipity",
5519 "detect clusters",
5520 "list constellations",
5521 "galaxy stats",
5522 "export galaxy",
5523 "import galaxy",
5524 "karma",
5525 "karma history",
5526 "clear karma",
5527 "dharma rules",
5528 "dharma audit",
5529 "dharma profiles",
5530 "dharma",
5531 "register agent",
5532 "list agents",
5533 "heartbeat",
5534 "distribute task",
5535 "task status",
5536 "system health",
5537 "system config",
5538 "flush",
5539 "tools",
5540 "nearby memories",
5541 ];
5542 let mut tools: std::collections::HashSet<&str> = std::collections::HashSet::new();
5543 for input in &inputs {
5544 let (tool, _) = WmMetaTool::classify(input);
5545 tools.insert(tool);
5546 }
5547 assert!(
5549 tools.len() >= 30,
5550 "Expected 30+ unique NLU targets, got {}",
5551 tools.len()
5552 );
5553 }
5554
5555 #[tokio::test]
5556 async fn nlu_routes_shadow_report() {
5557 let (tool, conf) = WmMetaTool::classify("shadow mode disagreement report");
5558 assert_eq!(tool, "nlu.shadow_report");
5559 assert!(conf > 0.0);
5560 }
5561
5562 #[tokio::test]
5563 async fn nlu_routes_oats_report() {
5564 let (tool, conf) = WmMetaTool::classify("oats disagreement nlu router");
5565 assert_eq!(tool, "nlu.shadow_report");
5566 assert!(conf > 0.0);
5567 }
5568}