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