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
15pub use expansion::lkep::{
16 LkepError, LkepExecTool, decode_lkep, parse_lkep_expression, primary_arg_for_route,
17 resolve_arg, resolve_route,
18};
19
20use async_trait::async_trait;
21
22use std::sync::Arc;
23
24use serde_json::{Value, json};
25use wm_cognitive::GanYingBus;
26use wm_core::{
27 Capability, Context, EffectRow, EpisodicCapturePolicy, EpisodicKind, EpisodicRecord, Galaxy,
28 Gana, Provenance, ProvenanceSource, Resource, Tool, ToolStats,
29};
30use wm_dispatch::{DispatchPipeline, ToolRegistry, ToolRegistryBuilder};
31use wm_governance::{DharmaGate, KarmaLedger, ResourceRules};
32use wm_memory::{
33 Association, AssociationStore, ConversationalSearch, Memory, MemoryQuery, MemoryStore,
34 RecallEngine, SearchEngine, VectorStore,
35};
36use wm_substrate::SubstrateMonitor;
37use wm_substrate::anomaly::AnomalyDetector;
38use wm_substrate::homeostatic::HomeostaticLoop;
39use wm_substrate::sensorimotor::{ReflexLoop, SensorimotorBus};
40
41use crate::expansion::common::{
42 bool_prop, fresh_write_galaxies, int_prop, memory_galaxy_reads, memory_galaxy_writes, num_prop,
43 schema, str_array_prop, str_prop,
44};
45
46pub(crate) const GLYPH_ROUTES: &[(&str, &str)] = &[
58 ("memory.search", "Ms"),
59 ("memory.create", "Mc"),
60 ("memory.read", "Mr"),
61 ("memory.hybrid_recall", "Mh"),
62 ("memory.list", "Ml"),
63 ("session.record", "Sr"),
64 ("session.continuity", "Sc"),
65 ("session.checkpoint", "Sk"),
66 ("dharma.escalate", "De"),
67 ("dharma.review_queue", "Dq"),
68 ("dharma.resolve_review", "Dr"),
69 ("dharma.rules", "Du"),
70 ("graph.walk", "Gw"),
71 ("citta.status", "Cs"),
72 ("dream.status", "Ds"),
73 ("smarana.status", "Sm"),
74 ("tools.list", "Tl"),
75 ("agent.list", "Al"),
76 ("karma.report", "Kr"),
77 ("memory.search", "忆"),
79 ("memory.search", "索"),
80 ("memory.create", "录"),
81 ("memory.create", "存"),
82 ("memory.read", "读"),
83 ("memory.hybrid_recall", "回"),
84 ("session.continuity", "续"),
85 ("session.checkpoint", "契"),
86 ("session.record", "记"),
87 ("citta.status", "心"),
88 ("dharma.rules", "律"),
89 ("karma.report", "业"),
90 ("tools.list", "具"),
91];
92
93pub(crate) const GLYPH_ARGS: &[(&str, &str)] = &[
94 ("route", "r"),
95 ("args", "a"),
96 ("query", "q"),
97 ("limit", "n"),
98 ("content", "c"),
99 ("id", "i"),
100 ("tags", "t"),
101 ("title", "h"),
102 ("session_id", "s"),
103 ("role", "o"),
104 ("turn_type", "y"),
105 ("importance", "p"),
106 ("tool", "T"),
107 ("action", "N"),
108 ("purpose", "u"),
109 ("decision", "d"),
110 ("score", "e"),
111 ("depth", "D"),
112 ("scope", "S"),
113 ("name", "m"),
114 ("arguments", "g"),
115 ("query", "问"),
117 ("query", "寻"),
118 ("limit", "数"),
119 ("content", "文"),
120 ("tags", "标"),
121 ("scope", "界"),
122 ("id", "号"),
123];
124
125#[must_use]
127pub fn glyph_mode_from_env() -> bool {
128 std::env::var("WM_GLYPH").is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
129}
130
131pub(crate) fn glyph_lookup<'a>(book: &'a [(&'a str, &'a str)], from: &str) -> Option<&'a str> {
132 book.iter().find(|(k, _)| *k == from).map(|(_, code)| *code)
133}
134
135pub(crate) fn glyph_reverse<'a>(book: &'a [(&'a str, &'a str)], code: &str) -> Option<&'a str> {
136 book.iter().find(|(_, v)| *v == code).map(|(k, _)| *k)
137}
138
139#[must_use]
143pub fn decode_glyph(args: &Value) -> Option<Value> {
144 let obj = args.as_object()?;
145 let rcode = obj.get("r")?.as_str()?;
146 let route = glyph_reverse(GLYPH_ROUTES, rcode)?;
147 let mut out = serde_json::Map::new();
148 out.insert("route".into(), Value::String(route.to_string()));
149 let a = obj.get("a").cloned().unwrap_or_else(|| json!({}));
150 if let Some(aobj) = a.as_object() {
151 let mut decoded = serde_json::Map::new();
152 for (k, v) in aobj {
153 let name = glyph_reverse(GLYPH_ARGS, k).unwrap_or(k);
154 decoded.insert(name.to_string(), v.clone());
155 }
156 out.insert("args".into(), Value::Object(decoded));
157 }
158 Some(Value::Object(out))
159}
160
161#[must_use]
164pub fn encode_glyph(route: &str, args: &Value) -> Value {
165 let code = glyph_lookup(GLYPH_ROUTES, route).unwrap_or(route);
166 let mut a = serde_json::Map::new();
167 if let Some(obj) = args.as_object() {
168 for (k, v) in obj {
169 let kc = glyph_lookup(GLYPH_ARGS, k).unwrap_or(k);
170 a.insert(kc.to_string(), v.clone());
171 }
172 }
173 json!({ "r": code, "a": Value::Object(a) })
174}
175
176const NLU_LOW_CONFIDENCE: f64 = 0.30;
184const NLU_ABSTENTION_THRESHOLD: f64 = 0.15;
185
186fn capture_explicit_memory(
191 store: &MemoryStore,
192 memory: &Memory,
193 kind: EpisodicKind,
194 source: ProvenanceSource,
195 session_id: Option<uuid::Uuid>,
196 sequence: u64,
197) {
198 let record = explicit_memory_record(memory, kind, source, session_id, sequence);
199 if let Err(error) = store
200 .episodic()
201 .append_explicit(&record, EpisodicCapturePolicy::explicit_only())
202 {
203 tracing::warn!(
204 memory_id = %memory.metadata.id,
205 "episodic capture failed after legacy write: {error}"
206 );
207 }
208}
209
210fn explicit_memory_record(
211 memory: &Memory,
212 kind: EpisodicKind,
213 source: ProvenanceSource,
214 session_id: Option<uuid::Uuid>,
215 sequence: u64,
216) -> EpisodicRecord {
217 let resolved_kind = resolve_episodic_kind(memory, kind);
218 EpisodicRecord::new(
219 session_id,
220 sequence,
221 resolved_kind,
222 memory.content.clone(),
223 Provenance::new(source),
224 )
225 .with_id(memory.metadata.id)
226 .with_visibility(memory.metadata.is_private, memory.metadata.model_exclude)
227}
228
229fn resolve_episodic_kind(memory: &Memory, default: EpisodicKind) -> EpisodicKind {
232 let tags = &memory.metadata.tags;
233 if tags.iter().any(|t| t == "user") {
234 EpisodicKind::UserStatement
235 } else if tags.iter().any(|t| t == "assistant") {
236 EpisodicKind::AssistantResponse
237 } else {
238 default
239 }
240}
241
242fn capture_explicit_memories(
243 store: &MemoryStore,
244 memories: &[(Galaxy, Memory)],
245 kind: EpisodicKind,
246 source: ProvenanceSource,
247 session_id: Option<uuid::Uuid>,
248) {
249 if memories.is_empty() {
250 return;
251 }
252 let records: Vec<EpisodicRecord> = memories
253 .iter()
254 .enumerate()
255 .map(|(sequence, (_, memory))| {
256 explicit_memory_record(memory, kind, source, session_id, sequence as u64)
257 })
258 .collect();
259 if let Err(error) = store
260 .episodic()
261 .append_explicit_batch(&records, EpisodicCapturePolicy::explicit_only())
262 {
263 tracing::warn!("episodic batch capture failed after legacy write: {error}");
264 }
265}
266
267fn attestation_agent_id(ctx: &Context) -> String {
274 ctx.session_id
275 .map(|u| u.to_string())
276 .or_else(|| ctx.user_id.clone())
277 .unwrap_or_else(|| "local".to_string())
278}
279
280fn node_attestation_key() -> Option<String> {
284 std::env::var(wm_memory::attestation::ATTESTATION_KEY_ENV)
285 .ok()
286 .filter(|k| !k.trim().is_empty())
287}
288
289fn attest_created_memory(
296 store: &MemoryStore,
297 galaxy: Galaxy,
298 id: uuid::Uuid,
299 record_hash: &str,
300 ctx: &Context,
301 key_hex: Option<&str>,
302) -> (bool, Option<String>) {
303 let key_hex = match key_hex {
304 Some(k) if !k.trim().is_empty() => k,
305 _ => return (false, Some("node key unavailable".to_string())),
306 };
307 let agent_id = attestation_agent_id(ctx);
308 let timestamp = wm_core::time::now_unix_secs();
309 let payload = wm_memory::attestation::attestation_payload(
310 galaxy.db_name(),
311 &id.to_string(),
312 record_hash,
313 &agent_id,
314 timestamp,
315 );
316 let Some((public_key_hex, signature_hex)) =
317 wm_memory::attestation::sign_attestation(&payload, key_hex)
318 else {
319 tracing::warn!("creation attestation skipped for memory {id}: key material invalid");
320 return (false, Some("node key invalid".to_string()));
321 };
322 let entry = wm_memory::attestation::RecordAttestation {
323 domain: wm_memory::attestation::ATTESTATION_DOMAIN.to_string(),
324 galaxy: galaxy.db_name().to_string(),
325 memory_id: id.to_string(),
326 record_hash: record_hash.to_string(),
327 agent_id,
328 timestamp,
329 public_key_hex,
330 signature_hex,
331 };
332 if let Err(e) = store.record_attestation(galaxy, id, &entry) {
333 tracing::warn!("creation attestation write failed for memory {id}: {e}");
334 return (false, Some("attestation store write failed".to_string()));
335 }
336 (true, None)
337}
338
339pub struct MemoryCreateTool {
344 store: Arc<MemoryStore>,
345 search: Option<Arc<SearchEngine>>,
346 recall: Option<Arc<RecallEngine>>,
347 stats: ToolStats,
348 effects: EffectRow,
349 attestation_key: Option<String>,
353}
354
355impl MemoryCreateTool {
356 pub fn new(
357 store: Arc<MemoryStore>,
358 search: Option<Arc<SearchEngine>>,
359 recall: Option<Arc<RecallEngine>>,
360 ) -> Self {
361 Self {
362 store,
363 search,
364 recall,
365 stats: ToolStats::default(),
366 effects: EffectRow {
367 writes: fresh_write_galaxies(),
371 invokes: vec![Capability::MemoryWrite],
372 sandbox: wm_core::Sandbox::StoreScoped,
376 ..Default::default()
377 },
378 attestation_key: node_attestation_key(),
379 }
380 }
381
382 #[must_use]
385 pub fn with_attestation_key(
386 store: Arc<MemoryStore>,
387 search: Option<Arc<SearchEngine>>,
388 recall: Option<Arc<RecallEngine>>,
389 attestation_key: Option<String>,
390 ) -> Self {
391 let mut tool = Self::new(store, search, recall);
392 tool.attestation_key = attestation_key;
393 tool
394 }
395}
396
397#[async_trait]
398impl Tool for MemoryCreateTool {
399 fn name(&self) -> &str {
400 "memory.create"
401 }
402 fn gana(&self) -> Gana {
403 Gana::Encampment
404 }
405 fn effects(&self) -> &EffectRow {
406 &self.effects
407 }
408 fn input_schema(&self) -> Value {
409 schema(
410 &json!({
411 "content": str_prop("Memory content (text)"),
412 "galaxy": str_prop("Target galaxy (default codex)"),
413 "tags": str_array_prop("Optional tags"),
414 "title": str_prop("Optional human-readable title (envelope v2)"),
415 "topic": str_prop("Optional topic label for subject-scoped retrieval (envelope v2)"),
416 "importance": num_prop("Optional importance 0.0-1.0 (write gate applies class ceilings/floors when the class is recognized)"),
417 "source": str_prop("Authorship claim: user (user-dictated content, trust 1.0) | agent (default, trust 0.7) | other free-form class (trust 0.7)"),
418 }),
419 &["content"],
420 )
421 }
422 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
423 let content = args
424 .get("content")
425 .and_then(|v| v.as_str())
426 .ok_or_else(|| wm_core::CoreError::InvalidArgs("content (string) required".into()))?;
427 let galaxy_str = args
428 .get("galaxy")
429 .and_then(|v| v.as_str())
430 .unwrap_or("codex");
431 let galaxy = parse_galaxy(galaxy_str)?;
432 let tags: Vec<String> = args
433 .get("tags")
434 .and_then(|v| v.as_array())
435 .map(|a| {
436 a.iter()
437 .filter_map(|v| v.as_str().map(String::from))
438 .collect()
439 })
440 .unwrap_or_default();
441
442 if let Some(search) = &self.search {
443 if search.is_readonly() {
444 return Err(wm_core::CoreError::InvalidArgs(
445 "read-only mode: memory.create disabled (another process owns the index)"
446 .into(),
447 ));
448 }
449 }
450 let kinds = wm_memory::credential_shaped_content(content);
454 let warnings: Vec<String> = kinds
455 .iter()
456 .map(|k| {
457 format!(
458 "content looks like a credential ({k}) — {}",
459 wm_memory::CREDENTIAL_ADVICE
460 )
461 })
462 .collect();
463 let mut memory = Memory::new(galaxy, content.to_string());
464 memory.metadata.tags = tags;
465 memory.metadata.title = args
468 .get("title")
469 .and_then(Value::as_str)
470 .map(str::trim)
471 .filter(|s| !s.is_empty())
472 .map(String::from);
473 memory.metadata.topic = args
474 .get("topic")
475 .and_then(Value::as_str)
476 .map(str::trim)
477 .filter(|s| !s.is_empty())
478 .map(String::from);
479 if let Some(importance) =
484 wm_dispatch::write_gate::parse_importance_value(args.get("importance"))
485 .map_err(wm_core::CoreError::InvalidArgs)?
486 {
487 memory.metadata.importance = importance;
488 }
489 memory.metadata.class = wm_memory::typology::detect_class(content, &memory.metadata.tags);
490 memory.metadata.tier = memory.metadata.class.map_or(
491 wm_memory::memory::Tier::Working,
492 wm_memory::typology::initial_tier,
493 );
494 let claimed_source = args
500 .get("source")
501 .and_then(Value::as_str)
502 .map(str::trim)
503 .filter(|s| !s.is_empty());
504 let (source, trust) = match claimed_source {
505 Some("user") => ("user", 1.0),
506 Some(other) => (other, 0.7),
507 None => ("agent", 0.7),
508 };
509 memory.metadata.source = source.to_string();
510 memory.metadata.source_trust = trust;
511 let id = memory.metadata.id;
512
513 if let Some(recall) = &self.recall {
516 if let Err(e) = recall.store_with_embedding(galaxy, &memory) {
517 tracing::warn!("RecallEngine store_with_embedding failed for memory {id}: {e}");
518 self.store.put(galaxy, &memory)?;
520 if let Some(search) = &self.search {
521 if let Err(e) = (|| {
522 let mut writer = search.writer()?;
523 search.add_document(
524 &mut writer,
525 &id.to_string(),
526 galaxy.db_name(),
527 content,
528 &memory.metadata.tags,
529 memory.metadata.created_at.timestamp(),
530 )?;
531 search.commit(&mut writer)?;
532 Ok::<(), wm_core::CoreError>(())
533 })() {
534 tracing::warn!("Tantivy indexing failed for memory {id}: {e}");
535 }
536 }
537 }
538 } else {
539 self.store.put(galaxy, &memory)?;
540 if let Some(search) = &self.search {
542 if let Err(e) = (|| {
543 let mut writer = search.writer()?;
544 search.add_document(
545 &mut writer,
546 &id.to_string(),
547 galaxy.db_name(),
548 content,
549 &memory.metadata.tags,
550 memory.metadata.created_at.timestamp(),
551 )?;
552 search.commit(&mut writer)?;
553 Ok::<(), wm_core::CoreError>(())
554 })() {
555 tracing::warn!("Tantivy indexing failed for memory {id}: {e}");
556 }
557 }
558 }
559
560 capture_explicit_memory(
561 &self.store,
562 &memory,
563 EpisodicKind::Observation,
564 if source == "user" {
567 ProvenanceSource::User
568 } else {
569 ProvenanceSource::Agent
570 },
571 ctx.session_id,
572 0,
573 );
574
575 let (attested, attested_reason) = attest_created_memory(
579 &self.store,
580 galaxy,
581 id,
582 &memory.metadata.content_hash,
583 ctx,
584 self.attestation_key.as_deref(),
585 );
586
587 let mut response = json!({
588 "status": "success",
589 "id": id.to_string(),
590 "galaxy": galaxy.db_name(),
591 "content_hash": memory.metadata.content_hash,
592 "source": source,
593 "source_trust": trust,
594 "attested": attested,
595 });
596 if let Some(reason) = attested_reason {
597 response["attested_reason"] = json!(reason);
598 }
599 if !warnings.is_empty() {
600 response["warnings"] = json!(warnings);
601 }
602 Ok(response)
603 }
604 fn stats(&self) -> &ToolStats {
605 &self.stats
606 }
607}
608
609pub struct MemoryBatchCreateTool {
617 store: Arc<MemoryStore>,
618 search: Option<Arc<SearchEngine>>,
619 recall: Option<Arc<RecallEngine>>,
620 stats: ToolStats,
621 effects: EffectRow,
622 attestation_key: Option<String>,
625}
626
627impl MemoryBatchCreateTool {
628 pub fn new(
629 store: Arc<MemoryStore>,
630 search: Option<Arc<SearchEngine>>,
631 recall: Option<Arc<RecallEngine>>,
632 ) -> Self {
633 Self {
634 store,
635 search,
636 recall,
637 stats: ToolStats::default(),
638 effects: EffectRow {
639 writes: fresh_write_galaxies(),
640 invokes: vec![Capability::MemoryWrite],
641 sandbox: wm_core::Sandbox::StoreScoped,
643 ..Default::default()
644 },
645 attestation_key: node_attestation_key(),
646 }
647 }
648
649 #[must_use]
651 pub fn with_attestation_key(
652 store: Arc<MemoryStore>,
653 search: Option<Arc<SearchEngine>>,
654 recall: Option<Arc<RecallEngine>>,
655 attestation_key: Option<String>,
656 ) -> Self {
657 let mut tool = Self::new(store, search, recall);
658 tool.attestation_key = attestation_key;
659 tool
660 }
661}
662
663#[async_trait]
664impl Tool for MemoryBatchCreateTool {
665 fn name(&self) -> &str {
666 "memory.batch_create"
667 }
668 fn gana(&self) -> Gana {
669 Gana::Encampment
670 }
671 fn effects(&self) -> &EffectRow {
672 &self.effects
673 }
674 fn input_schema(&self) -> Value {
675 schema(
676 &json!({
677 "items": {
678 "type": "array",
679 "description": "Array of {content, galaxy?, tags?} objects",
680 "items": {
681 "type": "object",
682 "properties": {
683 "content": str_prop("Memory content (text)"),
684 "galaxy": str_prop("Target galaxy (default codex)"),
685 "tags": str_array_prop("Optional tags"),
686 "importance": num_prop("Optional importance 0.0-1.0 (write gate applies class ceilings/floors when the class is recognized)"),
687 },
688 "required": ["content"],
689 },
690 },
691 }),
692 &["items"],
693 )
694 }
695 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
696 let items = args
697 .get("items")
698 .and_then(|v| v.as_array())
699 .ok_or_else(|| wm_core::CoreError::InvalidArgs("items (array) required".into()))?;
700
701 if let Some(search) = &self.search {
702 if search.is_readonly() {
703 return Err(wm_core::CoreError::InvalidArgs(
704 "read-only mode: memory.batch_create disabled (another process owns the index)"
705 .into(),
706 ));
707 }
708 }
709
710 let mut ids: Vec<String> = Vec::new();
711 let mut all_items_user_claimed = true;
716 let mut cred_kinds: Vec<&'static str> = Vec::new();
719 let mut writer_guard = if self.recall.is_none() {
723 if let Some(search) = &self.search {
724 Some(search.writer()?)
725 } else {
726 None
727 }
728 } else {
729 None
730 };
731
732 let mut memories: Vec<(Galaxy, Memory)> = Vec::new();
734
735 for item in items {
736 let content = item
737 .get("content")
738 .and_then(|v| v.as_str())
739 .ok_or_else(|| {
740 wm_core::CoreError::InvalidArgs("each item needs content (string)".into())
741 })?;
742 let galaxy_str = item
743 .get("galaxy")
744 .and_then(|v| v.as_str())
745 .unwrap_or("codex");
746 let galaxy = parse_galaxy(galaxy_str)?;
747 let tags: Vec<String> = item
748 .get("tags")
749 .and_then(|v| v.as_array())
750 .map(|a| {
751 a.iter()
752 .filter_map(|v| v.as_str().map(String::from))
753 .collect()
754 })
755 .unwrap_or_default();
756
757 let mut memory = Memory::new(galaxy, content.to_string());
758 memory.metadata.tags = tags;
759 if let Some(importance) =
766 wm_dispatch::write_gate::parse_importance_value(item.get("importance"))
767 .map_err(wm_core::CoreError::InvalidArgs)?
768 {
769 memory.metadata.importance = importance;
770 }
771 memory.metadata.class =
772 wm_memory::typology::detect_class(content, &memory.metadata.tags);
773 memory.metadata.tier = memory.metadata.class.map_or(
774 wm_memory::memory::Tier::Working,
775 wm_memory::typology::initial_tier,
776 );
777 let claimed_source = item
781 .get("source")
782 .and_then(Value::as_str)
783 .map(str::trim)
784 .filter(|s| !s.is_empty());
785 let (source, trust) = match claimed_source {
786 Some("user") => ("user", 1.0),
787 Some(other) => (other, 0.7),
788 None => ("agent", 0.7),
789 };
790 if source != "user" {
791 all_items_user_claimed = false;
792 }
793 memory.metadata.source = source.to_string();
794 memory.metadata.source_trust = trust;
795 let id = memory.metadata.id;
796 ids.push(id.to_string());
797 for k in wm_memory::credential_shaped_content(content) {
798 if !cred_kinds.contains(&k) {
799 cred_kinds.push(k);
800 }
801 }
802 memories.push((galaxy, memory));
803 }
804
805 if let Some(recall) = &self.recall {
807 let entries: Vec<(Galaxy, &Memory)> = memories.iter().map(|(g, m)| (*g, m)).collect();
808 match recall.store_batch_with_embedding(&entries) {
809 Ok(n) => {
810 tracing::info!("batch_create: embedded {n} memories in single batch");
811 }
812 Err(e) => {
813 tracing::warn!(
814 "batch_create: store_batch_with_embedding failed ({e}), falling back to per-item"
815 );
816 let mut fallback_writer = if writer_guard.is_none() {
820 if let Some(search) = &self.search {
821 search.writer().ok()
822 } else {
823 None
824 }
825 } else {
826 None
827 };
828 for (galaxy, memory) in &memories {
829 self.store.put(*galaxy, memory)?;
830 let writer_slot = writer_guard.as_mut().or(fallback_writer.as_mut());
831 if let Some(guard) = writer_slot {
832 if let Some(search) = &self.search {
833 if let Err(e) = search.add_document(
834 guard,
835 &memory.metadata.id.to_string(),
836 galaxy.db_name(),
837 &memory.content,
838 &memory.metadata.tags,
839 memory.metadata.created_at.timestamp(),
840 ) {
841 tracing::warn!(
842 "Tantivy indexing failed for memory {}: {e}",
843 memory.metadata.id
844 );
845 }
846 }
847 }
848 }
849 if let Some(mut guard) = fallback_writer {
851 if let Some(search) = &self.search {
852 if let Err(e) = search.commit(&mut guard) {
853 tracing::warn!("Tantivy fallback commit failed: {e}");
854 }
855 }
856 }
857 }
858 }
859 } else {
860 for (galaxy, memory) in &memories {
862 self.store.put(*galaxy, memory)?;
863 if let Some(ref mut guard) = writer_guard {
864 if let Some(search) = &self.search {
865 if let Err(e) = search.add_document(
866 &mut *guard,
867 &memory.metadata.id.to_string(),
868 galaxy.db_name(),
869 &memory.content,
870 &memory.metadata.tags,
871 memory.metadata.created_at.timestamp(),
872 ) {
873 tracing::warn!(
874 "Tantivy indexing failed for memory {}: {e}",
875 memory.metadata.id
876 );
877 }
878 }
879 }
880 }
881 }
882
883 if let Some(ref mut guard) = writer_guard {
885 if let Some(search) = &self.search {
886 if let Err(e) = search.commit(&mut *guard) {
887 tracing::warn!("Tantivy batch commit failed: {e}");
888 }
889 }
890 }
891
892 capture_explicit_memories(
893 &self.store,
894 &memories,
895 EpisodicKind::Observation,
896 if all_items_user_claimed {
899 ProvenanceSource::User
900 } else {
901 ProvenanceSource::Agent
902 },
903 ctx.session_id,
904 );
905
906 let mut attested_count = 0usize;
909 for (galaxy, memory) in &memories {
910 let (ok, _) = attest_created_memory(
911 &self.store,
912 *galaxy,
913 memory.metadata.id,
914 &memory.metadata.content_hash,
915 ctx,
916 self.attestation_key.as_deref(),
917 );
918 attested_count += usize::from(ok);
919 }
920
921 let mut response = json!({
922 "status": "success",
923 "count": ids.len(),
924 "ids": ids,
925 "attested_count": attested_count,
926 });
927 if !cred_kinds.is_empty() {
928 response["warnings"] = json!(
929 cred_kinds
930 .iter()
931 .map(|k| format!(
932 "some items look like credentials ({k}) — {}",
933 wm_memory::CREDENTIAL_ADVICE
934 ))
935 .collect::<Vec<String>>()
936 );
937 }
938 Ok(response)
939 }
940 fn stats(&self) -> &ToolStats {
941 &self.stats
942 }
943}
944
945pub struct MemoryReadTool {
949 store: Arc<MemoryStore>,
950 stats: ToolStats,
951 effects: EffectRow,
952}
953
954impl MemoryReadTool {
955 pub fn new(store: Arc<MemoryStore>) -> Self {
956 Self {
957 store,
958 stats: ToolStats::default(),
959 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
960 }
961 }
962}
963
964#[async_trait]
965impl Tool for MemoryReadTool {
966 fn name(&self) -> &str {
967 "memory.read"
968 }
969 fn gana(&self) -> Gana {
970 Gana::WinnowingBasket
971 }
972 fn effects(&self) -> &EffectRow {
973 &self.effects
974 }
975 fn input_schema(&self) -> Value {
976 schema(
977 &json!({
978 "id": str_prop("Memory UUID"),
979 "galaxy": str_prop("Galaxy containing the memory (default codex)"),
980 }),
981 &["id"],
982 )
983 }
984 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
985 let id_str = args
986 .get("id")
987 .and_then(|v| v.as_str())
988 .ok_or_else(|| wm_core::CoreError::InvalidArgs("id (string) required".into()))?;
989 let id = uuid::Uuid::parse_str(id_str)
990 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid UUID: {e}")))?;
991 let galaxy_str = args
992 .get("galaxy")
993 .and_then(|v| v.as_str())
994 .unwrap_or("codex");
995 let galaxy = parse_galaxy(galaxy_str)?;
996
997 let memory = if let Some(memory) = self.store.get(galaxy, id)? {
998 memory
999 } else {
1000 let Some(record) = self.store.get_cold_record(id)? else {
1007 return Ok(json!({
1008 "status": "not_found",
1009 "id": id_str,
1010 "galaxy": galaxy.db_name(),
1011 }));
1012 };
1013 if record.id != id || record.galaxy != galaxy {
1014 return Ok(json!({
1015 "status": "not_found",
1016 "id": id_str,
1017 "galaxy": galaxy.db_name(),
1018 }));
1019 }
1020 let memory = record.decompress()?;
1021 if memory.metadata.id != id
1022 || memory.metadata.galaxy != galaxy
1023 || memory.metadata.content_hash != record.content_hash
1024 || wm_memory::content_hash(&memory.content) != record.content_hash
1025 {
1026 return Err(wm_core::CoreError::Memory(
1027 "cold memory header/payload integrity mismatch".into(),
1028 ));
1029 }
1030 memory
1031 };
1032 if memory.metadata.is_private {
1033 return Ok(json!({
1036 "status": "not_found",
1037 "id": id_str,
1038 "galaxy": galaxy.db_name(),
1039 }));
1040 }
1041 Ok(json!({
1042 "status": "success",
1043 "id": memory.metadata.id.to_string(),
1044 "galaxy": memory.metadata.galaxy.db_name(),
1045 "content": memory.content,
1046 "tags": memory.metadata.tags,
1047 "created_at": memory.metadata.created_at.to_rfc3339(),
1048 }))
1049 }
1050 fn stats(&self) -> &ToolStats {
1051 &self.stats
1052 }
1053}
1054
1055pub struct MemoryListTool {
1059 store: Arc<MemoryStore>,
1060 stats: ToolStats,
1061 effects: EffectRow,
1062}
1063
1064impl MemoryListTool {
1065 pub fn new(store: Arc<MemoryStore>) -> Self {
1066 Self {
1067 store,
1068 stats: ToolStats::default(),
1069 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
1070 }
1071 }
1072}
1073
1074#[async_trait]
1075impl Tool for MemoryListTool {
1076 fn name(&self) -> &str {
1077 "memory.list"
1078 }
1079 fn gana(&self) -> Gana {
1080 Gana::WinnowingBasket
1081 }
1082 fn effects(&self) -> &EffectRow {
1083 &self.effects
1084 }
1085 fn input_schema(&self) -> Value {
1086 schema(
1087 &json!({
1088 "galaxy": str_prop("Galaxy to list (default codex)"),
1089 "limit": int_prop("Maximum entries (default 20)"),
1090 "offset": int_prop("Skip this many matching entries before returning (default 0)"),
1091 "exclude_tags": {
1092 "type": "array",
1093 "items": {"type": "string"},
1094 "description": "Drop memories carrying any of these tags",
1095 },
1096 }),
1097 &[],
1098 )
1099 }
1100 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1101 let galaxy_str = args
1102 .get("galaxy")
1103 .and_then(|v| v.as_str())
1104 .unwrap_or("codex");
1105 let limit = args
1106 .get("limit")
1107 .and_then(serde_json::Value::as_u64)
1108 .unwrap_or(20) as usize;
1109 let offset = args
1110 .get("offset")
1111 .and_then(serde_json::Value::as_u64)
1112 .unwrap_or(0) as usize;
1113 let exclude_tags: Vec<String> = args
1114 .get("exclude_tags")
1115 .and_then(|v| v.as_array())
1116 .map(|arr| {
1117 arr.iter()
1118 .filter_map(|t| t.as_str().map(String::from))
1119 .collect()
1120 })
1121 .unwrap_or_default();
1122 let galaxy = parse_galaxy(galaxy_str)?;
1123
1124 let memories = self.store.scan(galaxy, 10_000)?;
1128 let total = self.store.count(galaxy)?;
1129
1130 let visible: Vec<&wm_memory::Memory> = memories
1131 .iter()
1132 .filter(|m| crate::expansion::common::mcp_visible(m))
1133 .filter(|m| crate::expansion::common::validity_visible(m))
1134 .filter(|m| {
1135 !exclude_tags
1136 .iter()
1137 .any(|t| m.metadata.tags.iter().any(|mt| mt == t))
1138 })
1139 .collect();
1140 let entries: Vec<Value> = visible
1141 .iter()
1142 .skip(offset)
1143 .take(limit)
1144 .map(|m| {
1145 json!({
1146 "id": m.metadata.id.to_string(),
1147 "content_preview": m.content.chars().take(80).collect::<String>(),
1148 "tags": m.metadata.tags,
1149 "created_at": m.metadata.created_at.to_rfc3339(),
1150 })
1151 })
1152 .collect();
1153
1154 Ok(json!({
1155 "status": "success",
1156 "galaxy": galaxy.db_name(),
1157 "total": total,
1158 "matched": visible.len(),
1159 "offset": offset,
1160 "returned": entries.len(),
1161 "memories": entries,
1162 }))
1163 }
1164 fn stats(&self) -> &ToolStats {
1165 &self.stats
1166 }
1167}
1168
1169pub struct GnosisTool {
1173 store: Arc<MemoryStore>,
1174 tool_count: usize,
1175 stats: ToolStats,
1176 effects: EffectRow,
1177}
1178
1179impl GnosisTool {
1180 pub fn new(store: Arc<MemoryStore>) -> Self {
1181 Self {
1182 store,
1183 tool_count: 0,
1184 stats: ToolStats::default(),
1185 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
1186 }
1187 }
1188
1189 pub fn with_tool_count(store: Arc<MemoryStore>, tool_count: usize) -> Self {
1191 Self {
1192 store,
1193 tool_count,
1194 stats: ToolStats::default(),
1195 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
1196 }
1197 }
1198}
1199
1200#[async_trait]
1201impl Tool for GnosisTool {
1202 fn input_schema(&self) -> Value {
1203 schema(&json!({}), &[])
1204 }
1205 fn name(&self) -> &str {
1206 "gnosis"
1207 }
1208 fn gana(&self) -> Gana {
1209 Gana::Root
1210 }
1211 fn effects(&self) -> &EffectRow {
1212 &self.effects
1213 }
1214 async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
1215 let mut galaxy_stats = serde_json::Map::new();
1216 for galaxy in Galaxy::all() {
1217 let count = self.store.count(galaxy).unwrap_or(0);
1218 if count > 0 {
1219 galaxy_stats.insert(galaxy.db_name().to_string(), json!(count));
1220 }
1221 }
1222
1223 Ok(json!({
1224 "status": "success",
1225 "version": env!("CARGO_PKG_VERSION"),
1226 "store_path": self.store.path().display().to_string(),
1227 "brain_wave": format!("{:?}", ctx.brain_wave),
1228 "available_tools": self.tool_count,
1229 "galaxies_with_data": galaxy_stats.len(),
1230 "galaxy_counts": galaxy_stats,
1231 "ganas": Gana::COUNT,
1232 "galaxies": Galaxy::COUNT,
1233 }))
1234 }
1235 fn stats(&self) -> &ToolStats {
1236 &self.stats
1237 }
1238}
1239
1240pub struct ToolsListTool {
1244 registry: Arc<ToolRegistry>,
1245 stats: ToolStats,
1246 effects: EffectRow,
1247}
1248
1249impl ToolsListTool {
1250 #[must_use]
1251 pub fn new(registry: Arc<ToolRegistry>) -> Self {
1252 Self {
1253 registry,
1254 stats: ToolStats::default(),
1255 effects: EffectRow::pure(),
1256 }
1257 }
1258}
1259
1260#[async_trait]
1261impl Tool for ToolsListTool {
1262 fn input_schema(&self) -> Value {
1263 schema(&json!({}), &[])
1264 }
1265 fn name(&self) -> &str {
1266 "tools.list"
1267 }
1268 fn gana(&self) -> Gana {
1269 Gana::Ghost
1270 }
1271 fn effects(&self) -> &EffectRow {
1272 &self.effects
1273 }
1274 async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
1275 let available = self.registry.available_in(ctx.brain_wave);
1276 let tools: Vec<Value> = available
1277 .iter()
1278 .map(|t| {
1279 let effects = t.effects();
1282 json!({
1283 "name": t.name(),
1284 "gana": format!("{:?}", t.gana()),
1285 "description": t.description(),
1286 "input_schema": t.input_schema(),
1287 "annotations": {
1288 "readOnlyHint": effects.writes.is_empty(),
1289 "destructiveHint": effects.destructive,
1290 },
1291 })
1292 })
1293 .collect();
1294 Ok(json!({
1295 "status": "success",
1296 "brain_wave": format!("{:?}", ctx.brain_wave),
1297 "total": tools.len(),
1298 "tools": tools,
1299 }))
1300 }
1301 fn stats(&self) -> &ToolStats {
1302 &self.stats
1303 }
1304}
1305
1306pub struct MemoryDeleteTool {
1318 store: Arc<MemoryStore>,
1319 search: Option<Arc<SearchEngine>>,
1320 stats: ToolStats,
1321 effects: EffectRow,
1322}
1323
1324impl MemoryDeleteTool {
1325 pub fn new(store: Arc<MemoryStore>, search: Option<Arc<SearchEngine>>) -> Self {
1326 Self {
1327 store,
1328 search,
1329 stats: ToolStats::default(),
1330 effects: EffectRow {
1331 writes: memory_galaxy_writes(),
1334 reads: memory_galaxy_reads(),
1335 invokes: vec![Capability::MemoryWrite],
1336 destructive: true,
1337 sandbox: wm_core::Sandbox::StoreScoped,
1339 ..Default::default()
1340 },
1341 }
1342 }
1343}
1344
1345#[async_trait]
1346impl Tool for MemoryDeleteTool {
1347 fn name(&self) -> &str {
1348 "memory.delete"
1349 }
1350 fn gana(&self) -> Gana {
1351 Gana::Encampment
1352 }
1353 fn effects(&self) -> &EffectRow {
1354 &self.effects
1355 }
1356 fn input_schema(&self) -> Value {
1357 schema(
1358 &json!({
1359 "id": str_prop("Memory UUID"),
1360 "galaxy": str_prop("Galaxy containing the memory (optional; when omitted the id is resolved across all memory galaxies)"),
1361 "confirm": bool_prop("Required — memory.delete is destructive"),
1362 }),
1363 &["id", "confirm"],
1364 )
1365 }
1366 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1367 let id_str = args
1368 .get("id")
1369 .and_then(|v| v.as_str())
1370 .ok_or_else(|| wm_core::CoreError::InvalidArgs("id (string) required".into()))?;
1371 let id = uuid::Uuid::parse_str(id_str)
1372 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid UUID: {e}")))?;
1373
1374 if let Some(search) = &self.search {
1375 if search.is_readonly() {
1376 return Err(wm_core::CoreError::InvalidArgs(
1377 "read-only mode: memory.delete disabled (another process owns the index)"
1378 .into(),
1379 ));
1380 }
1381 }
1382
1383 let targets: Vec<Galaxy> = match args.get("galaxy").and_then(|v| v.as_str()) {
1384 Some(g) => vec![parse_galaxy(g)?],
1385 None => Galaxy::memory_galaxies().to_vec(),
1386 };
1387
1388 let mut deleted_from: Vec<&str> = Vec::new();
1389 for galaxy in targets {
1390 if self.store.delete(galaxy, id)? {
1391 deleted_from.push(galaxy.db_name());
1392 }
1393 }
1394
1395 if !deleted_from.is_empty() {
1397 if let Some(search) = &self.search {
1398 if let Err(e) = (|| {
1399 let mut writer = search.writer()?;
1400 search.delete_document(&mut writer, id_str)?;
1401 search.commit(&mut writer)?;
1402 Ok::<(), wm_core::CoreError>(())
1403 })() {
1404 tracing::warn!("Tantivy de-indexing failed for memory {id_str}: {e}");
1405 }
1406 }
1407 }
1408
1409 if deleted_from.is_empty() {
1410 return Ok(json!({
1411 "status": "not_found",
1412 "id": id_str,
1413 "hint": "id not found in any memory galaxy; pass an explicit galaxy to target one"
1414 }));
1415 }
1416
1417 let mut body = serde_json::Map::new();
1418 body.insert("status".into(), json!("success"));
1419 body.insert("id".into(), json!(id_str));
1420 if args.get("galaxy").and_then(|v| v.as_str()).is_some() {
1421 body.insert("galaxy".into(), json!(deleted_from[0]));
1422 }
1423 body.insert(
1424 "galaxies".into(),
1425 json!(deleted_from.iter().map(|g| json!(g)).collect::<Vec<_>>()),
1426 );
1427 body.insert("deleted".into(), json!(deleted_from.len()));
1428 Ok(Value::Object(body))
1429 }
1430 fn stats(&self) -> &ToolStats {
1431 &self.stats
1432 }
1433}
1434
1435pub struct MemoryBatchDeleteTool {
1443 store: Arc<MemoryStore>,
1444 search: Option<Arc<SearchEngine>>,
1445 stats: ToolStats,
1446 effects: EffectRow,
1447}
1448
1449impl MemoryBatchDeleteTool {
1450 pub fn new(store: Arc<MemoryStore>, search: Option<Arc<SearchEngine>>) -> Self {
1451 Self {
1452 store,
1453 search,
1454 stats: ToolStats::default(),
1455 effects: EffectRow {
1456 writes: memory_galaxy_writes(),
1457 reads: memory_galaxy_reads(),
1458 invokes: vec![Capability::MemoryWrite],
1459 destructive: true,
1460 ..Default::default()
1461 },
1462 }
1463 }
1464}
1465
1466#[async_trait]
1467impl Tool for MemoryBatchDeleteTool {
1468 fn name(&self) -> &str {
1469 "memory.batch_delete"
1470 }
1471 fn gana(&self) -> Gana {
1472 Gana::Encampment
1473 }
1474 fn effects(&self) -> &EffectRow {
1475 &self.effects
1476 }
1477 fn input_schema(&self) -> Value {
1478 schema(
1479 &json!({
1480 "ids": {"type": "array", "items": {"type": "string"},
1481 "description": "Memory UUIDs to delete (max 200000)"},
1482 "confirm": bool_prop("Required — memory.batch_delete is destructive"),
1483 }),
1484 &["ids", "confirm"],
1485 )
1486 }
1487 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1488 const MAX_IDS: usize = 200_000;
1489 if !args
1490 .get("confirm")
1491 .and_then(serde_json::Value::as_bool)
1492 .unwrap_or(false)
1493 {
1494 return Err(wm_core::CoreError::InvalidArgs(
1495 "confirm (bool) required — memory.batch_delete is destructive".into(),
1496 ));
1497 }
1498 let ids: Vec<String> = args
1499 .get("ids")
1500 .and_then(|v| v.as_array())
1501 .map(|a| {
1502 a.iter()
1503 .filter_map(|v| v.as_str().map(String::from))
1504 .collect()
1505 })
1506 .ok_or_else(|| {
1507 wm_core::CoreError::InvalidArgs("ids (array of UUID strings) required".into())
1508 })?;
1509 if ids.is_empty() {
1510 return Ok(json!({"status": "success", "requested": 0, "deleted": 0, "not_found": 0}));
1511 }
1512 if ids.len() > MAX_IDS {
1513 return Err(wm_core::CoreError::InvalidArgs(format!(
1514 "ids capped at {MAX_IDS}; split the batch"
1515 )));
1516 }
1517
1518 if let Some(search) = &self.search {
1519 if search.is_readonly() {
1520 return Err(wm_core::CoreError::InvalidArgs(
1521 "read-only mode: memory.batch_delete disabled (another process owns the index)"
1522 .into(),
1523 ));
1524 }
1525 }
1526
1527 let targets: Vec<Galaxy> = Galaxy::memory_galaxies().to_vec();
1528 let mut deleted_ids: Vec<(String, Vec<&str>)> = Vec::new();
1529 let mut not_found: usize = 0;
1530 for id_str in &ids {
1531 let Ok(id) = uuid::Uuid::parse_str(id_str) else {
1532 not_found += 1;
1533 continue;
1534 };
1535 let mut deleted_from: Vec<&str> = Vec::new();
1536 for galaxy in targets.iter().copied() {
1537 if self.store.delete(galaxy, id)? {
1538 deleted_from.push(galaxy.db_name());
1539 }
1540 }
1541 if deleted_from.is_empty() {
1542 not_found += 1;
1543 } else {
1544 deleted_ids.push((id_str.clone(), deleted_from));
1545 }
1546 }
1547
1548 if !deleted_ids.is_empty() {
1550 if let Some(search) = &self.search {
1551 if let Err(e) = (|| {
1552 let mut writer = search.writer()?;
1553 for (id_str, _) in &deleted_ids {
1554 search.delete_document(&mut writer, id_str)?;
1555 }
1556 search.commit(&mut writer)?;
1557 Ok::<(), wm_core::CoreError>(())
1558 })() {
1559 tracing::warn!(
1560 "Tantivy batch de-indexing failed ({} ids): {e}",
1561 deleted_ids.len()
1562 );
1563 }
1564 }
1565 }
1566
1567 Ok(json!({
1568 "status": "success",
1569 "requested": ids.len(),
1570 "deleted": deleted_ids.len(),
1571 "not_found": not_found,
1572 }))
1573 }
1574 fn stats(&self) -> &ToolStats {
1575 &self.stats
1576 }
1577}
1578
1579pub struct MemoryQueryTool {
1583 store: Arc<MemoryStore>,
1584 stats: ToolStats,
1585 effects: EffectRow,
1586}
1587
1588impl MemoryQueryTool {
1589 pub fn new(store: Arc<MemoryStore>) -> Self {
1590 Self {
1591 store,
1592 stats: ToolStats::default(),
1593 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
1594 }
1595 }
1596}
1597
1598#[async_trait]
1599impl Tool for MemoryQueryTool {
1600 fn name(&self) -> &str {
1601 "memory.query"
1602 }
1603 fn gana(&self) -> Gana {
1604 Gana::WinnowingBasket
1605 }
1606 fn effects(&self) -> &EffectRow {
1607 &self.effects
1608 }
1609 fn input_schema(&self) -> Value {
1610 schema(
1611 &json!({
1612 "query": str_prop("Case-insensitive substring filter over content (literal match). For tokenized, ranked full-text retrieval use memory.search"),
1613 "galaxy": str_prop("Galaxy to query (default codex)"),
1614 "tags": str_array_prop("Filter: memories with all of these tags"),
1615 "min_importance": num_prop("Filter: minimum importance (0-1)"),
1616 "max_importance": num_prop("Filter: maximum importance (0-1)"),
1617 "created_after": str_prop("Filter: only memories created at or after this RFC 3339 timestamp (e.g. 2026-08-01T00:00:00Z)"),
1618 "created_before": str_prop("Filter: only memories created at or before this RFC 3339 timestamp"),
1619 "limit": int_prop("Maximum entries (default 50)"),
1620 }),
1621 &[],
1622 )
1623 }
1624 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1625 let galaxy_str = args
1626 .get("galaxy")
1627 .and_then(|v| v.as_str())
1628 .unwrap_or("codex");
1629 let galaxy = parse_galaxy(galaxy_str)?;
1630 let limit = args
1631 .get("limit")
1632 .and_then(serde_json::Value::as_u64)
1633 .unwrap_or(50) as usize;
1634 let mut query = MemoryQuery::new().with_limit(limit);
1635 if let Some(text) = args
1636 .get("query")
1637 .and_then(serde_json::Value::as_str)
1638 .map(str::trim)
1639 .filter(|s| !s.is_empty())
1640 {
1641 query = query.with_content_substring(text);
1642 }
1643 if let Some(tags) = args.get("tags").and_then(|v| v.as_array()) {
1644 let tag_list: Vec<String> = tags
1645 .iter()
1646 .filter_map(|v| v.as_str().map(String::from))
1647 .collect();
1648 if !tag_list.is_empty() {
1649 query = query.with_tags(tag_list);
1650 }
1651 }
1652 let parse_bound = |name: &str| -> wm_core::Result<Option<chrono::DateTime<chrono::Utc>>> {
1655 match args.get(name).and_then(|v| v.as_str()) {
1656 Some(s) if !s.trim().is_empty() => chrono::DateTime::parse_from_rfc3339(s.trim())
1657 .map(|t| Some(t.with_timezone(&chrono::Utc)))
1658 .map_err(|_| {
1659 wm_core::CoreError::InvalidArgs(format!(
1660 "{name} must be an RFC 3339 timestamp (e.g. \"2026-08-01T00:00:00Z\"), got: {s}"
1661 ))
1662 }),
1663 _ => Ok(None),
1664 }
1665 };
1666 let created_after = parse_bound("created_after")?;
1667 let created_before = parse_bound("created_before")?;
1668 if let Some(after) = created_after {
1669 query = query.with_created_after(after);
1670 }
1671 if let Some(before) = created_before {
1672 query = query.with_created_before(before);
1673 }
1674
1675 let min_imp = args
1676 .get("min_importance")
1677 .and_then(serde_json::Value::as_f64);
1678 let max_imp = args
1679 .get("max_importance")
1680 .and_then(serde_json::Value::as_f64);
1681 if let (Some(min), Some(max)) = (min_imp, max_imp) {
1682 query = query.with_importance_range(min as f32, max as f32);
1683 } else if let Some(min) = min_imp {
1684 query = query.with_importance_range(min as f32, 1.0);
1685 }
1686
1687 let memories = self.store.query(galaxy, &query)?;
1688
1689 let entries: Vec<Value> = memories
1690 .iter()
1691 .filter(|m| crate::expansion::common::mcp_visible(m))
1692 .filter(|m| crate::expansion::common::validity_visible(m))
1693 .map(|m| {
1694 json!({
1695 "id": m.metadata.id.to_string(),
1696 "content_preview": m.content.chars().take(80).collect::<String>(),
1697 "tags": m.metadata.tags,
1698 "importance": m.metadata.importance,
1699 "created_at": m.metadata.created_at.to_rfc3339(),
1700 })
1701 })
1702 .collect();
1703
1704 let query_applied = args
1711 .get("query")
1712 .and_then(|v| v.as_str())
1713 .is_some_and(|s| !s.trim().is_empty());
1714 let mut response = json!({
1715 "status": "success",
1716 "galaxy": galaxy.db_name(),
1717 "total": entries.len(),
1718 "memories": entries,
1719 });
1720 if query_applied {
1721 response["note"] = json!(
1722 "'query' applied as a literal substring filter over content — \
1723 for tokenized, ranked full-text retrieval use memory.search."
1724 );
1725 }
1726 if created_after.is_some() || created_before.is_some() {
1727 response["time_range"] = json!({
1728 "created_after": created_after
1729 .map(|t| t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
1730 "created_before": created_before
1731 .map(|t| t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
1732 });
1733 }
1734 Ok(response)
1735 }
1736 fn stats(&self) -> &ToolStats {
1737 &self.stats
1738 }
1739}
1740
1741#[allow(dead_code)]
1746pub struct MemorySearchTool {
1747 search: Arc<SearchEngine>,
1748 store: Arc<MemoryStore>,
1749 stats: ToolStats,
1750 effects: EffectRow,
1751}
1752
1753impl MemorySearchTool {
1754 #[must_use]
1755 pub fn new(search: Arc<SearchEngine>, store: Arc<MemoryStore>) -> Self {
1756 Self {
1757 search,
1758 store,
1759 stats: ToolStats::default(),
1760 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
1761 }
1762 }
1763}
1764
1765#[async_trait]
1766impl Tool for MemorySearchTool {
1767 fn name(&self) -> &str {
1768 "memory.search"
1769 }
1770 fn gana(&self) -> Gana {
1771 Gana::WinnowingBasket
1772 }
1773 fn effects(&self) -> &EffectRow {
1774 &self.effects
1775 }
1776 fn input_schema(&self) -> Value {
1777 schema(
1778 &json!({
1779 "query": str_prop("Full-text query"),
1780 "galaxy": str_prop("Galaxy filter (default: all galaxies)"),
1781 "limit": int_prop("Maximum results (default 20)"),
1782 "min_score": num_prop("Absolute BM25 score floor"),
1783 "min_score_ratio": num_prop("Relative floor: reject hits below this fraction of the top score"),
1784 }),
1785 &["query"],
1786 )
1787 }
1788 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1789 let query = args
1790 .get("query")
1791 .and_then(|v| v.as_str())
1792 .ok_or_else(|| wm_core::CoreError::InvalidArgs("query (string) required".into()))?;
1793 let limit = args
1794 .get("limit")
1795 .and_then(serde_json::Value::as_u64)
1796 .unwrap_or(20) as usize;
1797 let min_score = args
1798 .get("min_score")
1799 .and_then(serde_json::Value::as_f64)
1800 .map(|v| v as f32)
1801 .filter(|v| *v > 0.0);
1802 let min_score_ratio = args
1803 .get("min_score_ratio")
1804 .and_then(serde_json::Value::as_f64)
1805 .map(|v| v as f32)
1806 .filter(|v| *v > 0.0 && *v < 1.0);
1807 let galaxy_str = args.get("galaxy").and_then(|v| v.as_str());
1808
1809 let mut opts = wm_memory::SearchOptions {
1810 limit,
1811 min_score,
1812 relative_floor: min_score_ratio,
1813 ..wm_memory::SearchOptions::default()
1814 };
1815 if let Some(g) = galaxy_str {
1816 opts.galaxy = Some(parse_galaxy(g)?);
1817 }
1818 let results = self.search.search_opt(query, &opts)?;
1819
1820 let entries: Vec<Value> = results
1825 .iter()
1826 .filter_map(|r| {
1827 let galaxy = wm_core::Galaxy::from_db_name(&r.galaxy)?;
1828 let id = uuid::Uuid::parse_str(&r.memory_id).ok()?;
1829 let mem = self.store.get(galaxy, id).ok().flatten()?;
1830 if !crate::expansion::common::mcp_visible(&mem) {
1831 return None;
1832 }
1833 if !crate::expansion::common::validity_visible(&mem) {
1834 return None;
1835 }
1836 Some(json!({
1837 "memory_id": r.memory_id,
1838 "galaxy": r.galaxy,
1839 "score": r.score,
1840 "normalized_score": r.normalized_score,
1841 "content_preview": wm_memory::scrub_text(&mem.content).chars().take(120).collect::<String>(),
1842 }))
1843 })
1844 .collect();
1845
1846 Ok(json!({
1847 "status": "success",
1848 "query": query,
1849 "total": entries.len(),
1850 "results": entries,
1851 }))
1852 }
1853 fn stats(&self) -> &ToolStats {
1854 &self.stats
1855 }
1856}
1857
1858pub struct MemoryChatTool {
1864 search: std::sync::Mutex<ConversationalSearch>,
1865 stats: ToolStats,
1866 effects: EffectRow,
1867}
1868
1869impl MemoryChatTool {
1870 #[must_use]
1871 pub fn new(search: ConversationalSearch) -> Self {
1872 Self {
1873 search: std::sync::Mutex::new(search),
1874 stats: ToolStats::default(),
1875 effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
1876 }
1877 }
1878}
1879
1880#[async_trait]
1881impl Tool for MemoryChatTool {
1882 fn name(&self) -> &str {
1883 "memory.chat"
1884 }
1885 fn gana(&self) -> Gana {
1886 Gana::WinnowingBasket
1887 }
1888 fn effects(&self) -> &EffectRow {
1889 &self.effects
1890 }
1891 fn input_schema(&self) -> Value {
1892 schema(
1893 &json!({
1894 "query": str_prop("Conversational query"),
1895 "galaxy": str_prop("Optional galaxy filter"),
1896 "limit": int_prop("Maximum results"),
1897 }),
1898 &["query"],
1899 )
1900 }
1901 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
1902 let query = args
1903 .get("query")
1904 .and_then(|v| v.as_str())
1905 .ok_or_else(|| wm_core::CoreError::InvalidArgs("query (string) required".into()))?;
1906 let limit = args
1907 .get("limit")
1908 .and_then(serde_json::Value::as_u64)
1909 .map(|n| n as usize);
1910 let galaxy_str = args.get("galaxy").and_then(|v| v.as_str());
1911
1912 let galaxy = match galaxy_str {
1913 Some(g) => Some(parse_galaxy(g)?),
1914 None => None,
1915 };
1916
1917 let (results, metrics) = {
1918 let search = self
1919 .search
1920 .lock()
1921 .map_err(|e| wm_core::CoreError::Tool(format!("search lock: {e}")))?;
1922 let results = search.search_in_galaxy(query, limit, galaxy);
1923 let metrics = search.metrics();
1924 (results, metrics)
1925 };
1926
1927 let entries: Vec<Value> = results
1928 .iter()
1929 .map(|r| {
1930 json!({
1931 "memory_id": r.memory_id,
1932 "galaxy": format!("{:?}", r.galaxy),
1933 "score": r.score,
1934 "snippet": r.snippet,
1935 "from_cache": r.from_cache,
1936 "latency_us": r.latency_us,
1937 })
1938 })
1939 .collect();
1940
1941 Ok(json!({
1942 "status": "success",
1943 "query": query,
1944 "total": entries.len(),
1945 "results": entries,
1946 "metrics": {
1947 "total_queries": metrics.total_queries,
1948 "cache_hits": metrics.cache_hits,
1949 "cache_misses": metrics.cache_misses,
1950 "cache_hit_rate": metrics.cache_hit_rate(),
1951 "avg_latency_ms": metrics.avg_latency_ms(),
1952 "meets_latency_target": metrics.meets_latency_target(),
1953 },
1954 }))
1955 }
1956 fn stats(&self) -> &ToolStats {
1957 &self.stats
1958 }
1959}
1960
1961pub struct MemoryVectorSearchTool {
1969 store: Arc<MemoryStore>,
1970 vector_store: Arc<std::sync::Mutex<VectorStore>>,
1971 stats: ToolStats,
1972 effects: EffectRow,
1973}
1974
1975impl MemoryVectorSearchTool {
1976 #[must_use]
1980 pub fn new(store: Arc<MemoryStore>, vector_store: Arc<std::sync::Mutex<VectorStore>>) -> Self {
1981 Self {
1982 store,
1983 vector_store,
1984 stats: ToolStats::default(),
1985 effects: EffectRow::read_only(vec![Resource::VectorStore]),
1986 }
1987 }
1988
1989 fn ensure_loaded(&self) -> wm_core::Result<()> {
1991 let mut vs = self
1992 .vector_store
1993 .lock()
1994 .map_err(|e| wm_core::CoreError::Tool(format!("vector store lock: {e}")))?;
1995 if !vs.is_loaded() {
1996 vs.load(&self.store)?;
1997 }
1998 drop(vs);
1999 Ok(())
2000 }
2001}
2002
2003#[async_trait]
2004impl Tool for MemoryVectorSearchTool {
2005 fn input_schema(&self) -> Value {
2006 schema(
2007 &json!({
2008 "memory_id": str_prop("Memory UUID whose stored embedding is the query"),
2009 "embedding": json!({"type": "array", "items": {"type": "number"}, "description": "Raw embedding vector (alternative to memory_id)"}),
2010 "galaxy": str_prop("Galaxy filter (optional)"),
2011 "limit": int_prop("Maximum results (default 10)"),
2012 }),
2013 &["memory_id"],
2014 )
2015 }
2016 fn name(&self) -> &str {
2017 "memory.vector.search"
2018 }
2019 fn gana(&self) -> Gana {
2020 Gana::WinnowingBasket
2021 }
2022 fn effects(&self) -> &EffectRow {
2023 &self.effects
2024 }
2025 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2026 self.ensure_loaded()?;
2027
2028 let limit = args
2029 .get("limit")
2030 .and_then(serde_json::Value::as_u64)
2031 .unwrap_or(10) as usize;
2032 let galaxy_str = args.get("galaxy").and_then(|v| v.as_str());
2033 let galaxy_filter = match galaxy_str {
2034 Some(g) => Some(parse_galaxy(g)?),
2035 None => None,
2036 };
2037
2038 let results = if let Some(id_str) = args.get("memory_id").and_then(|v| v.as_str()) {
2040 let memory_id = uuid::Uuid::parse_str(id_str).map_err(|e| {
2042 wm_core::CoreError::InvalidArgs(format!("Invalid memory_id UUID: {e}"))
2043 })?;
2044
2045 let vs = self
2046 .vector_store
2047 .lock()
2048 .map_err(|e| wm_core::CoreError::Tool(format!("vector store lock: {e}")))?;
2049 vs.search_similar_to(memory_id, limit)
2050 } else if let Some(embedding_arr) = args.get("embedding").and_then(|v| v.as_array()) {
2051 let embedding: Vec<f32> = embedding_arr
2053 .iter()
2054 .filter_map(|v| v.as_f64().map(|f| f as f32))
2055 .collect();
2056
2057 if embedding.is_empty() {
2058 return Err(wm_core::CoreError::InvalidArgs(
2059 "embedding (array of numbers) or memory_id (string) required".into(),
2060 ));
2061 }
2062
2063 let vs = self
2064 .vector_store
2065 .lock()
2066 .map_err(|e| wm_core::CoreError::Tool(format!("vector store lock: {e}")))?;
2067 vs.search(&embedding, limit, galaxy_filter)
2068 } else {
2069 return Err(wm_core::CoreError::InvalidArgs(
2070 "Either 'embedding' (array of floats) or 'memory_id' (UUID string) is required"
2071 .into(),
2072 ));
2073 };
2074
2075 let entries: Vec<Value> = results
2076 .iter()
2077 .filter_map(|r| {
2078 let stored = self.store.get(r.galaxy, r.memory_id).ok().flatten();
2083 if let Some(mem) = &stored {
2084 if !crate::expansion::common::mcp_visible(mem) {
2085 return None;
2086 }
2087 if !crate::expansion::common::validity_visible(mem) {
2088 return None;
2089 }
2090 }
2091 let preview = stored
2092 .map(|m| m.content.chars().take(120).collect::<String>())
2093 .unwrap_or_default();
2094 Some(json!({
2095 "memory_id": r.memory_id.to_string(),
2096 "galaxy": r.galaxy.db_name(),
2097 "score": r.score,
2098 "content_preview": preview,
2099 }))
2100 })
2101 .collect();
2102
2103 Ok(json!({
2104 "status": "success",
2105 "total": entries.len(),
2106 "results": entries,
2107 }))
2108 }
2109 fn stats(&self) -> &ToolStats {
2110 &self.stats
2111 }
2112}
2113
2114pub struct MemoryAssociateTool {
2118 store: Arc<MemoryStore>,
2119 stats: ToolStats,
2120 effects: EffectRow,
2121}
2122
2123impl MemoryAssociateTool {
2124 pub fn new(store: Arc<MemoryStore>) -> Self {
2125 Self {
2126 store,
2127 stats: ToolStats::default(),
2128 effects: EffectRow {
2129 writes: vec![Resource::Galaxy("associations".into())],
2130 invokes: vec![Capability::MemoryWrite],
2131 ..Default::default()
2132 },
2133 }
2134 }
2135}
2136
2137#[async_trait]
2138impl Tool for MemoryAssociateTool {
2139 fn name(&self) -> &str {
2140 "memory.associate"
2141 }
2142 fn gana(&self) -> Gana {
2143 Gana::Net
2144 }
2145 fn effects(&self) -> &EffectRow {
2146 &self.effects
2147 }
2148 fn input_schema(&self) -> Value {
2149 schema(
2150 &json!({
2151 "source": str_prop("Source memory UUID"),
2152 "target": str_prop("Target memory UUID"),
2153 "type": str_prop("Link type (default: related)"),
2154 "weight": num_prop("Association weight (default 1.0)"),
2155 }),
2156 &["source", "target"],
2157 )
2158 }
2159 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2160 let source_str = args.get("source").and_then(|v| v.as_str()).ok_or_else(|| {
2161 wm_core::CoreError::InvalidArgs("source (UUID string) required".into())
2162 })?;
2163 let target_str = args.get("target").and_then(|v| v.as_str()).ok_or_else(|| {
2164 wm_core::CoreError::InvalidArgs("target (UUID string) required".into())
2165 })?;
2166 let source = uuid::Uuid::parse_str(source_str)
2167 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid source UUID: {e}")))?;
2168 let target = uuid::Uuid::parse_str(target_str)
2169 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid target UUID: {e}")))?;
2170 let weight = args
2171 .get("weight")
2172 .and_then(serde_json::Value::as_f64)
2173 .unwrap_or(1.0) as f32;
2174 let assoc_type = args
2175 .get("type")
2176 .and_then(|v| v.as_str())
2177 .unwrap_or("related");
2178 let link_type = wm_memory::LinkType::from_str_lossy(assoc_type);
2179
2180 let assoc = Association::new(source, target, link_type, weight);
2181 let assoc_store = AssociationStore::open(self.store.env())?;
2182 assoc_store.put(self.store.env(), &assoc)?;
2183
2184 Ok(json!({
2185 "status": "success",
2186 "source": source_str,
2187 "target": target_str,
2188 "weight": weight,
2189 }))
2190 }
2191 fn stats(&self) -> &ToolStats {
2192 &self.stats
2193 }
2194}
2195
2196pub struct MemoryAssociationsTool {
2200 store: Arc<MemoryStore>,
2201 stats: ToolStats,
2202 effects: EffectRow,
2203}
2204
2205impl MemoryAssociationsTool {
2206 pub fn new(store: Arc<MemoryStore>) -> Self {
2207 Self {
2208 store,
2209 stats: ToolStats::default(),
2210 effects: EffectRow::read_only(vec![Resource::Galaxy("associations".into())]),
2211 }
2212 }
2213}
2214
2215#[async_trait]
2216impl Tool for MemoryAssociationsTool {
2217 fn name(&self) -> &str {
2218 "memory.associations"
2219 }
2220 fn gana(&self) -> Gana {
2221 Gana::Net
2222 }
2223 fn effects(&self) -> &EffectRow {
2224 &self.effects
2225 }
2226 fn input_schema(&self) -> Value {
2227 schema(
2228 &json!({
2229 "id": str_prop("Memory UUID to inspect"),
2230 "direction": str_prop("Direction: from | to | both (default: both)"),
2231 }),
2232 &["id"],
2233 )
2234 }
2235 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2236 let id_str = args
2237 .get("id")
2238 .and_then(|v| v.as_str())
2239 .ok_or_else(|| wm_core::CoreError::InvalidArgs("id (UUID string) required".into()))?;
2240 let id = uuid::Uuid::parse_str(id_str)
2241 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid UUID: {e}")))?;
2242 let direction = args
2243 .get("direction")
2244 .and_then(|v| v.as_str())
2245 .unwrap_or("both");
2246
2247 let assoc_store = AssociationStore::open(self.store.env())?;
2248
2249 let mut entries = Vec::new();
2250
2251 if direction == "from" || direction == "both" {
2252 for a in assoc_store.find_from(self.store.env(), id)? {
2253 entries.push(json!({
2254 "source": a.source.to_string(),
2255 "target": a.target.to_string(),
2256 "weight": a.weight,
2257 "link_type": a.link_type.as_str(),
2258 "co_activation_count": a.co_activation_count,
2259 "direction": "outgoing",
2260 }));
2261 }
2262 }
2263 if direction == "to" || direction == "both" {
2264 for a in assoc_store.find_to(self.store.env(), id)? {
2265 entries.push(json!({
2266 "source": a.source.to_string(),
2267 "target": a.target.to_string(),
2268 "weight": a.weight,
2269 "link_type": a.link_type.as_str(),
2270 "co_activation_count": a.co_activation_count,
2271 "direction": "incoming",
2272 }));
2273 }
2274 }
2275
2276 let total = assoc_store.count(self.store.env())?;
2277
2278 Ok(json!({
2279 "status": "success",
2280 "id": id_str,
2281 "direction": direction,
2282 "associations": entries,
2283 "returned": entries.len(),
2284 "total_in_store": total,
2285 }))
2286 }
2287 fn stats(&self) -> &ToolStats {
2288 &self.stats
2289 }
2290}
2291
2292pub struct KarmaReportTool {
2296 ledger: Arc<KarmaLedger>,
2297 stats: ToolStats,
2298 effects: EffectRow,
2299}
2300
2301impl KarmaReportTool {
2302 pub fn new(ledger: Arc<KarmaLedger>) -> Self {
2303 Self {
2304 ledger,
2305 stats: ToolStats::default(),
2306 effects: EffectRow::read_only(vec![Resource::Galaxy("karma".into())]),
2307 }
2308 }
2309}
2310
2311#[async_trait]
2312impl Tool for KarmaReportTool {
2313 fn name(&self) -> &str {
2314 "karma.report"
2315 }
2316 fn gana(&self) -> Gana {
2317 Gana::Willow
2318 }
2319 fn effects(&self) -> &EffectRow {
2320 &self.effects
2321 }
2322 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2323 let recent_count = args
2324 .get("limit")
2325 .and_then(serde_json::Value::as_u64)
2326 .unwrap_or(10) as usize;
2327
2328 let recent = self.ledger.recent(recent_count)?;
2329 let tool_debt = self.ledger.tool_debt()?;
2330
2331 let recent_entries: Vec<Value> = recent
2332 .iter()
2333 .map(|e| {
2334 json!({
2335 "id": e.id,
2336 "tool": e.tool,
2337 "success": e.success,
2338 "mismatch": e.mismatch,
2339 "debt_delta": e.debt_delta,
2340 "guna": format!("{:?}", e.guna),
2341 "total_debt": e.total_debt,
2342 })
2343 })
2344 .collect();
2345
2346 let tool_debt_entries: Vec<Value> = tool_debt
2347 .iter()
2348 .map(|(tool, debt)| {
2349 json!({
2350 "tool": tool,
2351 "debt": debt,
2352 })
2353 })
2354 .collect();
2355
2356 Ok(json!({
2357 "status": "success",
2358 "total_debt": self.ledger.total_debt(),
2359 "chain_head": self.ledger.chain_head(),
2360 "entry_count": self.ledger.next_id(),
2361 "recent_entries": recent_entries,
2362 "per_tool_debt": tool_debt_entries,
2363 }))
2364 }
2365 fn stats(&self) -> &ToolStats {
2366 &self.stats
2367 }
2368}
2369
2370pub struct DharmaStatusTool {
2374 gate: Arc<DharmaGate>,
2375 stats: ToolStats,
2376 effects: EffectRow,
2377}
2378
2379impl DharmaStatusTool {
2380 pub fn new(gate: Arc<DharmaGate>) -> Self {
2381 Self {
2382 gate,
2383 stats: ToolStats::default(),
2384 effects: EffectRow::pure(),
2385 }
2386 }
2387}
2388
2389#[async_trait]
2390impl Tool for DharmaStatusTool {
2391 fn name(&self) -> &str {
2392 "dharma.status"
2393 }
2394 fn gana(&self) -> Gana {
2395 Gana::ExtendedNet
2396 }
2397 fn effects(&self) -> &EffectRow {
2398 &self.effects
2399 }
2400 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
2401 let homeostasis = self.gate.homeostasis();
2402 let health = homeostasis.health_score();
2403 let decisions = wm_governance::dharma_gate::verdict_counts();
2404
2405 Ok(json!({
2406 "status": "success",
2407 "homeostasis": {
2408 "cpu_load": homeostasis.cpu_load,
2409 "memory_pressure": homeostasis.memory_pressure,
2410 "active": homeostasis.active,
2411 "health_score": health,
2412 "stressed": homeostasis.is_stressed(),
2413 },
2414 "decisions": {
2415 "observe": decisions.observe,
2416 "advise": decisions.advise,
2417 "correct": decisions.correct,
2418 "intervene": decisions.intervene,
2419 "panic": decisions.panic,
2420 "total": decisions.total(),
2421 "blocked": decisions.blocked(),
2422 "blocked_ratio": decisions.blocked_ratio(),
2423 },
2424 "sutras": {
2425 "ahimsa": "Non-harm — destructive actions blocked in strict mode",
2426 "satya": "Truth — memory fabrication always forbidden",
2427 },
2428 }))
2429 }
2430 fn stats(&self) -> &ToolStats {
2431 &self.stats
2432 }
2433}
2434
2435pub struct HarmonyVectorTool {
2439 monitor: Arc<SubstrateMonitor>,
2440 stats: ToolStats,
2441 effects: EffectRow,
2442}
2443
2444impl HarmonyVectorTool {
2445 pub fn new(monitor: Arc<SubstrateMonitor>) -> Self {
2446 Self {
2447 monitor,
2448 stats: ToolStats::default(),
2449 effects: EffectRow::pure(),
2450 }
2451 }
2452}
2453
2454#[async_trait]
2455impl Tool for HarmonyVectorTool {
2456 fn name(&self) -> &str {
2457 "harmony.vector"
2458 }
2459 fn gana(&self) -> Gana {
2460 Gana::Dipper
2461 }
2462 fn effects(&self) -> &EffectRow {
2463 &self.effects
2464 }
2465 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
2466 let hv = self.monitor.sample();
2467 Ok(json!({
2468 "status": "success",
2469 "harmony_vector": hv.to_json(),
2470 }))
2471 }
2472 fn stats(&self) -> &ToolStats {
2473 &self.stats
2474 }
2475}
2476
2477pub struct HarmonyHistoryTool {
2481 monitor: Arc<SubstrateMonitor>,
2482 stats: ToolStats,
2483 effects: EffectRow,
2484}
2485
2486impl HarmonyHistoryTool {
2487 pub fn new(monitor: Arc<SubstrateMonitor>) -> Self {
2488 Self {
2489 monitor,
2490 stats: ToolStats::default(),
2491 effects: EffectRow::pure(),
2492 }
2493 }
2494}
2495
2496#[async_trait]
2497impl Tool for HarmonyHistoryTool {
2498 fn name(&self) -> &str {
2499 "harmony.history"
2500 }
2501 fn gana(&self) -> Gana {
2502 Gana::Dipper
2503 }
2504 fn effects(&self) -> &EffectRow {
2505 &self.effects
2506 }
2507 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2508 let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(20) as usize;
2509 let samples: Vec<Value> = self
2510 .monitor
2511 .history(limit)
2512 .iter()
2513 .map(wm_substrate::HarmonyVector::to_json)
2514 .collect();
2515 Ok(json!({
2516 "status": "success",
2517 "count": samples.len(),
2518 "samples": samples,
2519 }))
2520 }
2521 fn stats(&self) -> &ToolStats {
2522 &self.stats
2523 }
2524}
2525
2526pub struct GnosisStatusTool {
2534 dharma_gate: Arc<DharmaGate>,
2535 resource_rules: Arc<ResourceRules>,
2536 substrate: Arc<SubstrateMonitor>,
2537 stats: ToolStats,
2538 effects: EffectRow,
2539}
2540
2541impl GnosisStatusTool {
2542 pub fn new(
2543 dharma_gate: Arc<DharmaGate>,
2544 resource_rules: Arc<ResourceRules>,
2545 substrate: Arc<SubstrateMonitor>,
2546 ) -> Self {
2547 Self {
2548 dharma_gate,
2549 resource_rules,
2550 substrate,
2551 stats: ToolStats::default(),
2552 effects: EffectRow::pure(),
2553 }
2554 }
2555}
2556
2557#[async_trait]
2558impl Tool for GnosisStatusTool {
2559 fn input_schema(&self) -> Value {
2560 schema(&json!({}), &[])
2561 }
2562 fn name(&self) -> &str {
2563 "gnosis.status"
2564 }
2565 fn gana(&self) -> Gana {
2566 Gana::ThreeStars
2567 }
2568 fn effects(&self) -> &EffectRow {
2569 &self.effects
2570 }
2571 async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
2572 let homeostasis = self.dharma_gate.homeostasis();
2573 let health = homeostasis.health_score();
2574 let budget_usage = self.resource_rules.budget_usage();
2575 let human_approved = self.resource_rules.human_approved();
2576 let last_hv = self.substrate.last_sample();
2577
2578 Ok(json!({
2579 "status": "success",
2580 "brain_wave": format!("{:?}", ctx.brain_wave),
2581 "homeostasis": {
2582 "cpu_load": homeostasis.cpu_load,
2583 "memory_pressure": homeostasis.memory_pressure,
2584 "active": homeostasis.active,
2585 "health_score": health,
2586 "stressed": homeostasis.is_stressed(),
2587 },
2588 "resource_rules": {
2589 "writes_last_minute": budget_usage.writes_last_minute,
2590 "spawns_last_minute": budget_usage.spawns_last_minute,
2591 "network_last_minute": budget_usage.network_last_minute,
2592 "novelty_entries": budget_usage.novelty_entries,
2593 "human_approved": human_approved,
2594 "require_human_review": true,
2595 },
2596 "substrate": last_hv.as_ref().map(wm_substrate::HarmonyVector::to_json),
2597 "governance_layers": {
2598 "lakshmi": "Harmony Vector — hardware awareness (active)",
2599 "tiferet": "Resource Gating — brain-wave transitions gated by health (active)",
2600 "yama": "Dharma Resource Rules — budgets, novelty, purpose, human review (active)",
2601 "gnosis": "Transparency Portals — this tool (active)",
2602 },
2603 }))
2604 }
2605 fn stats(&self) -> &ToolStats {
2606 &self.stats
2607 }
2608}
2609
2610pub struct GnosisHistoryTool {
2614 substrate: Arc<SubstrateMonitor>,
2615 stats: ToolStats,
2616 effects: EffectRow,
2617}
2618
2619impl GnosisHistoryTool {
2620 pub fn new(substrate: Arc<SubstrateMonitor>) -> Self {
2621 Self {
2622 substrate,
2623 stats: ToolStats::default(),
2624 effects: EffectRow::pure(),
2625 }
2626 }
2627}
2628
2629#[async_trait]
2630impl Tool for GnosisHistoryTool {
2631 fn input_schema(&self) -> Value {
2632 schema(
2633 &json!({
2634 "limit": int_prop("Maximum history entries (default 20)"),
2635 }),
2636 &[],
2637 )
2638 }
2639 fn name(&self) -> &str {
2640 "gnosis.history"
2641 }
2642 fn gana(&self) -> Gana {
2643 Gana::ThreeStars
2644 }
2645 fn effects(&self) -> &EffectRow {
2646 &self.effects
2647 }
2648 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2649 let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(20) as usize;
2650 let history = self.substrate.history(limit);
2651 let samples: Vec<Value> = history
2652 .iter()
2653 .map(wm_substrate::HarmonyVector::to_json)
2654 .collect();
2655
2656 let avg_cpu = if samples.is_empty() {
2658 0.0
2659 } else {
2660 samples
2661 .iter()
2662 .filter_map(|s| s["cpu_load"].as_f64())
2663 .sum::<f64>()
2664 / samples.len() as f64
2665 };
2666 let avg_mem = if samples.is_empty() {
2667 0.0
2668 } else {
2669 samples
2670 .iter()
2671 .filter_map(|s| s["memory_pressure"].as_f64())
2672 .sum::<f64>()
2673 / samples.len() as f64
2674 };
2675 let avg_health = if samples.is_empty() {
2676 0.0
2677 } else {
2678 samples
2679 .iter()
2680 .filter_map(|s| s["health_score"].as_f64())
2681 .sum::<f64>()
2682 / samples.len() as f64
2683 };
2684
2685 Ok(json!({
2686 "status": "success",
2687 "count": samples.len(),
2688 "summary": {
2689 "avg_cpu_load": avg_cpu,
2690 "avg_memory_pressure": avg_mem,
2691 "avg_health_score": avg_health,
2692 },
2693 "samples": samples,
2694 }))
2695 }
2696 fn stats(&self) -> &ToolStats {
2697 &self.stats
2698 }
2699}
2700
2701pub struct GnosisExplainTool {
2709 dharma_gate: Arc<DharmaGate>,
2710 resource_rules: Arc<ResourceRules>,
2711 stats: ToolStats,
2712 effects: EffectRow,
2713}
2714
2715impl GnosisExplainTool {
2716 pub fn new(dharma_gate: Arc<DharmaGate>, resource_rules: Arc<ResourceRules>) -> Self {
2717 Self {
2718 dharma_gate,
2719 resource_rules,
2720 stats: ToolStats::default(),
2721 effects: EffectRow::pure(),
2722 }
2723 }
2724}
2725
2726#[async_trait]
2727impl Tool for GnosisExplainTool {
2728 fn input_schema(&self) -> Value {
2729 schema(
2730 &json!({
2731 "tool_name": str_prop("Tool name to explain"),
2732 "is_write": bool_prop("Claim: the invocation writes"),
2733 "is_spawn": bool_prop("Claim: the invocation spawns a process"),
2734 "is_network": bool_prop("Claim: the invocation uses the network"),
2735 "has_purpose": bool_prop("Claim: the invocation carries a purpose"),
2736 "args_hash": str_prop("Hash of the arguments under evaluation"),
2737 }),
2738 &[],
2739 )
2740 }
2741 fn name(&self) -> &str {
2742 "gnosis.explain"
2743 }
2744 fn gana(&self) -> Gana {
2745 Gana::ThreeStars
2746 }
2747 fn effects(&self) -> &EffectRow {
2748 &self.effects
2749 }
2750 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
2751 let tool_name = args
2752 .get("tool_name")
2753 .and_then(Value::as_str)
2754 .unwrap_or("unknown");
2755 let is_write = args
2756 .get("is_write")
2757 .and_then(Value::as_bool)
2758 .unwrap_or(false);
2759 let is_spawn = args
2760 .get("is_spawn")
2761 .and_then(Value::as_bool)
2762 .unwrap_or(false);
2763 let is_network = args
2764 .get("is_network")
2765 .and_then(Value::as_bool)
2766 .unwrap_or(false);
2767 let has_purpose = args
2768 .get("has_purpose")
2769 .and_then(Value::as_bool)
2770 .unwrap_or(true);
2771 let args_hash = args.get("args_hash").and_then(Value::as_u64).unwrap_or(0);
2772
2773 let homeostasis = self.dharma_gate.homeostasis();
2774
2775 let dummy_effects = if is_write {
2777 EffectRow {
2778 writes: vec![Resource::Filesystem],
2779 ..Default::default()
2780 }
2781 } else {
2782 EffectRow::pure()
2783 };
2784 let dharma_verdict = self.dharma_gate.evaluate(&dummy_effects, ctx);
2785
2786 let resource_verdict = self.resource_rules.evaluate(
2788 tool_name,
2789 args_hash,
2790 is_write,
2791 is_spawn,
2792 is_network,
2793 has_purpose,
2794 &homeostasis,
2795 ctx.brain_wave,
2796 );
2797
2798 Ok(json!({
2799 "status": "success",
2800 "tool_name": tool_name,
2801 "brain_wave": format!("{:?}", ctx.brain_wave),
2802 "homeostasis": {
2803 "cpu_load": homeostasis.cpu_load,
2804 "memory_pressure": homeostasis.memory_pressure,
2805 "health_score": homeostasis.health_score(),
2806 "stressed": homeostasis.is_stressed(),
2807 },
2808 "dharma_verdict": {
2809 "verdict": format!("{:?}", dharma_verdict),
2810 "blocks": dharma_verdict.blocks(),
2811 "reason": dharma_verdict.reason(),
2812 },
2813 "resource_verdict": {
2814 "verdict": format!("{:?}", resource_verdict),
2815 "blocks": resource_verdict.blocks(),
2816 "reason": resource_verdict.reason(),
2817 },
2818 "would_block": dharma_verdict.blocks() || resource_verdict.blocks(),
2819 "explanation": format!(
2820 "Tool '{}' under {:?} brain-wave with health {:.2}: Dharma says '{}', Resources say '{}'. {}",
2821 tool_name,
2822 ctx.brain_wave,
2823 homeostasis.health_score(),
2824 dharma_verdict.reason(),
2825 resource_verdict.reason(),
2826 if dharma_verdict.blocks() || resource_verdict.blocks() {
2827 "Action would be BLOCKED."
2828 } else {
2829 "Action would be ALLOWED."
2830 }
2831 ),
2832 }))
2833 }
2834 fn stats(&self) -> &ToolStats {
2835 &self.stats
2836 }
2837}
2838
2839pub struct WmMetaTool {
2843 registry: Arc<ToolRegistry>,
2844 stats: ToolStats,
2845 effects: EffectRow,
2846 embedding_router: Option<Arc<embedding_router::EmbeddingRouter>>,
2849 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
2851 pipeline: Option<Arc<DispatchPipeline>>,
2856}
2857
2858impl WmMetaTool {
2859 #[must_use]
2860 pub fn new(registry: Arc<ToolRegistry>) -> Self {
2861 Self {
2862 registry,
2863 stats: ToolStats::default(),
2864 effects: EffectRow::pure(),
2865 embedding_router: None,
2866 shadow_stats: Arc::new(std::sync::RwLock::new(
2867 embedding_router::ShadowModeStats::default(),
2868 )),
2869 pipeline: None,
2870 }
2871 }
2872
2873 #[must_use]
2878 pub fn with_embedder(
2879 registry: Arc<ToolRegistry>,
2880 embedder: Box<dyn wm_memory::Embedder>,
2881 ) -> Self {
2882 let embedding_router = Self::build_embedding_router(®istry, embedder).map(Arc::new);
2883 Self {
2884 registry,
2885 stats: ToolStats::default(),
2886 effects: EffectRow::pure(),
2887 embedding_router,
2888 shadow_stats: Arc::new(std::sync::RwLock::new(
2889 embedding_router::ShadowModeStats::default(),
2890 )),
2891 pipeline: None,
2892 }
2893 }
2894
2895 #[must_use]
2900 pub fn with_embedder_and_shadow_stats(
2901 registry: Arc<ToolRegistry>,
2902 embedder: Box<dyn wm_memory::Embedder>,
2903 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
2904 ) -> Self {
2905 let embedding_router = Self::build_embedding_router(®istry, embedder).map(Arc::new);
2906 Self {
2907 registry,
2908 stats: ToolStats::default(),
2909 effects: EffectRow::pure(),
2910 embedding_router,
2911 shadow_stats,
2912 pipeline: None,
2913 }
2914 }
2915
2916 #[must_use]
2919 pub fn with_router_shadow_stats_and_pipeline(
2920 registry: Arc<ToolRegistry>,
2921 embedder: Box<dyn wm_memory::Embedder>,
2922 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
2923 pipeline: Option<Arc<DispatchPipeline>>,
2924 ) -> Self {
2925 let embedding_router = Self::build_embedding_router(®istry, embedder).map(Arc::new);
2926 Self {
2927 registry,
2928 stats: ToolStats::default(),
2929 effects: EffectRow::pure(),
2930 embedding_router,
2931 shadow_stats,
2932 pipeline,
2933 }
2934 }
2935
2936 fn build_embedding_router(
2944 registry: &ToolRegistry,
2945 embedder: Box<dyn wm_memory::Embedder>,
2946 ) -> Option<embedding_router::EmbeddingRouter> {
2947 let tools = registry.all_ref();
2948 if tools.is_empty() {
2949 return embedding_router::EmbeddingRouter::new(embedder);
2950 }
2951 let descriptions = embedding_router::anchored_descriptions(tools);
2952 embedding_router::EmbeddingRouter::with_descriptions(embedder, descriptions)
2953 }
2954
2955 fn classify(text: &str) -> (&'static str, f64) {
2961 nlu::classify(text)
2962 }
2963
2964 fn classify_with_router_inner(
2973 router: &embedding_router::EmbeddingRouter,
2974 shadow_stats: &std::sync::RwLock<embedding_router::ShadowModeStats>,
2975 text: &str,
2976 ) -> (String, f64, Option<Vec<f32>>) {
2977 let (emb_tool, emb_conf, margin, query_emb) =
2978 match router.route_with_margin_and_embedding(text) {
2979 Some(t) => t,
2980 None => ("gnosis".into(), 0.0, 0.0, Vec::new()),
2981 };
2982
2983 let (tfidf_tool, tfidf_conf) = nlu::classify(text);
2985 if emb_tool != tfidf_tool {
2986 tracing::debug!(
2987 query = text.chars().take(100).collect::<String>(),
2988 embedding_tool = %emb_tool,
2989 embedding_conf = emb_conf,
2990 margin = margin,
2991 tfidf_tool = %tfidf_tool,
2992 tfidf_conf = tfidf_conf,
2993 "shadow mode disagreement: embedding vs TF-IDF"
2994 );
2995 }
2996
2997 if let Ok(mut stats) = shadow_stats.write() {
2999 stats.record(text, &emb_tool, emb_conf, tfidf_tool, tfidf_conf);
3000 }
3001
3002 let selected = if margin < embedding_router::MIN_MARGIN {
3007 (tfidf_tool.to_string(), tfidf_conf)
3008 } else {
3009 (emb_tool, emb_conf)
3010 };
3011 let query_emb = (!query_emb.is_empty()).then_some(query_emb);
3012 (selected.0, selected.1, query_emb)
3013 }
3014
3015 async fn classify_async(&self, text: &str) -> (String, f64, Option<Vec<f32>>) {
3023 let Some(router) = self.embedding_router.clone() else {
3024 let (tool, conf) = Self::classify(text);
3025 return (tool.to_string(), conf, None);
3026 };
3027 let shadow_stats = Arc::clone(&self.shadow_stats);
3028 let text_owned = text.to_string();
3029 let fallback_text = text_owned.clone();
3030 match tokio::task::spawn_blocking(move || {
3031 Self::classify_with_router_inner(&router, &shadow_stats, &text_owned)
3032 })
3033 .await
3034 {
3035 Ok(result) => result,
3036 Err(join_err) => {
3037 tracing::warn!(
3038 error = %join_err,
3039 "NLU blocking classifier task failed — falling back to TF-IDF"
3040 );
3041 let (tool, conf) = Self::classify(&fallback_text);
3042 (tool.to_string(), conf, None)
3043 }
3044 }
3045 }
3046
3047 #[must_use]
3049 pub const fn shadow_stats(&self) -> &Arc<std::sync::RwLock<embedding_router::ShadowModeStats>> {
3050 &self.shadow_stats
3051 }
3052
3053 #[must_use]
3055 pub const fn embedding_router(&self) -> Option<&Arc<embedding_router::EmbeddingRouter>> {
3056 self.embedding_router.as_ref()
3057 }
3058
3059 fn required_arg(tool_name: &str) -> Option<&'static str> {
3062 match tool_name {
3063 "memory.create" => Some("content"),
3064 "memory.batch_create" => Some("items"),
3065 "memory.read" => Some("id"),
3066 "memory.delete" => Some("id"),
3067 "memory.search" => Some("query"),
3068 "memory.episodic_search" => Some("query"),
3069 "memory.associate" => Some("source"),
3070 "memory.associations" => Some("id"),
3071 "memory.update" => Some("id"),
3072 "memory.revisions" => Some("id"),
3073 "memory.tag" => Some("id"),
3074 "memory.batch_read" => Some("ids"),
3075 "memory.nearby" => Some("query"),
3076 "session.end" => Some("session_id"),
3077 "agent.register" => Some("name"),
3078 "agent.trust" => Some("agent_id"),
3079 "agent.descriptions" => Some("agent_id"),
3080 "agent.capabilities" => Some("agent_id"),
3081 "agent.heartbeat.history" => Some("agent_id"),
3082 "agent.deregister" => Some("agent_id"),
3083 "galaxy.purge" => Some("galaxy"),
3084 "memory.deduplicate" => Some("galaxy"),
3085 "task.distribute" => Some("task"),
3086 "code.claim" => Some("scope"),
3087 "code.check" => Some("scope"),
3088 "code.release" => Some("scope"),
3089 _ => None,
3090 }
3091 }
3092
3093 fn missing_arg_hint(tool_name: &str, missing: &str) -> String {
3095 match (tool_name, missing) {
3096 ("memory.create", "content") => "Provide the content to store, e.g. wm(thought='remember that rust is fast')".into(),
3097 ("memory.read", "id") => "Provide a memory UUID, e.g. wm(route='memory.read', args={\"id\": \"<uuid>\"}). To search by content instead, use wm(thought='find <text>') or wm(route='memory.search', args={\"query\": \"...\"}). To list memories, use wm(route='memory.list', args={\"galaxy\": \"codex\", \"limit\": 10})".into(),
3098 ("memory.delete", "id") => "Provide a memory UUID, e.g. wm(thought='delete memory <uuid>')".into(),
3099 ("memory.search", "query") => "Provide a search query, e.g. wm(thought='search for rust')".into(),
3100 ("memory.query", "query") => "memory.query accepts `query` as optional when filtering by tags/importance/dates, e.g. wm(route='memory.query', args={\"tags\": [\"project:myapp\"]})".into(),
3101 ("memory.vector.search", "memory_id") => "Provide a memory UUID for similarity search, e.g. wm(route='memory.vector.search', args={\"memory_id\": \"<uuid>\"})".into(),
3102 ("memory.update", "id") => "Provide a memory UUID to update, e.g. wm(route='memory.update', args={\"id\": \"<uuid>\", \"tags\": [\"new\"]})".into(),
3103 ("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(),
3104 ("memory.tag", "id") => "Provide a memory UUID to tag, e.g. wm(route='memory.tag', args={\"id\": \"<uuid>\", \"tags\": [\"rust\"]})".into(),
3105 _ => format!("Missing required argument: '{missing}' for tool '{tool_name}'"),
3106 }
3107 }
3108
3109 fn extract_payload(thought: &str, tool_name: &str) -> Option<(String, String)> {
3111 let lower = thought.to_lowercase();
3112 match tool_name {
3113 "memory.create" => {
3114 for prefix in &[
3115 "remember that ",
3116 "remember ",
3117 "store ",
3118 "save ",
3119 "note that ",
3120 "note ",
3121 ] {
3122 if lower.starts_with(prefix) {
3123 let content = thought[prefix.len()..].to_string();
3124 if !content.is_empty() {
3125 return Some(("content".into(), content));
3126 }
3127 }
3128 }
3129 if !thought.is_empty() {
3130 return Some(("content".into(), thought.to_string()));
3131 }
3132 }
3133 "memory.read" => {
3134 for prefix in &["recall ", "read memory ", "fetch memory ", "get memory "] {
3135 if lower.starts_with(prefix) {
3136 let id = thought[prefix.len()..].trim().to_string();
3137 if !id.is_empty() {
3138 return Some(("id".into(), id));
3139 }
3140 }
3141 }
3142 }
3143 "memory.list" => {
3144 for prefix in &[
3145 "list memories",
3146 "show memories",
3147 "search memories",
3148 "search for",
3149 ] {
3150 if lower.contains(prefix) {
3151 let after = &thought[lower.find(prefix).unwrap() + prefix.len()..];
3152 let query = after.trim().trim_start_matches("in ").trim();
3153 if !query.is_empty() {
3154 return Some(("galaxy".into(), query.to_string()));
3155 }
3156 }
3157 }
3158 }
3159 "memory.delete" => {
3160 for prefix in &["delete memory ", "remove memory ", "forget memory "] {
3161 if lower.starts_with(prefix) {
3162 let id = thought[prefix.len()..].trim().to_string();
3163 if !id.is_empty() {
3164 return Some(("id".into(), id));
3165 }
3166 }
3167 }
3168 }
3169 "memory.search" => {
3170 let mut text: &str = thought;
3175 if let Some((phrase, _, _)) = crate::nlu::PHRASE_ROUTES
3176 .iter()
3177 .find(|(phrase, tool, _)| *tool == "memory.search" && lower.starts_with(phrase))
3178 {
3179 text = &thought[phrase.len()..];
3180 } else if lower.starts_with("search for ") {
3181 text = &thought["search for ".len()..];
3182 } else if lower.starts_with("search ") {
3183 text = &thought["search ".len()..];
3184 } else {
3185 for (verb, tool, _) in crate::nlu::PREFIX_ROUTES {
3186 if *tool != "memory.search" {
3187 continue;
3188 }
3189 if let Some(rest) = lower.strip_prefix(verb) {
3190 if rest.is_empty() || rest.starts_with(' ') || rest.starts_with(':') {
3191 text = thought[verb.len()..].trim_start_matches([' ', ':']);
3192 break;
3193 }
3194 }
3195 }
3196 }
3197 let lower_text = text.to_lowercase();
3200 for filler in ["memory for ", "memories for ", "memory ", "memories "] {
3201 if lower_text.starts_with(filler) {
3202 text = &text[filler.len()..];
3203 break;
3204 }
3205 }
3206 let query = text
3207 .trim()
3208 .trim_end_matches(['?', '!'])
3209 .trim()
3210 .trim_end_matches(" in memory")
3211 .trim();
3212 if !query.is_empty() {
3213 return Some(("query".into(), query.to_string()));
3214 }
3215 }
3216 "memory.chat" => {
3217 for prefix in &[
3218 "chat about ",
3219 "chat ",
3220 "ask about ",
3221 "ask ",
3222 "discuss ",
3223 "explore ",
3224 "converse about ",
3225 ] {
3226 if lower.starts_with(prefix) {
3227 let query = thought[prefix.len()..].trim().to_string();
3228 if !query.is_empty() {
3229 return Some(("query".into(), query));
3230 }
3231 }
3232 }
3233 if !thought.is_empty() {
3234 return Some(("query".into(), thought.to_string()));
3235 }
3236 }
3237 "memory.vector.search" => {
3238 for prefix in &[
3239 "find similar to ",
3240 "similar to memory ",
3241 "vector search ",
3242 "semantic search ",
3243 "embedding search ",
3244 ] {
3245 if lower.starts_with(prefix) {
3246 let id = thought[prefix.len()..].trim().to_string();
3247 if !id.is_empty() {
3248 return Some(("memory_id".into(), id));
3249 }
3250 }
3251 }
3252 }
3253 "memory.count" => {
3254 for prefix in &[
3255 "count memories in ",
3256 "how many memories in ",
3257 "memory count ",
3258 ] {
3259 if lower.starts_with(prefix) {
3260 let galaxy = thought[prefix.len()..].trim().to_string();
3261 if !galaxy.is_empty() {
3262 return Some(("galaxy".into(), galaxy));
3263 }
3264 }
3265 }
3266 }
3267 "session.start" => {
3268 for prefix in &["start session ", "new session ", "begin session "] {
3269 if lower.starts_with(prefix) {
3270 let title = thought[prefix.len()..].trim().to_string();
3271 if !title.is_empty() {
3272 return Some(("title".into(), title));
3276 }
3277 }
3278 }
3279 }
3280 "session.end" => {
3281 for prefix in &["end session ", "close session ", "stop session "] {
3282 if lower.starts_with(prefix) {
3283 let id = thought[prefix.len()..].trim().to_string();
3284 if !id.is_empty() {
3285 return Some(("session_id".into(), id));
3286 }
3287 }
3288 }
3289 }
3290 "agent.register" => {
3291 for prefix in &[
3292 "register agent ",
3293 "new agent ",
3294 "create agent ",
3295 "add agent ",
3296 ] {
3297 if lower.starts_with(prefix) {
3298 let name = thought[prefix.len()..].trim().to_string();
3299 if !name.is_empty() {
3300 return Some(("name".into(), name));
3301 }
3302 }
3303 }
3304 }
3305 "agent.trust"
3306 | "agent.descriptions"
3307 | "agent.capabilities"
3308 | "agent.heartbeat.history"
3309 | "agent.deregister" => {
3310 for prefix in &[
3311 "trust agent ",
3312 "describe agent ",
3313 "capabilities agent ",
3314 "heartbeat history agent ",
3315 "deregister agent ",
3316 "unregister agent ",
3317 "remove agent ",
3318 ] {
3319 if lower.starts_with(prefix) {
3320 let id = thought[prefix.len()..].trim().to_string();
3321 if !id.is_empty() {
3322 return Some(("agent_id".into(), id));
3323 }
3324 }
3325 }
3326 }
3327 "galaxy.purge" => {
3328 for prefix in &["purge galaxy ", "wipe galaxy ", "clear galaxy "] {
3329 if lower.starts_with(prefix) {
3330 let galaxy = thought[prefix.len()..].trim().to_string();
3331 if !galaxy.is_empty() {
3332 return Some(("galaxy".into(), galaxy));
3333 }
3334 }
3335 }
3336 }
3337 "task.distribute" => {
3338 for prefix in &["distribute task ", "assign task ", "dispatch task "] {
3339 if lower.starts_with(prefix) {
3340 let task = thought[prefix.len()..].trim().to_string();
3341 if !task.is_empty() {
3342 return Some(("task".into(), task));
3343 }
3344 }
3345 }
3346 }
3347 "memory.sort" => {
3348 for prefix in &["sort memories ", "sort memory ", "order memories "] {
3349 if lower.starts_with(prefix) {
3350 let galaxy = thought[prefix.len()..].trim().to_string();
3351 if !galaxy.is_empty() {
3352 return Some(("galaxy".into(), galaxy));
3353 }
3354 }
3355 }
3356 }
3357 "memory.filter" => {
3358 for prefix in &["filter memories ", "filter memory "] {
3359 if lower.starts_with(prefix) {
3360 let galaxy = thought[prefix.len()..].trim().to_string();
3361 if !galaxy.is_empty() {
3362 return Some(("galaxy".into(), galaxy));
3363 }
3364 }
3365 }
3366 }
3367 "memory.deduplicate" => {
3368 for prefix in &[
3369 "deduplicate memories ",
3370 "deduplicate memory ",
3371 "dedup memories ",
3372 ] {
3373 if lower.starts_with(prefix) {
3374 let galaxy = thought[prefix.len()..].trim().to_string();
3375 if !galaxy.is_empty() {
3376 return Some(("galaxy".into(), galaxy));
3377 }
3378 }
3379 }
3380 }
3381 "memory.export" => {
3382 for prefix in &["export memories ", "export memory "] {
3383 if lower.starts_with(prefix) {
3384 let galaxy = thought[prefix.len()..].trim().to_string();
3385 if !galaxy.is_empty() {
3386 return Some(("galaxy".into(), galaxy));
3387 }
3388 }
3389 }
3390 }
3391 "speculative.decode" => {
3392 for prefix in &[
3393 "speculative decode ",
3394 "speculative ",
3395 "decode ",
3396 "draft and verify ",
3397 "accelerate inference ",
3398 ] {
3399 if lower.starts_with(prefix) {
3400 let prompt = thought[prefix.len()..].trim().to_string();
3401 if !prompt.is_empty() {
3402 return Some(("prompt".into(), prompt));
3403 }
3404 }
3405 }
3406 }
3407 "meta.enhance" => {
3408 for prefix in &[
3409 "enhance ",
3410 "enhance prompt ",
3411 "grounded inference ",
3412 "self-correct ",
3413 "meta enhance ",
3414 "cognitive enhance ",
3415 "augment ",
3416 ] {
3417 if lower.starts_with(prefix) {
3418 let prompt = thought[prefix.len()..].trim().to_string();
3419 if !prompt.is_empty() {
3420 return Some(("prompt".into(), prompt));
3421 }
3422 }
3423 }
3424 }
3425 "dense.encode" => {
3426 for prefix in &["dense encode ", "compress ", "encode ", "compact "] {
3427 if lower.starts_with(prefix) {
3428 let text = thought[prefix.len()..].trim().to_string();
3429 if !text.is_empty() {
3430 return Some(("text".into(), text));
3431 }
3432 }
3433 }
3434 }
3435 "dream.trigger" => {
3436 for prefix in &[
3437 "dream trigger ",
3438 "trigger dream ",
3439 "start dream ",
3440 "force dream ",
3441 "initiate dream ",
3442 ] {
3443 if lower.starts_with(prefix) {
3444 let rest = thought[prefix.len()..].trim();
3445 if !rest.is_empty() {
3446 return Some(("force".into(), rest.to_string()));
3447 }
3448 }
3449 }
3450 }
3451 _ => {}
3452 }
3453 None
3454 }
3455}
3456
3457#[async_trait]
3458impl Tool for WmMetaTool {
3459 fn input_schema(&self) -> Value {
3460 schema(
3461 &json!({
3462 "route": str_prop("Explicit canonical route, e.g. \"memory.search\" (preferred for agents)"),
3463 "thought": str_prop("Natural-language convenience routing (least reliable; prefer route)"),
3464 "args": json!({"type": "object", "description": "Arguments passed through to the target tool"}),
3465 }),
3466 &[],
3467 )
3468 }
3469 fn name(&self) -> &str {
3470 "wm"
3471 }
3472 fn gana(&self) -> Gana {
3473 Gana::Horn
3474 }
3475 fn effects(&self) -> &EffectRow {
3476 &self.effects
3477 }
3478 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
3479 let thought = args.get("thought").and_then(|v| v.as_str()).unwrap_or("");
3480 let (route, passthrough_args) = if glyph_mode_from_env() {
3486 if let Some((r, a)) = decode_lkep(&args) {
3487 (Some(r), a)
3488 } else if let Some(Value::Object(map)) = decode_glyph(&args) {
3489 (
3490 map.get("route").and_then(Value::as_str).map(String::from),
3491 map.get("args").cloned().unwrap_or(Value::Null),
3492 )
3493 } else {
3494 let r = args
3495 .get("route")
3496 .and_then(Value::as_str)
3497 .map(|s| resolve_route(s).unwrap_or(s).to_string());
3498 let a = args.get("args").cloned().unwrap_or(Value::Null);
3499 (r, a)
3500 }
3501 } else {
3502 (
3503 args.get("route").and_then(Value::as_str).map(|s| {
3504 expansion::common::canonical_tool_alias(s)
3505 .unwrap_or(s)
3506 .to_string()
3507 }),
3508 args.get("args").cloned().unwrap_or(Value::Null),
3509 )
3510 };
3511 let route = route.as_deref();
3512
3513 if thought.is_empty() && route.is_none() {
3514 let received: Vec<String> = args
3520 .as_object()
3521 .map(|o| o.keys().cloned().collect())
3522 .unwrap_or_default();
3523 let detail = if received.is_empty() {
3524 String::new()
3525 } else {
3526 format!("; received argument keys: {received:?}")
3527 };
3528 return Ok(json!({
3529 "status": "error",
3530 "message": format!(
3531 "Either 'thought' (natural language) or 'route' (explicit) is required{detail}"
3532 ),
3533 "hint": "wm(thought='remember that X is Y') or wm(route='memory.create', args={\"content\": \"...\"})"
3534 }));
3535 }
3536
3537 let (tool_name, confidence, query_emb) = if let Some(r) = route {
3539 (r.to_string(), 1.0, None)
3540 } else {
3541 self.classify_async(thought).await
3542 };
3543
3544 if route.is_none() && tool_name == "gnosis" && confidence < NLU_ABSTENTION_THRESHOLD {
3550 let alternative = crate::nlu::classify_with_alternative(thought).2;
3551 let mut meta = json!({
3552 "tool": tool_name,
3553 "confidence": confidence,
3554 "abstained": true
3555 });
3556 if let Some((alt_tool, alt_confidence)) = alternative {
3557 meta["suggested_route"] = json!(alt_tool);
3558 meta["suggested_confidence"] = json!(alt_confidence);
3559 }
3560 return Ok(json!({
3561 "status": "error",
3562 "message": "Could not confidently match your request to a tool.",
3563 "confidence": confidence,
3564 "hint": "Use explicit routing: wm(route='tool.name', args={...}). Use wm(route='tools.list') to see available tools.",
3565 "_wm_route": meta
3566 }));
3567 }
3568
3569 let mut route_meta = json!({ "tool": tool_name, "confidence": confidence });
3574 if route.is_none() && confidence < NLU_LOW_CONFIDENCE {
3575 route_meta["low_confidence"] = json!(true);
3576 if let (_, _, Some((alt_tool, alt_confidence))) =
3577 crate::nlu::classify_with_alternative(thought)
3578 {
3579 route_meta["alternative_route"] = json!(alt_tool);
3580 route_meta["alternative_confidence"] = json!(alt_confidence);
3581 }
3582 }
3583
3584 let mut tool_args = if passthrough_args.is_object() {
3586 let mut args = passthrough_args;
3590 if let Some(obj) = args.as_object_mut() {
3591 obj.remove("_meta");
3592 }
3593 args
3594 } else {
3595 Value::Null
3596 };
3597
3598 if route.is_none() && !thought.is_empty() && tool_args.is_null() {
3600 if let Some((param, value)) = Self::extract_payload(thought, &tool_name) {
3601 tool_args = json!({ param: value });
3602 }
3603 }
3604
3605 let tool = self.registry.get(&tool_name);
3607 match tool {
3608 Some(t) => {
3609 if route.is_none() && t.effects().destructive {
3616 return Ok(json!({
3617 "status": "error",
3618 "message": format!(
3619 "tool '{tool_name}' is destructive and cannot be reached via natural language — use wm(route='{tool_name}', args={{...}}) with \"confirm\": true"
3620 ),
3621 "_wm_route": route_meta.clone(),
3622 }));
3623 }
3624
3625 if let Some(required) = Self::required_arg(&tool_name) {
3627 let has_arg = tool_args.is_object()
3628 && tool_args.get(required).is_some()
3629 && !tool_args
3630 .get(required)
3631 .is_some_and(serde_json::Value::is_null);
3632 if !has_arg {
3633 return Ok(json!({
3634 "status": "error",
3635 "message": format!("Missing required argument: '{required}' for tool '{tool_name}'"),
3636 "hint": Self::missing_arg_hint(&tool_name, required),
3637 "_wm_route": route_meta.clone(),
3638 }));
3639 }
3640 }
3641
3642 let result = match &self.pipeline {
3648 Some(p) => p.dispatch(t.as_ref(), ctx, tool_args).await,
3649 None => t.call(ctx, tool_args).await,
3650 };
3651 if let Some(ref router) = self.embedding_router {
3658 let success = result.is_ok();
3659 if let Some(emb) = &query_emb {
3660 router.record_outcome_with_embedding(&tool_name, thought, success, emb);
3661 } else {
3662 let router = Arc::clone(router);
3663 let tool_name_owned = tool_name.clone();
3664 let thought_owned = thought.to_string();
3665 tokio::task::spawn_blocking(move || {
3666 router.record_outcome(&tool_name_owned, &thought_owned, success);
3667 });
3668 }
3669 }
3670 match result {
3671 Ok(mut output) => {
3672 if let Value::Object(ref mut map) = output {
3674 let mut meta = route_meta.clone();
3675 meta["input"] = json!(thought.chars().take(200).collect::<String>());
3676 map.insert("_wm_route".into(), meta);
3677 }
3678 Ok(output)
3679 }
3680 Err(e) => Ok(json!({
3681 "status": "error",
3682 "error": e.to_string(),
3683 "_wm_route": route_meta.clone(),
3684 })),
3685 }
3686 }
3687 None => Ok(json!({
3688 "status": "error",
3689 "message": format!("Unknown tool: '{tool_name}'"),
3690 "_wm_route": route_meta.clone(),
3691 })),
3692 }
3693 }
3694 fn stats(&self) -> &ToolStats {
3695 &self.stats
3696 }
3697}
3698
3699#[must_use]
3707pub fn required_arg_for(tool_name: &str) -> Option<&'static str> {
3708 WmMetaTool::required_arg(tool_name)
3709}
3710
3711fn parse_galaxy(s: &str) -> wm_core::Result<Galaxy> {
3713 expansion::common::parse_galaxy(s)
3714}
3715
3716#[allow(clippy::too_many_arguments)]
3723pub fn register_all(
3724 registry: &ToolRegistry,
3725 store: &Arc<MemoryStore>,
3726 search: Option<Arc<SearchEngine>>,
3727 karma: Option<Arc<KarmaLedger>>,
3728 dharma: &Option<Arc<DharmaGate>>,
3729 substrate: Option<Arc<SubstrateMonitor>>,
3730 resource_rules: &Option<Arc<ResourceRules>>,
3731 associations: Arc<AssociationStore>,
3732 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
3733 vector_store: Arc<std::sync::Mutex<VectorStore>>,
3734 conversational: Option<ConversationalSearch>,
3735 recall: Option<Arc<RecallEngine>>,
3736 homeostatic_loop: Option<Arc<std::sync::Mutex<HomeostaticLoop>>>,
3737 anomaly_detector: Option<Arc<std::sync::Mutex<AnomalyDetector>>>,
3738 sensorimotor_bus: Option<Arc<std::sync::Mutex<SensorimotorBus>>>,
3739 reflex_loop: Option<Arc<std::sync::Mutex<ReflexLoop>>>,
3740 gan_ying_bus: Option<&Arc<std::sync::Mutex<GanYingBus>>>,
3741 transaction_state: expansion::TransactionState,
3742 escalation_queue: Option<&Arc<std::sync::Mutex<wm_governance::EscalationQueue>>>,
3743 firewall: Option<&Arc<expansion::firewall::TxFirewall>>,
3744 code_graph: Option<&Arc<std::sync::Mutex<expansion::code::CodeGraph>>>,
3745 registry_persistence: expansion::RegistryPersistenceMode,
3746 circuit_breakers: Arc<wm_dispatch::CircuitBreakerRegistry>,
3747) -> ToolRegistry {
3748 let reg = registry
3749 .register(Arc::new(MemoryCreateTool::new(
3750 store.clone(),
3751 search.clone(),
3752 recall.clone(),
3753 )))
3754 .register(Arc::new(MemoryBatchCreateTool::new(
3755 store.clone(),
3756 search.clone(),
3757 recall.clone(),
3758 )))
3759 .register(Arc::new(MemoryReadTool::new(store.clone())))
3760 .register(Arc::new(MemoryListTool::new(store.clone())))
3761 .register(Arc::new(MemoryDeleteTool::new(
3762 store.clone(),
3763 search.clone(),
3764 )))
3765 .register(Arc::new(MemoryBatchDeleteTool::new(
3766 store.clone(),
3767 search.clone(),
3768 )))
3769 .register(Arc::new(MemoryQueryTool::new(store.clone())))
3770 .register(Arc::new(MemoryAssociateTool::new(store.clone())))
3771 .register(Arc::new(MemoryAssociationsTool::new(store.clone())))
3772 .register(Arc::new(MemoryVectorSearchTool::new(
3773 store.clone(),
3774 vector_store,
3775 )))
3776 .register(Arc::new(GnosisTool::new(store.clone())))
3777 .register(Arc::new(expansion::MemoryReembedTool::new(recall.clone())));
3779
3780 let mut reg = expansion::breaker_tools::register_breakers(®, circuit_breakers);
3783
3784 if let Some(conv) = conversational {
3785 reg = reg.register(Arc::new(MemoryChatTool::new(conv)));
3786 }
3787
3788 if let Some(s) = search {
3789 reg = reg.register(Arc::new(
3793 expansion::MemoryHybridRecallTool::as_search(
3794 store.clone(),
3795 Some(s.clone()),
3796 recall.clone(),
3797 )
3798 .with_associations(Some(associations.clone())),
3799 ));
3800 reg = expansion::register_expansion(
3802 ®,
3803 store,
3804 Some(s),
3805 recall,
3806 associations,
3807 spiral_tracker,
3808 karma.clone(),
3809 substrate.clone(),
3810 homeostatic_loop,
3811 anomaly_detector,
3812 sensorimotor_bus,
3813 reflex_loop,
3814 gan_ying_bus,
3815 transaction_state,
3816 resource_rules.as_ref(),
3817 escalation_queue,
3818 dharma.as_ref(),
3819 firewall,
3820 code_graph,
3821 registry_persistence,
3822 );
3823 } else {
3824 reg = expansion::register_expansion(
3825 ®,
3826 store,
3827 None,
3828 recall,
3829 associations,
3830 spiral_tracker,
3831 karma.clone(),
3832 substrate.clone(),
3833 homeostatic_loop,
3834 anomaly_detector,
3835 sensorimotor_bus,
3836 reflex_loop,
3837 gan_ying_bus,
3838 transaction_state,
3839 resource_rules.as_ref(),
3840 escalation_queue,
3841 dharma.as_ref(),
3842 firewall,
3843 code_graph,
3844 registry_persistence,
3845 );
3846 }
3847 if let Some(k) = karma {
3848 reg = reg.register(Arc::new(KarmaReportTool::new(k)));
3849 }
3850 if let Some(d) = dharma {
3851 reg = reg.register(Arc::new(DharmaStatusTool::new(d.clone())));
3852 }
3853 if let Some(s) = substrate {
3854 reg = reg
3855 .register(Arc::new(HarmonyVectorTool::new(s.clone())))
3856 .register(Arc::new(HarmonyHistoryTool::new(s.clone())));
3857 if let Some(d) = dharma {
3858 if let Some(r) = resource_rules {
3859 reg = reg
3860 .register(Arc::new(GnosisStatusTool::new(
3861 d.clone(),
3862 r.clone(),
3863 s.clone(),
3864 )))
3865 .register(Arc::new(GnosisHistoryTool::new(s)))
3866 .register(Arc::new(GnosisExplainTool::new(d.clone(), r.clone())));
3867 }
3868 }
3869 }
3870
3871 reg
3872}
3873
3874pub fn register_meta_tools(
3880 registry: &ToolRegistry,
3881 store: &Arc<MemoryStore>,
3882 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3883) -> ToolRegistry {
3884 register_meta_tools_with_router(registry, store, shadow_stats, None).0
3885}
3886
3887#[must_use]
3897pub fn register_meta_tools_with_router(
3898 registry: &ToolRegistry,
3899 store: &Arc<MemoryStore>,
3900 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3901 pipeline: Option<Arc<DispatchPipeline>>,
3902) -> (ToolRegistry, Option<Arc<embedding_router::EmbeddingRouter>>) {
3903 let base_snapshot: Vec<Arc<dyn Tool>> = registry.all();
3904 let tool_count = base_snapshot.len();
3906
3907 let non_gnosis: Vec<Arc<dyn Tool>> = base_snapshot
3908 .iter()
3909 .filter(|t| t.name() != "gnosis")
3910 .cloned()
3911 .collect();
3912
3913 let mut list_builder = ToolRegistryBuilder::new();
3915 for tool in &non_gnosis {
3916 list_builder.register(tool.clone());
3917 }
3918 let list_registry = Arc::new(list_builder.build());
3919 let tools_list = Arc::new(ToolsListTool::new(Arc::clone(&list_registry)));
3920
3921 let usage_report = Arc::new(expansion::ToolsUsageReportTool::new(list_registry));
3925
3926 let gnosis = Arc::new(GnosisTool::with_tool_count(Arc::clone(store), tool_count));
3928 let mut wm_builder = ToolRegistryBuilder::new();
3929 for tool in &non_gnosis {
3930 wm_builder.register(tool.clone());
3931 }
3932 wm_builder.register(tools_list.clone());
3933 wm_builder.register(usage_report.clone());
3934 wm_builder.register(gnosis.clone());
3935
3936 let shadow_report = Arc::new(expansion::NluShadowReportTool::new(Arc::clone(
3941 &shadow_stats,
3942 )));
3943 wm_builder.register(shadow_report.clone());
3944 let wm = Arc::new(WmMetaTool::with_router_shadow_stats_and_pipeline(
3945 Arc::new(wm_builder.build()),
3946 wm_memory::create_embedder(),
3947 shadow_stats,
3948 pipeline,
3949 ));
3950 let router = wm.embedding_router().cloned();
3951
3952 let mut final_builder = ToolRegistryBuilder::new();
3954 for tool in non_gnosis {
3955 final_builder.register(tool);
3956 }
3957 final_builder.register(tools_list);
3958 final_builder.register(usage_report);
3959 final_builder.register(wm);
3960 final_builder.register(gnosis);
3961 final_builder.register(shadow_report);
3962 (final_builder.build(), router)
3963}
3964
3965#[cfg(test)]
3966mod tests {
3967 use super::*;
3968 use std::collections::BTreeMap;
3969 use std::path::{Path, PathBuf};
3970 use wm_core::BrainWave;
3971
3972 fn test_store() -> Arc<MemoryStore> {
3973 let tmp = tempfile::tempdir().unwrap();
3974 Arc::new(MemoryStore::open_default(tmp.path()).unwrap())
3975 }
3976
3977 fn cold_factors() -> wm_memory::cold_storage::OuterRimFactors {
3978 wm_memory::cold_storage::OuterRimFactors {
3979 age_factor: 1.0,
3980 access_factor: 1.0,
3981 resonance_factor: 1.0,
3982 emotional_factor: 1.0,
3983 importance_factor: 1.0,
3984 distance: 1.0,
3985 }
3986 }
3987
3988 fn freeze_for_read_test(
3989 store: &MemoryStore,
3990 galaxy: Galaxy,
3991 content: &str,
3992 is_private: bool,
3993 ) -> (uuid::Uuid, wm_memory::cold_storage::ColdRecord) {
3994 let mut memory = wm_memory::Memory::new(galaxy, content.to_string());
3995 memory.metadata.is_private = is_private;
3996 let id = memory.metadata.id;
3997 store.put(galaxy, &memory).unwrap();
3998 let record = store
3999 .freeze_to_cold(
4000 None,
4001 id,
4002 1.0,
4003 cold_factors(),
4004 None,
4005 None,
4006 wm_memory::cold_storage::CompressionCodec::Gzip,
4007 )
4008 .unwrap();
4009 (id, record)
4010 }
4011
4012 fn readonly_tree_snapshot(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
4013 fn visit(root: &Path, path: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
4014 for entry in std::fs::read_dir(path).unwrap() {
4015 let entry = entry.unwrap();
4016 let entry_path = entry.path();
4017 let relative = entry_path.strip_prefix(root).unwrap().to_path_buf();
4018 if relative == Path::new("lock.mdb") {
4019 continue;
4020 }
4021 if entry.file_type().unwrap().is_dir() {
4022 out.insert(relative.clone(), Vec::new());
4023 visit(root, &entry_path, out);
4024 } else {
4025 out.insert(relative, std::fs::read(entry_path).unwrap());
4026 }
4027 }
4028 }
4029
4030 let mut snapshot = BTreeMap::new();
4031 visit(root, root, &mut snapshot);
4032 snapshot
4033 }
4034
4035 #[tokio::test]
4036 async fn memory_create_warns_on_credential_shaped_content() {
4037 let store = test_store();
4038 let tool = MemoryCreateTool::new(store, None, None);
4039 let mut ctx = Context::default();
4040
4041 let clean = tool
4042 .call(
4043 &mut ctx,
4044 json!({"content": "the password policy requires rotation"}),
4045 )
4046 .await
4047 .unwrap();
4048 assert!(clean.get("warnings").is_none(), "clean content: {clean}");
4049
4050 let flagged = tool
4051 .call(
4052 &mut ctx,
4053 json!({"content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEow\n-----END RSA PRIVATE KEY-----"}),
4054 )
4055 .await
4056 .unwrap();
4057 assert_eq!(
4058 flagged["status"], "success",
4059 "warning, not refusal: {flagged}"
4060 );
4061 let warnings = flagged["warnings"].as_array().unwrap();
4062 assert!(
4063 warnings[0].as_str().unwrap().contains("private_key_pem"),
4064 "got: {warnings:?}"
4065 );
4066 assert!(warnings[0].as_str().unwrap().contains("keyring"));
4067 }
4068
4069 #[tokio::test]
4070 async fn memory_batch_create_aggregates_credential_warnings() {
4071 let store = test_store();
4072 let tool = MemoryBatchCreateTool::new(store, None, None);
4073 let mut ctx = Context::default();
4074 let r = tool
4075 .call(
4076 &mut ctx,
4077 json!({"items": [
4078 {"content": "ordinary note"},
4079 {"content": "AKIAIOSFODNN7EXAMPLE"},
4080 ]}),
4081 )
4082 .await
4083 .unwrap();
4084 assert_eq!(r["count"], 2);
4085 let warnings = r["warnings"].as_array().unwrap();
4086 assert!(warnings[0].as_str().unwrap().contains("aws_access_key_id"));
4087 }
4088
4089 fn test_registry_with(store: &Arc<MemoryStore>) -> ToolRegistry {
4090 let registry = ToolRegistry::new();
4091 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
4092 let spiral_tracker =
4093 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
4094 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4095 register_all(
4096 ®istry,
4097 store,
4098 None,
4099 None,
4100 &None,
4101 None,
4102 &None,
4103 associations,
4104 spiral_tracker,
4105 vector_store,
4106 None,
4107 None,
4108 None,
4109 None,
4110 None,
4111 None,
4112 None,
4113 std::sync::Arc::new(std::sync::Mutex::new(None)),
4114 None,
4115 None,
4116 None,
4117 expansion::RegistryPersistenceMode::Normal,
4118 Arc::new(wm_dispatch::CircuitBreakerRegistry::default()),
4119 )
4120 }
4121
4122 #[tokio::test]
4123 async fn memory_create_and_read() {
4124 let store = test_store();
4125 let tool = MemoryCreateTool::new(store.clone(), None, None);
4126 let mut ctx = Context::new(BrainWave::Gamma);
4127
4128 let args = json!({"content": "test memory content", "galaxy": "codex"});
4129 let result = tool.call(&mut ctx, args).await.unwrap();
4130 assert_eq!(result["status"], "success");
4131 let id = result["id"].as_str().unwrap();
4132
4133 let read_tool = MemoryReadTool::new(store.clone());
4134 let result = read_tool.call(&mut ctx, json!({"id": id})).await.unwrap();
4135 assert_eq!(result["status"], "success");
4136 assert_eq!(result["content"], "test memory content");
4137
4138 let episodic = store
4139 .episodic()
4140 .get(uuid::Uuid::parse_str(id).unwrap())
4141 .unwrap()
4142 .expect("explicit memory writes mirror into episodic storage");
4143 assert_eq!(episodic.content, "test memory content");
4144 }
4145
4146 #[tokio::test]
4147 async fn memory_read_recovers_cold_content_after_reopen_without_thawing() {
4148 let directory = tempfile::tempdir().unwrap();
4149 let path = directory.path().to_path_buf();
4150 let content = "cold UTF-8: cafe\u{301} \u{1f980}\nsecond line — exact".repeat(128);
4151 let (id, before) = {
4152 let store = MemoryStore::open_default(&path).unwrap();
4153 freeze_for_read_test(&store, Galaxy::Codex, &content, false)
4154 };
4155
4156 let store = Arc::new(MemoryStore::open_default(&path).unwrap());
4157 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
4158 assert_eq!(store.get_cold_record(id).unwrap().as_ref(), Some(&before));
4159 let before_read_tree = readonly_tree_snapshot(&path);
4160
4161 let mut ctx = Context::default();
4162 let result = MemoryReadTool::new(store.clone())
4163 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4164 .await
4165 .unwrap();
4166 assert_eq!(result["status"], "success");
4167 assert_eq!(result["content"], content);
4168
4169 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
4172 assert_eq!(store.get_cold_record(id).unwrap().as_ref(), Some(&before));
4173 assert_eq!(readonly_tree_snapshot(&path), before_read_tree);
4174 drop(store);
4175 let reopened = MemoryStore::open_default(&path).unwrap();
4176 assert!(reopened.get(Galaxy::Codex, id).unwrap().is_none());
4177 assert_eq!(
4178 reopened.get_cold_record(id).unwrap().as_ref(),
4179 Some(&before)
4180 );
4181 }
4182
4183 #[tokio::test]
4184 async fn memory_read_cold_fallback_is_galaxy_bound_and_missing_is_not_found() {
4185 let store = test_store();
4186 let (id, _) = freeze_for_read_test(&store, Galaxy::Codex, "cold codex only", false);
4187 let mut ctx = Context::default();
4188 let tool = MemoryReadTool::new(store);
4189
4190 let wrong_galaxy = tool
4191 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4192 .await
4193 .unwrap();
4194 assert_eq!(wrong_galaxy["status"], "not_found");
4195 assert_eq!(wrong_galaxy["galaxy"], "sessions");
4196 assert!(wrong_galaxy.get("content").is_none());
4197
4198 let missing = tool
4199 .call(
4200 &mut ctx,
4201 json!({"id": uuid::Uuid::new_v4(), "galaxy": "codex"}),
4202 )
4203 .await
4204 .unwrap();
4205 assert_eq!(missing["status"], "not_found");
4206 assert!(missing.get("content").is_none());
4207 }
4208
4209 #[tokio::test]
4210 async fn memory_read_private_cold_record_is_not_found_without_headers() {
4211 let store = test_store();
4212 let (id, _) = freeze_for_read_test(&store, Galaxy::Codex, "private cold content", true);
4213 let mut ctx = Context::default();
4214 let result = MemoryReadTool::new(store)
4215 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4216 .await
4217 .unwrap();
4218 assert_eq!(result["status"], "not_found");
4219 assert!(result.get("content").is_none());
4220 assert!(result.get("tags").is_none());
4221 assert!(result.get("created_at").is_none());
4222 }
4223
4224 #[tokio::test]
4225 async fn memory_read_refuses_corrupt_cold_payload_or_header_mismatch() {
4226 let store = test_store();
4227 let (payload_id, mut payload_record) =
4228 freeze_for_read_test(&store, Galaxy::Codex, "payload integrity", false);
4229 payload_record.compressed_payload[0] ^= 0xff;
4230 store.put_cold_record(&payload_record).unwrap();
4231
4232 let mut ctx = Context::default();
4233 let tool = MemoryReadTool::new(store.clone());
4234 assert!(
4235 tool.call(&mut ctx, json!({"id": payload_id, "galaxy": "codex"}))
4236 .await
4237 .is_err()
4238 );
4239 assert!(store.get(Galaxy::Codex, payload_id).unwrap().is_none());
4240
4241 let (header_id, mut header_record) =
4242 freeze_for_read_test(&store, Galaxy::Codex, "header integrity", false);
4243 header_record.content_hash = "wrong-header-hash".into();
4244 store.put_cold_record(&header_record).unwrap();
4245 assert!(
4246 tool.call(&mut ctx, json!({"id": header_id, "galaxy": "codex"}))
4247 .await
4248 .is_err()
4249 );
4250 assert!(store.get(Galaxy::Codex, header_id).unwrap().is_none());
4251 }
4252
4253 #[tokio::test]
4258 async fn memory_create_attestation_disclosure() {
4259 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
4260 let mut ctx = Context::new(BrainWave::Gamma);
4261
4262 let store = test_store();
4264 let tool = MemoryCreateTool::with_attestation_key(store.clone(), None, None, None);
4265 let result = tool
4266 .call(
4267 &mut ctx,
4268 json!({"content": "unattested create", "galaxy": "codex"}),
4269 )
4270 .await
4271 .unwrap();
4272 assert_eq!(result["status"], "success");
4273 assert_eq!(result["attested"], false);
4274 assert_eq!(result["attested_reason"], "node key unavailable");
4275
4276 let tool = MemoryCreateTool::with_attestation_key(
4278 store.clone(),
4279 None,
4280 None,
4281 Some("not-hex".to_string()),
4282 );
4283 let result = tool
4284 .call(
4285 &mut ctx,
4286 json!({"content": "bad key create", "galaxy": "codex"}),
4287 )
4288 .await
4289 .unwrap();
4290 assert_eq!(result["attested"], false);
4291 assert_eq!(result["attested_reason"], "node key invalid");
4292
4293 let tool = MemoryCreateTool::with_attestation_key(
4295 store.clone(),
4296 None,
4297 None,
4298 Some(TEST_KEY.to_string()),
4299 );
4300 let result = tool
4301 .call(
4302 &mut ctx,
4303 json!({"content": "attested create", "galaxy": "codex"}),
4304 )
4305 .await
4306 .unwrap();
4307 assert_eq!(result["attested"], true);
4308 assert!(result.get("attested_reason").is_none());
4309 let id = uuid::Uuid::parse_str(result["id"].as_str().unwrap()).unwrap();
4310 let report = store.verify_attestation(Galaxy::Codex, id).unwrap();
4311 assert!(report.attested, "{:?}", report.breaks);
4312 assert!(report.signature_valid, "{:?}", report.breaks);
4313 assert!(report.matches_head, "{:?}", report.breaks);
4314 assert!(report.memory_present);
4315 assert!(report.breaks.is_empty());
4316
4317 let mut memory = store.get(Galaxy::Codex, id).unwrap().unwrap();
4321 memory.content = "edited after attestation".to_string();
4322 memory.metadata.content_hash = wm_memory::content_hash(&memory.content);
4323 store.put(Galaxy::Codex, &memory).unwrap();
4324 let stale = store.verify_attestation(Galaxy::Codex, id).unwrap();
4325 assert!(stale.attested);
4326 assert!(stale.signature_valid);
4327 assert!(!stale.matches_head);
4328
4329 let scanned = store.scan_attestations().unwrap();
4331 assert_eq!(scanned.len(), 1);
4332 assert_eq!(scanned[0].memory_id, id.to_string());
4333 }
4334
4335 #[tokio::test]
4336 async fn memory_batch_create_attests_each_item() {
4337 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
4338 let store = test_store();
4339 let tool = MemoryBatchCreateTool::with_attestation_key(
4340 store.clone(),
4341 None,
4342 None,
4343 Some(TEST_KEY.to_string()),
4344 );
4345 let mut ctx = Context::new(BrainWave::Gamma);
4346 let result = tool
4347 .call(
4348 &mut ctx,
4349 json!({"items": [{"content": "batch one"}, {"content": "batch two"}]}),
4350 )
4351 .await
4352 .unwrap();
4353 assert_eq!(result["attested_count"], 2);
4354 assert_eq!(store.scan_attestations().unwrap().len(), 2);
4355
4356 let tool = MemoryBatchCreateTool::with_attestation_key(store.clone(), None, None, None);
4358 let result = tool
4359 .call(&mut ctx, json!({"items": [{"content": "batch three"}]}))
4360 .await
4361 .unwrap();
4362 assert_eq!(result["attested_count"], 0);
4363 assert_eq!(result["count"], 1);
4364 }
4365
4366 #[tokio::test]
4367 async fn memory_batch_create_mirrors_into_episodic_lane() {
4368 let store = test_store();
4369 let tool = MemoryBatchCreateTool::new(store.clone(), None, None);
4370 let mut ctx = Context::new(BrainWave::Gamma);
4371 let result = tool
4372 .call(
4373 &mut ctx,
4374 json!({
4375 "items": [
4376 {"content": "batch rust retrieval"},
4377 {"content": "batch grocery list"}
4378 ]
4379 }),
4380 )
4381 .await
4382 .unwrap();
4383 assert_eq!(result["status"], "success");
4384 let ids = result["ids"].as_array().unwrap();
4385 let first = uuid::Uuid::parse_str(ids[0].as_str().unwrap()).unwrap();
4386 let hits = store
4387 .episodic()
4388 .search("rust retrieval", 10, false)
4389 .unwrap();
4390 assert_eq!(hits.len(), 1);
4391 assert_eq!(hits[0].record.id, first);
4392 }
4393
4394 #[tokio::test]
4395 async fn memory_list_returns_entries() {
4396 let store = test_store();
4397 let create = MemoryCreateTool::new(store.clone(), None, None);
4398 let mut ctx = Context::new(BrainWave::Gamma);
4399
4400 for i in 0..3 {
4401 create
4402 .call(&mut ctx, json!({"content": format!("item-{i}")}))
4403 .await
4404 .unwrap();
4405 }
4406
4407 let list = MemoryListTool::new(store);
4408 let result = list.call(&mut ctx, json!({"limit": 10})).await.unwrap();
4409 assert_eq!(result["status"], "success");
4410 assert_eq!(result["total"], 3);
4411 assert_eq!(result["returned"], 3);
4412 }
4413
4414 #[tokio::test]
4418 async fn memory_list_offset_and_exclude_tags_page_visible_surface() {
4419 let store = test_store();
4420 let mut ctx = Context::new(BrainWave::Gamma);
4421
4422 for i in 0..5 {
4423 let mut m = wm_memory::Memory::new(wm_core::Galaxy::Codex, format!("page note {i}"));
4424 if i == 1 {
4425 m.metadata.tags = vec!["noise".into()];
4426 }
4427 if i == 3 {
4428 m.metadata.is_private = true;
4429 }
4430 store.put(wm_core::Galaxy::Codex, &m).unwrap();
4431 }
4432
4433 let list = MemoryListTool::new(store);
4434
4435 let all = list
4438 .call(
4439 &mut ctx,
4440 json!({"galaxy": "codex", "limit": 50, "exclude_tags": ["noise"]}),
4441 )
4442 .await
4443 .unwrap();
4444 assert_eq!(all["total"], 5, "total counts the whole galaxy");
4445 assert_eq!(all["matched"], 3, "private + excluded are invisible");
4446 assert_eq!(all["returned"], 3);
4447 assert_eq!(all["offset"], 0);
4448
4449 let page1 = list
4451 .call(
4452 &mut ctx,
4453 json!({"galaxy": "codex", "limit": 2, "offset": 0, "exclude_tags": ["noise"]}),
4454 )
4455 .await
4456 .unwrap();
4457 assert_eq!(page1["returned"], 2);
4458 let page2 = list
4459 .call(
4460 &mut ctx,
4461 json!({"galaxy": "codex", "limit": 2, "offset": 2, "exclude_tags": ["noise"]}),
4462 )
4463 .await
4464 .unwrap();
4465 assert_eq!(
4466 page2["returned"], 1,
4467 "matched is 3 — the tail page is short"
4468 );
4469 assert_eq!(page2["offset"], 2);
4470
4471 let ids_of = |v: &Value| -> Vec<String> {
4472 v["memories"]
4473 .as_array()
4474 .unwrap()
4475 .iter()
4476 .filter_map(|m| m["id"].as_str().map(String::from))
4477 .collect()
4478 };
4479 let (p1, p2, everything) = (ids_of(&page1), ids_of(&page2), ids_of(&all));
4480 assert_eq!(p1.len(), 2);
4481 let mut union = p1;
4482 union.extend(p2);
4483 let mut sorted_union = union.clone();
4484 sorted_union.sort();
4485 let mut sorted_all = everything;
4486 sorted_all.sort();
4487 assert_eq!(sorted_union, sorted_all, "pages must partition the surface");
4488 }
4489
4490 #[tokio::test]
4495 async fn memory_create_stamps_provenance_by_claim() {
4496 let store = test_store();
4497 let create = MemoryCreateTool::new(store.clone(), None, None);
4498 let mut ctx = Context::new(BrainWave::Gamma);
4499
4500 let silent = create
4501 .call(&mut ctx, json!({"content": "no claim"}))
4502 .await
4503 .unwrap();
4504 assert_eq!(silent["source"], "agent");
4505 assert!((silent["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
4506
4507 let claimed = create
4508 .call(
4509 &mut ctx,
4510 json!({"content": "user dictated this", "source": "user"}),
4511 )
4512 .await
4513 .unwrap();
4514 assert_eq!(claimed["source"], "user");
4515 assert!((claimed["source_trust"].as_f64().unwrap() - 1.0).abs() < 1e-5);
4516
4517 let custom = create
4518 .call(&mut ctx, json!({"content": "web import", "source": "web"}))
4519 .await
4520 .unwrap();
4521 assert_eq!(custom["source"], "web");
4522 assert!((custom["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
4523
4524 let fetch = |id: &str| {
4525 store
4526 .get(wm_core::Galaxy::Codex, uuid::Uuid::parse_str(id).unwrap())
4527 .expect("stored")
4528 .expect("present")
4529 };
4530 assert_eq!(
4531 fetch(silent["id"].as_str().unwrap()).metadata.source,
4532 "agent"
4533 );
4534 assert_eq!(
4535 fetch(claimed["id"].as_str().unwrap()).metadata.source,
4536 "user"
4537 );
4538 }
4539
4540 #[tokio::test]
4541 async fn gnosis_returns_system_info() {
4542 let store = test_store();
4543 let tool = GnosisTool::new(store);
4544 let mut ctx = Context::new(BrainWave::Gamma);
4545 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4546 assert_eq!(result["status"], "success");
4547 assert!(result["version"].is_string());
4548 }
4549
4550 #[tokio::test]
4551 async fn memory_delete_removes_entry() {
4552 let store = test_store();
4553 let create = MemoryCreateTool::new(store.clone(), None, None);
4554 let mut ctx = Context::new(BrainWave::Gamma);
4555
4556 let result = create
4557 .call(&mut ctx, json!({"content": "to be deleted"}))
4558 .await
4559 .unwrap();
4560 let id = result["id"].as_str().unwrap();
4561
4562 let delete = MemoryDeleteTool::new(store.clone(), None);
4563 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4564 assert_eq!(result["status"], "success");
4565
4566 let read = MemoryReadTool::new(store);
4567 let result = read.call(&mut ctx, json!({"id": id})).await.unwrap();
4568 assert_eq!(result["status"], "not_found");
4569 }
4570
4571 #[tokio::test]
4572 async fn memory_delete_without_galaxy_resolves_across_memory_galaxies() {
4573 let store = test_store();
4574 let create = MemoryCreateTool::new(store.clone(), None, None);
4575 let mut ctx = Context::new(BrainWave::Gamma);
4576
4577 let result = create
4579 .call(
4580 &mut ctx,
4581 json!({"content": "session decision", "galaxy": "sessions"}),
4582 )
4583 .await
4584 .unwrap();
4585 let id = result["id"].as_str().unwrap();
4586
4587 let delete = MemoryDeleteTool::new(store.clone(), None);
4589 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4590 assert_eq!(result["status"], "success");
4591 assert!(
4592 result["galaxies"]
4593 .as_array()
4594 .unwrap()
4595 .contains(&json!("sessions"))
4596 );
4597
4598 let read = MemoryReadTool::new(store.clone());
4599 let result = read
4600 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4601 .await
4602 .unwrap();
4603 assert_eq!(result["status"], "not_found");
4604 }
4605
4606 #[tokio::test]
4607 async fn memory_delete_explicit_galaxy_does_not_miss_other_galaxies() {
4608 let store = test_store();
4609 let create = MemoryCreateTool::new(store.clone(), None, None);
4610 let mut ctx = Context::new(BrainWave::Gamma);
4611
4612 let result = create
4613 .call(
4614 &mut ctx,
4615 json!({"content": "in sessions", "galaxy": "sessions"}),
4616 )
4617 .await
4618 .unwrap();
4619 let id = result["id"].as_str().unwrap();
4620
4621 let delete = MemoryDeleteTool::new(store.clone(), None);
4623 let result = delete
4624 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4625 .await
4626 .unwrap();
4627 assert_eq!(result["status"], "not_found");
4628 assert!(result["hint"].is_string());
4629
4630 let read = MemoryReadTool::new(store.clone());
4631 let result = read
4632 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4633 .await
4634 .unwrap();
4635 assert_eq!(result["status"], "success");
4636 }
4637
4638 #[tokio::test]
4639 async fn memory_query_filters_by_tags() {
4640 let store = test_store();
4641 let create = MemoryCreateTool::new(store.clone(), None, None);
4642 let mut ctx = Context::new(BrainWave::Gamma);
4643
4644 create
4645 .call(&mut ctx, json!({"content": "tagged", "tags": ["rust"]}))
4646 .await
4647 .unwrap();
4648 create
4649 .call(&mut ctx, json!({"content": "untagged"}))
4650 .await
4651 .unwrap();
4652
4653 let query = MemoryQueryTool::new(store);
4654 let result = query
4655 .call(&mut ctx, json!({"tags": ["rust"]}))
4656 .await
4657 .unwrap();
4658 assert_eq!(result["status"], "success");
4659 assert_eq!(result["total"], 1);
4660 }
4661
4662 #[tokio::test]
4665 async fn memory_query_time_range_passthrough() {
4666 let store = test_store();
4667 let mut ctx = Context::new(BrainWave::Gamma);
4668
4669 let mut old = wm_memory::Memory::new(wm_core::Galaxy::Codex, "old relic".into());
4670 old.metadata.created_at = chrono::Utc::now() - chrono::Duration::days(60);
4671 store.put(wm_core::Galaxy::Codex, &old).unwrap();
4672 let mut recent = wm_memory::Memory::new(wm_core::Galaxy::Codex, "recent note".into());
4673 recent.metadata.created_at = chrono::Utc::now() - chrono::Duration::hours(1);
4674 store.put(wm_core::Galaxy::Codex, &recent).unwrap();
4675
4676 let query = MemoryQueryTool::new(store);
4677 let cutoff = (chrono::Utc::now() - chrono::Duration::days(1))
4678 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
4679
4680 let only_recent = query
4681 .call(&mut ctx, json!({"created_after": cutoff}))
4682 .await
4683 .unwrap();
4684 assert_eq!(only_recent["total"], 1);
4685 assert_eq!(only_recent["memories"][0]["content_preview"], "recent note");
4686 assert_eq!(
4687 only_recent["time_range"]["created_after"], cutoff,
4688 "the applied time range must be disclosed"
4689 );
4690
4691 let only_old = query
4692 .call(&mut ctx, json!({"created_before": cutoff}))
4693 .await
4694 .unwrap();
4695 assert_eq!(only_old["total"], 1);
4696 assert_eq!(only_old["memories"][0]["content_preview"], "old relic");
4697
4698 let both = query
4700 .call(
4701 &mut ctx,
4702 json!({
4703 "created_after": (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
4704 "created_before": cutoff,
4705 }),
4706 )
4707 .await
4708 .unwrap();
4709 assert_eq!(both["total"], 1);
4710 assert_eq!(both["memories"][0]["content_preview"], "old relic");
4711
4712 let bad = query
4714 .call(&mut ctx, json!({"created_after": "not-a-timestamp"}))
4715 .await;
4716 assert!(bad.is_err(), "invalid RFC 3339 must be refused");
4717 }
4718
4719 #[tokio::test]
4720 async fn memory_vector_search_by_embedding() {
4721 let store = test_store();
4722 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4723
4724 {
4726 let mut vs = vector_store.lock().unwrap();
4727 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![1.0, 0.0, 0.0]);
4728 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![0.9, 0.1, 0.0]);
4729 vs.add(uuid::Uuid::new_v4(), Galaxy::Research, vec![0.0, 1.0, 0.0]);
4730 }
4731
4732 let tool = MemoryVectorSearchTool::new(store, vector_store);
4733 let mut ctx = Context::new(BrainWave::Gamma);
4734
4735 let result = tool
4737 .call(&mut ctx, json!({"embedding": [1.0, 0.0, 0.0], "limit": 2}))
4738 .await
4739 .unwrap();
4740 assert_eq!(result["status"], "success");
4741 assert_eq!(result["total"], 2);
4742 }
4743
4744 #[tokio::test]
4745 async fn memory_vector_search_missing_args() {
4746 let store = test_store();
4747 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4748
4749 let tool = MemoryVectorSearchTool::new(store, vector_store);
4750 let mut ctx = Context::new(BrainWave::Gamma);
4751
4752 let result = tool.call(&mut ctx, json!({"limit": 5})).await;
4753 assert!(result.is_err());
4754 }
4755
4756 #[tokio::test]
4757 async fn wm_routes_vector_search_to_memory_vector_search() {
4758 let store = test_store();
4759 let registry = test_registry_with(&store);
4760 let registry = register_meta_tools(
4761 ®istry,
4762 &store,
4763 std::sync::Arc::new(std::sync::RwLock::new(
4764 embedding_router::ShadowModeStats::default(),
4765 )),
4766 );
4767
4768 let wm = registry.get("wm").unwrap();
4769 let mut ctx = Context::new(BrainWave::Gamma);
4770 let result = wm
4771 .call(
4772 &mut ctx,
4773 json!({"route": "memory.vector.search", "args": {"embedding": [1.0, 0.0, 0.0]}}),
4774 )
4775 .await
4776 .unwrap();
4777
4778 assert_eq!(result["status"], "success");
4779 assert_eq!(result["_wm_route"]["tool"], "memory.vector.search");
4780 }
4781
4782 #[tokio::test]
4783 async fn wm_routes_shadow_report_inside_meta_tool() {
4784 let store = test_store();
4788 let registry = test_registry_with(&store);
4789 let registry = register_meta_tools(
4790 ®istry,
4791 &store,
4792 std::sync::Arc::new(std::sync::RwLock::new(
4793 embedding_router::ShadowModeStats::default(),
4794 )),
4795 );
4796
4797 let wm = registry.get("wm").unwrap();
4798 let mut ctx = Context::new(BrainWave::Gamma);
4799 let result = wm
4800 .call(&mut ctx, json!({"route": "nlu.shadow_report"}))
4801 .await
4802 .unwrap();
4803
4804 assert_eq!(result["_wm_route"]["tool"], "nlu.shadow_report");
4805 assert!(
4806 result.get("total_queries").is_some(),
4807 "expected shadow report payload"
4808 );
4809 }
4810
4811 #[tokio::test]
4812 async fn memory_associate_and_find() {
4813 let store = test_store();
4814 let create = MemoryCreateTool::new(store.clone(), None, None);
4815 let mut ctx = Context::new(BrainWave::Gamma);
4816
4817 let r1 = create
4818 .call(&mut ctx, json!({"content": "source mem"}))
4819 .await
4820 .unwrap();
4821 let r2 = create
4822 .call(&mut ctx, json!({"content": "target mem"}))
4823 .await
4824 .unwrap();
4825 let id1 = r1["id"].as_str().unwrap();
4826 let id2 = r2["id"].as_str().unwrap();
4827
4828 let assoc = MemoryAssociateTool::new(store.clone());
4829 let result = assoc
4830 .call(
4831 &mut ctx,
4832 json!({"source": id1, "target": id2, "weight": 0.8}),
4833 )
4834 .await
4835 .unwrap();
4836 assert_eq!(result["status"], "success");
4837
4838 let find = MemoryAssociationsTool::new(store);
4839 let result = find
4840 .call(&mut ctx, json!({"id": id1, "direction": "from"}))
4841 .await
4842 .unwrap();
4843 assert_eq!(result["status"], "success");
4844 assert_eq!(result["returned"], 1);
4845 }
4846
4847 #[tokio::test]
4848 async fn karma_report_shows_entries() {
4849 let tmp = tempfile::tempdir().unwrap();
4850 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
4851 let ledger = Arc::new(KarmaLedger::new(store).unwrap());
4852
4853 ledger.record("test_tool", false, 0, true).unwrap();
4855 ledger.record("wasteful_tool", true, 0, true).unwrap();
4856
4857 let tool = KarmaReportTool::new(ledger);
4858 let mut ctx = Context::new(BrainWave::Gamma);
4859 let result = tool.call(&mut ctx, json!({"limit": 5})).await.unwrap();
4860 assert_eq!(result["status"], "success");
4861 assert_eq!(result["entry_count"], 2);
4862 assert_eq!(result["recent_entries"].as_array().unwrap().len(), 2);
4863 }
4864
4865 #[tokio::test]
4866 async fn dharma_status_returns_homeostasis() {
4867 let gate = Arc::new(DharmaGate::default());
4868 let tool = DharmaStatusTool::new(gate);
4869 let mut ctx = Context::new(BrainWave::Gamma);
4870 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4871 assert_eq!(result["status"], "success");
4872 assert!(result["homeostasis"]["health_score"].is_f64());
4873 assert!(result["sutras"]["ahimsa"].is_string());
4874 assert!(result["decisions"]["total"].is_u64());
4875 assert!(result["decisions"]["blocked_ratio"].is_number());
4876 }
4877
4878 #[tokio::test]
4879 async fn wm_routes_remember_to_memory_create() {
4880 let store = test_store();
4881 let registry = test_registry_with(&store);
4882 let registry = register_meta_tools(
4883 ®istry,
4884 &store,
4885 std::sync::Arc::new(std::sync::RwLock::new(
4886 embedding_router::ShadowModeStats::default(),
4887 )),
4888 );
4889
4890 let wm = registry.get("wm").unwrap();
4891 let mut ctx = Context::new(BrainWave::Gamma);
4892 let result = wm
4893 .call(
4894 &mut ctx,
4895 json!({"thought": "remember that the API uses X-User-Id headers"}),
4896 )
4897 .await
4898 .unwrap();
4899
4900 assert_eq!(result["status"], "success");
4901 assert_eq!(result["_wm_route"]["tool"], "memory.create");
4902 assert!(result["id"].is_string());
4903 }
4904
4905 #[tokio::test]
4906 async fn wm_explicit_route() {
4907 let store = test_store();
4908 let registry = test_registry_with(&store);
4909 let registry = register_meta_tools(
4910 ®istry,
4911 &store,
4912 std::sync::Arc::new(std::sync::RwLock::new(
4913 embedding_router::ShadowModeStats::default(),
4914 )),
4915 );
4916
4917 let wm = registry.get("wm").unwrap();
4918 let mut ctx = Context::new(BrainWave::Gamma);
4919 let result = wm
4920 .call(
4921 &mut ctx,
4922 json!({
4923 "route": "gnosis"
4924 }),
4925 )
4926 .await
4927 .unwrap();
4928
4929 assert_eq!(result["status"], "success");
4930 assert_eq!(result["_wm_route"]["tool"], "gnosis");
4931 }
4932
4933 #[tokio::test]
4934 async fn wm_no_input_returns_error() {
4935 let store = test_store();
4936 let registry = test_registry_with(&store);
4937 let registry = register_meta_tools(
4938 ®istry,
4939 &store,
4940 std::sync::Arc::new(std::sync::RwLock::new(
4941 embedding_router::ShadowModeStats::default(),
4942 )),
4943 );
4944
4945 let wm = registry.get("wm").unwrap();
4946 let mut ctx = Context::new(BrainWave::Gamma);
4947 let result = wm.call(&mut ctx, json!({})).await.unwrap();
4948
4949 assert_eq!(result["status"], "error");
4950 }
4951
4952 #[tokio::test]
4953 async fn wm_missing_route_echoes_received_keys() {
4954 let store = test_store();
4959 let registry = test_registry_with(&store);
4960 let registry = register_meta_tools(
4961 ®istry,
4962 &store,
4963 std::sync::Arc::new(std::sync::RwLock::new(
4964 embedding_router::ShadowModeStats::default(),
4965 )),
4966 );
4967
4968 let wm = registry.get("wm").unwrap();
4969 let mut ctx = Context::new(BrainWave::Gamma);
4970 let result = wm
4971 .call(
4972 &mut ctx,
4973 json!({"content": "x", "turn_type": "summary", "importance": 0.5}),
4974 )
4975 .await
4976 .unwrap();
4977
4978 assert_eq!(result["status"], "error");
4979 let message = result["message"].as_str().unwrap();
4980 assert!(
4981 message.contains("received argument keys"),
4982 "error must disclose received keys, got: {message}"
4983 );
4984 for key in ["content", "turn_type", "importance"] {
4985 assert!(
4986 message.contains(key),
4987 "error must list received key '{key}', got: {message}"
4988 );
4989 }
4990 let empty = wm.call(&mut ctx, json!({})).await.unwrap();
4992 assert!(
4993 !empty["message"]
4994 .as_str()
4995 .unwrap()
4996 .contains("received argument keys: ["),
4997 "empty input must not list keys, got: {}",
4998 empty["message"]
4999 );
5000 }
5001
5002 #[tokio::test]
5003 async fn wm_unknown_tool_returns_error() {
5004 let store = test_store();
5005 let registry = test_registry_with(&store);
5006 let registry = register_meta_tools(
5007 ®istry,
5008 &store,
5009 std::sync::Arc::new(std::sync::RwLock::new(
5010 embedding_router::ShadowModeStats::default(),
5011 )),
5012 );
5013
5014 let wm = registry.get("wm").unwrap();
5015 let mut ctx = Context::new(BrainWave::Gamma);
5016 let result = wm
5017 .call(&mut ctx, json!({"route": "nonexistent.tool"}))
5018 .await
5019 .unwrap();
5020
5021 assert_eq!(result["status"], "error");
5022 assert!(result["message"].as_str().unwrap().contains("Unknown tool"));
5023 }
5024
5025 #[tokio::test]
5026 async fn memory_query_tags_only_is_allowed() {
5027 let store = test_store();
5031 let mut mem = Memory::new(Galaxy::Codex, "atlas constraint note".into());
5032 mem.metadata.tags = vec!["atlas".into(), "constraint".into()];
5033 store.put(Galaxy::Codex, &mem).unwrap();
5034
5035 let registry = test_registry_with(&store);
5036 let registry = register_meta_tools(
5037 ®istry,
5038 &store,
5039 std::sync::Arc::new(std::sync::RwLock::new(
5040 embedding_router::ShadowModeStats::default(),
5041 )),
5042 );
5043 let wm = registry.get("wm").unwrap();
5044 let mut ctx = Context::new(BrainWave::Gamma);
5045 let result = wm
5046 .call(
5047 &mut ctx,
5048 json!({"route": "memory.query", "args": {"tags": ["atlas", "constraint"]}}),
5049 )
5050 .await
5051 .unwrap();
5052 assert_eq!(result["status"], "success", "{result}");
5053 assert_eq!(result["total"], 1, "{result}");
5054 assert!(
5055 result["memories"][0]
5056 .to_string()
5057 .contains("atlas constraint"),
5058 "{result}"
5059 );
5060 }
5061
5062 #[tokio::test]
5063 async fn memory_search_cold_discovery_is_opt_in_and_verified() {
5064 let store = test_store();
5065 let factors = wm_memory::cold_storage::OuterRimFactors {
5066 age_factor: 0.5,
5067 access_factor: 0.5,
5068 resonance_factor: 0.5,
5069 emotional_factor: 0.5,
5070 importance_factor: 0.5,
5071 distance: 0.5,
5072 };
5073 let mem = Memory::new(
5074 Galaxy::Codex,
5075 "cold original zxquniquehotcold999 deep".into(),
5076 );
5077 let rec = wm_memory::cold_storage::ColdRecord::new(
5078 &mem,
5079 0.5,
5080 factors,
5081 None,
5082 None,
5083 wm_memory::cold_storage::CompressionCodec::Gzip,
5084 )
5085 .unwrap();
5086 store.put_cold_record(&rec).unwrap();
5087
5088 let registry = test_registry_with(&store);
5089 let _ = ®istry;
5092 let search = expansion::MemoryHybridRecallTool::as_search(store.clone(), None, None);
5093 let mut ctx = Context::new(BrainWave::Gamma);
5094
5095 let without = search
5097 .call(
5098 &mut ctx,
5099 json!({"query": "zxquniquehotcold999", "limit": 5}),
5100 )
5101 .await
5102 .unwrap();
5103 assert_eq!(without["count"], 0, "{without}");
5104
5105 let with = search
5107 .call(
5108 &mut ctx,
5109 json!({"query": "zxquniquehotcold999", "limit": 5, "include_cold": true}),
5110 )
5111 .await
5112 .unwrap();
5113 assert_eq!(with["cold_discovery"]["no_thaw"], true, "{with}");
5114 assert!(
5115 with["results"]
5116 .as_array()
5117 .unwrap()
5118 .iter()
5119 .any(|r| r["source"] == "cold" && r["integrity"] == "verified"),
5120 "{with}"
5121 );
5122 }
5123
5124 #[tokio::test]
5125 async fn wm_missing_arg_returns_hint() {
5126 let store = test_store();
5127 let registry = test_registry_with(&store);
5128 let registry = register_meta_tools(
5129 ®istry,
5130 &store,
5131 std::sync::Arc::new(std::sync::RwLock::new(
5132 embedding_router::ShadowModeStats::default(),
5133 )),
5134 );
5135
5136 let wm = registry.get("wm").unwrap();
5137 let mut ctx = Context::new(BrainWave::Gamma);
5138
5139 let result = wm
5141 .call(&mut ctx, json!({"route": "memory.read"}))
5142 .await
5143 .unwrap();
5144
5145 assert_eq!(result["status"], "error");
5146 assert!(
5147 result["message"]
5148 .as_str()
5149 .unwrap()
5150 .contains("Missing required argument")
5151 );
5152 assert!(result["hint"].as_str().unwrap().contains("uuid"));
5153 }
5154
5155 #[test]
5156 fn search_payload_extracts_curated_intents() {
5157 let cases = [
5158 (
5159 "find BETA quartz submarine in memory",
5160 "BETA quartz submarine",
5161 ),
5162 (
5163 "What do you remember about BETA quartz submarine?",
5164 "BETA quartz submarine",
5165 ),
5166 (
5167 "What did we decide about BETA quartz submarine?",
5168 "BETA quartz submarine",
5169 ),
5170 ("recall BETA quartz submarine", "BETA quartz submarine"),
5171 ("look up BETA quartz submarine", "BETA quartz submarine"),
5172 ("search for rust", "rust"),
5173 ("search memory for rust", "rust"),
5174 ];
5175 for (thought, expected) in cases {
5176 let got = WmMetaTool::extract_payload(thought, "memory.search");
5177 assert_eq!(
5178 got,
5179 Some(("query".to_string(), expected.to_string())),
5180 "for {thought:?}"
5181 );
5182 }
5183 }
5184
5185 #[tokio::test]
5186 async fn wm_auto_route_missing_arg_returns_hint() {
5187 let store = test_store();
5188 let registry = test_registry_with(&store);
5189 let registry = register_meta_tools(
5190 ®istry,
5191 &store,
5192 std::sync::Arc::new(std::sync::RwLock::new(
5193 embedding_router::ShadowModeStats::default(),
5194 )),
5195 );
5196
5197 let wm = registry.get("wm").unwrap();
5198 let mut ctx = Context::new(BrainWave::Gamma);
5199
5200 let result = wm
5205 .call(&mut ctx, json!({"thought": "fetch memory"}))
5206 .await
5207 .unwrap();
5208
5209 assert_eq!(result["status"], "error");
5210 assert!(
5211 result["hint"].as_str().is_some_and(|h| h.contains("uuid")),
5212 "expected a read hint, got {result}"
5213 );
5214 }
5215
5216 #[tokio::test]
5217 async fn wm_routes_karma_to_karma_report() {
5218 let tmp = tempfile::tempdir().unwrap();
5219 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
5220 let ledger = Arc::new(KarmaLedger::new(store.clone()).unwrap());
5221 let gate = Arc::new(DharmaGate::default());
5222
5223 let registry = ToolRegistry::new();
5224 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
5225 let spiral_tracker =
5226 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
5227 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
5228 let registry = register_all(
5229 ®istry,
5230 &store,
5231 None,
5232 Some(ledger),
5233 &Some(gate),
5234 None,
5235 &None,
5236 associations,
5237 spiral_tracker,
5238 vector_store,
5239 None,
5240 None,
5241 None,
5242 None,
5243 None,
5244 None,
5245 None,
5246 std::sync::Arc::new(std::sync::Mutex::new(None)),
5247 None,
5248 None,
5249 None,
5250 expansion::RegistryPersistenceMode::Normal,
5251 Arc::new(wm_dispatch::CircuitBreakerRegistry::default()),
5252 );
5253 let registry = register_meta_tools(
5254 ®istry,
5255 &store,
5256 std::sync::Arc::new(std::sync::RwLock::new(
5257 embedding_router::ShadowModeStats::default(),
5258 )),
5259 );
5260
5261 let wm = registry.get("wm").unwrap();
5262 let mut ctx = Context::new(BrainWave::Gamma);
5263 let result = wm
5264 .call(&mut ctx, json!({"thought": "show me the karma report"}))
5265 .await
5266 .unwrap();
5267
5268 assert_eq!(result["status"], "success");
5269 assert_eq!(result["_wm_route"]["tool"], "karma.report");
5270 }
5271
5272 fn test_registry_with_pipeline(
5275 store: &Arc<MemoryStore>,
5276 ) -> (ToolRegistry, Arc<DispatchPipeline>) {
5277 let registry = test_registry_with(store);
5278 let pipeline = Arc::new(DispatchPipeline::with_defaults());
5279 let (registry, _router) = register_meta_tools_with_router(
5280 ®istry,
5281 store,
5282 std::sync::Arc::new(std::sync::RwLock::new(
5283 embedding_router::ShadowModeStats::default(),
5284 )),
5285 Some(pipeline.clone()),
5286 );
5287 (registry, pipeline)
5288 }
5289
5290 #[tokio::test]
5291 async fn wm_route_destructive_without_confirm_blocked_by_pipeline() {
5292 let store = test_store();
5293 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5294
5295 let wm = registry.get("wm").unwrap();
5296 let mut ctx = Context::new(BrainWave::Gamma);
5297 let result = wm
5298 .call(
5299 &mut ctx,
5300 json!({"route": "memory.delete", "args": {"id": "00000000-0000-0000-0000-000000000001"}}),
5301 )
5302 .await
5303 .unwrap();
5304
5305 assert_eq!(result["status"], "error");
5306 assert!(
5307 result["error"].as_str().unwrap().contains("destructive"),
5308 "expected destructive-gate message, got: {result}"
5309 );
5310 assert!(result["error"].as_str().unwrap().contains("confirm"));
5311 }
5312
5313 #[tokio::test]
5314 async fn wm_route_destructive_with_confirm_proceeds() {
5315 let store = test_store();
5316 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5317
5318 let memory = Memory::new(Galaxy::Codex, "delete me via wm route".into());
5320 let id = memory.metadata.id;
5321 store.put(Galaxy::Codex, &memory).unwrap();
5322
5323 let wm = registry.get("wm").unwrap();
5324 let mut ctx = Context::new(BrainWave::Gamma);
5325 let result = wm
5326 .call(
5327 &mut ctx,
5328 json!({"route": "memory.delete", "args": {"id": id.to_string(), "galaxy": "codex", "confirm": true}}),
5329 )
5330 .await
5331 .unwrap();
5332
5333 assert_eq!(result["status"], "success");
5334 assert_eq!(result["_wm_route"]["tool"], "memory.delete");
5335 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
5336 }
5337
5338 #[tokio::test]
5339 async fn wm_thought_cannot_reach_destructive_tool() {
5340 let store = test_store();
5341 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5342
5343 let wm = registry.get("wm").unwrap();
5344 let mut ctx = Context::new(BrainWave::Gamma);
5345 let result = wm
5348 .call(
5349 &mut ctx,
5350 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001"}),
5351 )
5352 .await
5353 .unwrap();
5354
5355 assert_eq!(result["status"], "error");
5356 assert!(
5357 result["message"]
5358 .as_str()
5359 .unwrap()
5360 .contains("cannot be reached via natural language"),
5361 "expected NLU hard-block message, got: {result}"
5362 );
5363 }
5364
5365 #[tokio::test]
5366 async fn wm_thought_cannot_reach_destructive_tool_even_with_confirm() {
5367 let store = test_store();
5368 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5369
5370 let wm = registry.get("wm").unwrap();
5371 let mut ctx = Context::new(BrainWave::Gamma);
5372 let result = wm
5376 .call(
5377 &mut ctx,
5378 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001", "args": {"confirm": true, "id": "00000000-0000-0000-0000-000000000001"}}),
5379 )
5380 .await
5381 .unwrap();
5382
5383 assert_eq!(result["status"], "error");
5384 assert!(
5385 result["message"]
5386 .as_str()
5387 .unwrap()
5388 .contains("cannot be reached via natural language")
5389 );
5390 }
5391
5392 #[tokio::test]
5397 async fn nlu_cannot_reach_any_destructive_tool() {
5398 let store = test_store();
5399 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5400 let wm = registry.get("wm").unwrap();
5401
5402 let destructive_tools: Vec<String> = registry
5405 .all_ref()
5406 .iter()
5407 .filter(|t| t.effects().destructive)
5408 .map(|t| t.name().to_string())
5409 .collect();
5410
5411 assert!(
5412 !destructive_tools.is_empty(),
5413 "registry must contain at least one destructive tool for this test to be meaningful"
5414 );
5415
5416 let mut ctx = Context::new(BrainWave::Gamma);
5417 for tool_name in &destructive_tools {
5418 let result = wm
5422 .call(
5423 &mut ctx,
5424 json!({
5425 "thought": tool_name,
5426 "args": {"confirm": true}
5427 }),
5428 )
5429 .await
5430 .unwrap();
5431
5432 let routed_tool = result
5437 .get("_wm_route")
5438 .and_then(|r| r.get("tool"))
5439 .and_then(|t| t.as_str())
5440 .unwrap_or("");
5441 let resolved_destructive = registry
5442 .get(routed_tool)
5443 .is_some_and(|t| t.effects().destructive);
5444 assert!(
5445 result["status"] != "success" || !resolved_destructive,
5446 "destructive tool '{tool_name}' executed via NLU (resolved as '{routed_tool}') — structural gate failed"
5447 );
5448
5449 if routed_tool == tool_name {
5452 assert!(
5453 result
5454 .get("message")
5455 .and_then(|m| m.as_str())
5456 .is_some_and(|m| m.contains("cannot be reached via natural language")),
5457 "destructive tool '{tool_name}' was routed to but gate message missing: {result}"
5458 );
5459 }
5460
5461 let nl_phrase = match tool_name.as_str() {
5466 "memory.delete" => "delete memory 00000000-0000-0000-0000-000000000001",
5467 "transaction.rollback" => "rollback the transaction",
5468 "galaxy.purge" => "purge galaxy codex",
5469 "galaxy.transfer" => "transfer galaxy codex to archive",
5470 "galaxy.restore" => "restore galaxy codex from snapshot",
5471 "memory.consolidate" => "consolidate memories in codex",
5472 "memory.deduplicate" => "deduplicate memories in codex",
5473 "karma.purge" => "purge karma ledger",
5474 "system.flush" => "flush low importance memories",
5475 "galaxy.cold_rotate" => "rotate telemetry noise to cold storage",
5476 _ => tool_name.as_str(),
5477 };
5478 let result2 = wm
5479 .call(&mut ctx, json!({"thought": nl_phrase}))
5480 .await
5481 .unwrap();
5482
5483 let routed_tool2 = result2
5484 .get("_wm_route")
5485 .and_then(|r| r.get("tool"))
5486 .and_then(|t| t.as_str())
5487 .unwrap_or("");
5488 let resolved_destructive2 = registry
5489 .get(routed_tool2)
5490 .is_some_and(|t| t.effects().destructive);
5491 assert!(
5492 result2["status"] != "success" || !resolved_destructive2,
5493 "destructive tool '{tool_name}' executed via NLU phrase '{nl_phrase}' (resolved as '{routed_tool2}') — structural gate failed"
5494 );
5495 if routed_tool2 == tool_name {
5496 assert!(
5497 result2
5498 .get("message")
5499 .and_then(|m| m.as_str())
5500 .is_some_and(|m| m.contains("cannot be reached via natural language")),
5501 "destructive tool '{tool_name}' was routed to via '{nl_phrase}' but gate message missing: {result2}"
5502 );
5503 }
5504 }
5505 }
5506
5507 #[tokio::test]
5508 async fn nlu_abstention_returns_error_for_unmatched_query() {
5509 let store = test_store();
5510 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5511 let wm = registry.get("wm").unwrap();
5512 let mut ctx = Context::new(BrainWave::Gamma);
5513
5514 let result = wm
5517 .call(&mut ctx, json!({"thought": "xyzzy quux blargh frobnicate"}))
5518 .await
5519 .unwrap();
5520
5521 assert_eq!(result["status"], "error");
5522 assert!(
5523 result
5524 .get("_wm_route")
5525 .and_then(|r| r.get("abstained"))
5526 .and_then(serde_json::Value::as_bool)
5527 .unwrap_or(false),
5528 "expected abstained=true, got: {result}"
5529 );
5530 assert!(
5531 result["message"]
5532 .as_str()
5533 .unwrap()
5534 .contains("Could not confidently match"),
5535 "expected abstention message, got: {result}"
5536 );
5537 }
5538
5539 #[tokio::test]
5540 async fn nlu_abstention_does_not_fire_for_explicit_route() {
5541 let store = test_store();
5542 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5543 let wm = registry.get("wm").unwrap();
5544 let mut ctx = Context::new(BrainWave::Gamma);
5545
5546 let result = wm.call(&mut ctx, json!({"route": "gnosis"})).await.unwrap();
5549
5550 assert_eq!(result["status"], "success");
5551 assert!(
5552 !result
5553 .get("_wm_route")
5554 .and_then(|r| r.get("abstained"))
5555 .and_then(serde_json::Value::as_bool)
5556 .unwrap_or(false),
5557 "explicit route should not abstain, got: {result}"
5558 );
5559 }
5560
5561 struct FakeVecEmbedder;
5564
5565 impl wm_memory::Embedder for FakeVecEmbedder {
5566 fn embed_batch(&self, texts: &[&str]) -> wm_core::Result<Vec<Vec<f32>>> {
5567 Ok(texts
5568 .iter()
5569 .map(|t| {
5570 let mut v = vec![0.0_f32; 16];
5571 for (i, b) in t.bytes().take(16).enumerate() {
5572 v[i] = f32::from(b) / 255.0;
5573 }
5574 v
5575 })
5576 .collect())
5577 }
5578 fn dimension(&self) -> usize {
5579 16
5580 }
5581 fn is_available(&self) -> bool {
5582 true
5583 }
5584 fn backend_name(&self) -> &'static str {
5585 "fake"
5586 }
5587 }
5588
5589 #[tokio::test]
5590 async fn wm_classify_async_routes_off_thread_with_embedding_router() {
5591 let store = test_store();
5592 let registry = test_registry_with(&store);
5593 let shadow = std::sync::Arc::new(std::sync::RwLock::new(
5594 embedding_router::ShadowModeStats::default(),
5595 ));
5596 let router = embedding_router::EmbeddingRouter::with_descriptions(
5597 Box::new(FakeVecEmbedder),
5598 embedding_router::tool_descriptions(),
5599 )
5600 .expect("fake-embedder router should build");
5601 let mut meta = WmMetaTool::with_router_shadow_stats_and_pipeline(
5602 std::sync::Arc::new(registry),
5603 wm_memory::create_embedder(),
5604 shadow,
5605 None,
5606 );
5607 meta.embedding_router = Some(std::sync::Arc::new(router));
5608
5609 let (tool, conf, emb) = meta.classify_async("remember the meeting notes").await;
5612 assert!(!tool.is_empty());
5613 assert!(conf >= 0.0);
5614 assert!(
5615 emb.is_some(),
5616 "query embedding should be returned for OATS reuse"
5617 );
5618 }
5619
5620 #[tokio::test]
5621 async fn tools_list_shows_all() {
5622 let store = test_store();
5623 let registry = test_registry_with(&store);
5624 let registry = register_meta_tools(
5625 ®istry,
5626 &store,
5627 std::sync::Arc::new(std::sync::RwLock::new(
5628 embedding_router::ShadowModeStats::default(),
5629 )),
5630 );
5631
5632 let list = registry.get("tools.list").unwrap();
5633 let mut ctx = Context::new(BrainWave::Gamma);
5634 let result = list.call(&mut ctx, json!({})).await.unwrap();
5635
5636 assert_eq!(result["status"], "success");
5637 assert!(result["total"].as_u64().unwrap() >= 7);
5638 }
5639
5640 #[tokio::test]
5641 async fn tools_list_exposes_curated_argument_schemas() {
5642 let store = test_store();
5643 let registry = test_registry_with(&store);
5644 let registry = register_meta_tools(
5645 ®istry,
5646 &store,
5647 std::sync::Arc::new(std::sync::RwLock::new(
5648 embedding_router::ShadowModeStats::default(),
5649 )),
5650 );
5651
5652 let list = registry.get("tools.list").unwrap();
5653 let mut ctx = Context::new(BrainWave::Gamma);
5654 let result = list.call(&mut ctx, json!({})).await.unwrap();
5655
5656 let tools = result["tools"].as_array().unwrap();
5657 let create = tools
5658 .iter()
5659 .find(|t| t["name"] == "memory.create")
5660 .expect("tools.list must include memory.create");
5661 let schema = &create["input_schema"];
5662 assert_eq!(schema["type"], "object");
5663 assert!(
5664 schema["properties"].get("content").is_some(),
5665 "memory.create schema must describe content, got: {schema}"
5666 );
5667 assert!(
5668 schema["required"]
5669 .as_array()
5670 .unwrap()
5671 .iter()
5672 .any(|r| r == "content"),
5673 "memory.create schema must require content"
5674 );
5675
5676 let rollback = tools
5677 .iter()
5678 .find(|t| t["name"] == "transaction.rollback")
5679 .expect("tools.list must include transaction.rollback");
5680 assert!(
5681 rollback["input_schema"]["required"]
5682 .as_array()
5683 .unwrap()
5684 .iter()
5685 .any(|r| r == "confirm"),
5686 "transaction.rollback schema must require confirm"
5687 );
5688
5689 let annotations = &create["annotations"];
5691 assert_eq!(annotations["readOnlyHint"], false, "memory.create writes");
5692 assert_eq!(annotations["destructiveHint"], false);
5693 assert_eq!(
5694 rollback["annotations"]["destructiveHint"], true,
5695 "transaction.rollback is destructive"
5696 );
5697 let list_tool = tools
5698 .iter()
5699 .find(|t| t["name"] == "memory.list")
5700 .expect("tools.list must include memory.list");
5701 assert_eq!(
5702 list_tool["annotations"]["readOnlyHint"], true,
5703 "memory.list is read-only"
5704 );
5705 }
5706
5707 #[tokio::test]
5708 async fn tools_list_filters_by_brain_wave() {
5709 let store = test_store();
5710 let registry = test_registry_with(&store);
5711 let registry = register_meta_tools(
5712 ®istry,
5713 &store,
5714 std::sync::Arc::new(std::sync::RwLock::new(
5715 embedding_router::ShadowModeStats::default(),
5716 )),
5717 );
5718
5719 let list = registry.get("tools.list").unwrap();
5720
5721 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5723 let result_gamma = list.call(&mut ctx_gamma, json!({})).await.unwrap();
5724 let gamma_count = result_gamma["total"].as_u64().unwrap();
5725 assert!(gamma_count >= 7);
5726
5727 let mut ctx_alpha = Context::new(BrainWave::Alpha);
5729 let result_alpha = list.call(&mut ctx_alpha, json!({})).await.unwrap();
5730 let alpha_count = result_alpha["total"].as_u64().unwrap();
5731 assert!(alpha_count < gamma_count);
5732 assert!(alpha_count > 0);
5733
5734 let mut ctx_delta = Context::new(BrainWave::Delta);
5736 let result_delta = list.call(&mut ctx_delta, json!({})).await.unwrap();
5737 assert_eq!(result_delta["total"], 0);
5738 }
5739
5740 #[tokio::test]
5741 async fn gnosis_includes_brain_wave_and_tool_count() {
5742 let store = test_store();
5743 let registry = test_registry_with(&store);
5744 let registry = register_meta_tools(
5745 ®istry,
5746 &store,
5747 std::sync::Arc::new(std::sync::RwLock::new(
5748 embedding_router::ShadowModeStats::default(),
5749 )),
5750 );
5751
5752 let gnosis = registry.get("gnosis").unwrap();
5753 let mut ctx = Context::new(BrainWave::Gamma);
5754 let result = gnosis.call(&mut ctx, json!({})).await.unwrap();
5755
5756 assert_eq!(result["status"], "success");
5757 assert_eq!(result["brain_wave"], "Gamma");
5758 assert!(result["available_tools"].as_u64().unwrap() >= 9);
5759 }
5760
5761 #[tokio::test]
5762 async fn gnosis_available_tools_is_total_registered() {
5763 let store = test_store();
5764 let registry = test_registry_with(&store);
5765 let registry = register_meta_tools(
5766 ®istry,
5767 &store,
5768 std::sync::Arc::new(std::sync::RwLock::new(
5769 embedding_router::ShadowModeStats::default(),
5770 )),
5771 );
5772
5773 let gnosis = registry.get("gnosis").unwrap();
5774
5775 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5778 let result_gamma = gnosis.call(&mut ctx_gamma, json!({})).await.unwrap();
5779 let gamma_tools = result_gamma["available_tools"].as_u64().unwrap();
5780
5781 let mut ctx_delta = Context::new(BrainWave::Delta);
5782 let result_delta = gnosis.call(&mut ctx_delta, json!({})).await.unwrap();
5783 let delta_tools = result_delta["available_tools"].as_u64().unwrap();
5784
5785 assert_eq!(gamma_tools, delta_tools);
5786 assert!(
5787 gamma_tools >= 9,
5788 "expected at least 9 registered tools, got {gamma_tools}"
5789 );
5790 }
5791
5792 #[tokio::test]
5793 async fn expansion_brings_tool_count_to_50() {
5794 let store = test_store();
5795 let registry = test_registry_with(&store);
5796 let registry = register_meta_tools(
5797 ®istry,
5798 &store,
5799 std::sync::Arc::new(std::sync::RwLock::new(
5800 embedding_router::ShadowModeStats::default(),
5801 )),
5802 );
5803
5804 let list = registry.get("tools.list").unwrap();
5805 let mut ctx = Context::new(BrainWave::Gamma);
5806 let result = list.call(&mut ctx, json!({})).await.unwrap();
5807
5808 let total = result["total"].as_u64().unwrap();
5809 assert!(
5810 total >= 50,
5811 "Expected 50+ tools after expansion, got {total}"
5812 );
5813 }
5814
5815 #[tokio::test]
5818 async fn nlu_routes_consolidate() {
5819 let (tool, conf) = WmMetaTool::classify("consolidate memories in codex");
5820 assert_eq!(tool, "memory.consolidate");
5821 assert!(conf > 0.0);
5822 }
5823
5824 #[tokio::test]
5825 async fn nlu_routes_decay() {
5826 let (tool, conf) = WmMetaTool::classify("decay old memories");
5827 assert_eq!(tool, "memory.decay");
5828 assert!(conf > 0.0);
5829 }
5830
5831 #[tokio::test]
5832 async fn nlu_routes_batch_read() {
5833 let (tool, conf) = WmMetaTool::classify("batch read these memories");
5834 assert_eq!(tool, "memory.batch_read");
5835 assert!(conf > 0.0);
5836 }
5837
5838 #[tokio::test]
5839 async fn nlu_routes_update() {
5840 let (tool, conf) = WmMetaTool::classify("update memory tags");
5841 assert_eq!(tool, "memory.update");
5842 assert!(conf > 0.0);
5843 }
5844
5845 #[tokio::test]
5846 async fn nlu_routes_tag() {
5847 let (tool, conf) = WmMetaTool::classify("add tag to memory");
5848 assert_eq!(tool, "memory.tag");
5849 assert!(conf > 0.0);
5850 }
5851
5852 #[tokio::test]
5853 async fn nlu_routes_memory_stats() {
5854 let (tool, conf) = WmMetaTool::classify("memory stats for codex");
5855 assert_eq!(tool, "memory.stats");
5856 assert!(conf > 0.0);
5857 }
5858
5859 #[tokio::test]
5860 async fn nlu_routes_hybrid_recall() {
5861 let (tool, conf) = WmMetaTool::classify("hybrid recall for rust");
5862 assert_eq!(tool, "memory.hybrid_recall");
5863 assert!(conf > 0.0);
5864 }
5865
5866 #[tokio::test]
5867 async fn nlu_routes_count() {
5868 let (tool, conf) = WmMetaTool::classify("count memories in codex");
5869 assert_eq!(tool, "memory.count");
5870 assert!(conf > 0.0);
5871 }
5872
5873 #[tokio::test]
5874 async fn nlu_routes_tags() {
5875 let (tool, conf) = WmMetaTool::classify("list tags in codex");
5876 assert_eq!(tool, "memory.tags");
5877 assert!(conf > 0.0);
5878 }
5879
5880 #[tokio::test]
5881 async fn nlu_routes_associate_mine() {
5882 let (tool, conf) = WmMetaTool::classify("mine associations in codex");
5883 assert_eq!(tool, "memory.associate_mine");
5884 assert!(conf > 0.0);
5885 }
5886
5887 #[tokio::test]
5888 async fn nlu_routes_session_start() {
5889 let (tool, conf) = WmMetaTool::classify("start session research");
5890 assert_eq!(tool, "session.start");
5891 assert!(conf > 0.0);
5892 }
5893
5894 #[tokio::test]
5895 async fn nlu_routes_session_end() {
5896 let (tool, conf) = WmMetaTool::classify("end session 12345");
5897 assert_eq!(tool, "session.end");
5898 assert!(conf > 0.0);
5899 }
5900
5901 #[tokio::test]
5902 async fn nlu_routes_session_list() {
5903 let (tool, conf) = WmMetaTool::classify("list sessions");
5904 assert_eq!(tool, "session.list");
5905 assert!(conf > 0.0);
5906 }
5907
5908 #[tokio::test]
5909 async fn nlu_routes_citta_status() {
5910 let (tool, conf) = WmMetaTool::classify("citta status");
5911 assert_eq!(tool, "citta.status");
5912 assert!(conf > 0.0);
5913 }
5914
5915 #[tokio::test]
5916 async fn nlu_routes_citta_reflect() {
5917 let (tool, conf) = WmMetaTool::classify("reflect on recent events");
5918 assert_eq!(tool, "citta.reflect");
5919 assert!(conf > 0.0);
5920 }
5921
5922 #[tokio::test]
5923 async fn nlu_routes_coherence() {
5924 let (tool, conf) = WmMetaTool::classify("check coherence");
5925 assert_eq!(tool, "citta.coherence");
5926 assert!(conf > 0.0);
5927 }
5928
5929 #[tokio::test]
5930 async fn nlu_routes_dream_status() {
5931 let (tool, conf) = WmMetaTool::classify("dream cycle status");
5932 assert_eq!(tool, "dream.status");
5933 assert!(conf > 0.0);
5934 }
5935
5936 #[tokio::test]
5937 async fn nlu_routes_dream_trigger() {
5938 let (tool, conf) = WmMetaTool::classify("trigger dream cycle");
5939 assert_eq!(tool, "dream.trigger");
5940 assert!(conf > 0.0);
5941 }
5942
5943 #[tokio::test]
5944 async fn nlu_routes_effectiveness() {
5945 let (tool, conf) = WmMetaTool::classify("tool effectiveness report");
5946 assert_eq!(tool, "tools.effectiveness_report");
5947 assert!(conf > 0.0);
5948 }
5949
5950 #[tokio::test]
5951 async fn nlu_routes_retire() {
5952 let (tool, conf) = WmMetaTool::classify("retire tool memory.old");
5953 assert_eq!(tool, "tools.retire");
5954 assert!(conf > 0.0);
5955 }
5956
5957 #[tokio::test]
5958 async fn nlu_routes_pattern_search() {
5959 let (tool, conf) = WmMetaTool::classify("pattern search for rust");
5960 assert_eq!(tool, "pattern.search");
5961 assert!(conf > 0.0);
5962 }
5963
5964 #[tokio::test]
5965 async fn nlu_routes_salience() {
5966 let (tool, conf) = WmMetaTool::classify("salience spotlight");
5967 assert_eq!(tool, "salience.spotlight");
5968 assert!(conf > 0.0);
5969 }
5970
5971 #[tokio::test]
5972 async fn nlu_routes_serendipity() {
5973 let (tool, conf) = WmMetaTool::classify("serendipity surface");
5974 assert_eq!(tool, "serendipity.surface");
5975 assert!(conf > 0.0);
5976 }
5977
5978 #[tokio::test]
5979 async fn nlu_routes_constellation_detect() {
5980 let (tool, conf) = WmMetaTool::classify("detect clusters");
5981 assert_eq!(tool, "constellation.detect");
5982 assert!(conf > 0.0);
5983 }
5984
5985 #[tokio::test]
5986 async fn nlu_routes_constellation_list() {
5987 let (tool, conf) = WmMetaTool::classify("list constellations");
5988 assert_eq!(tool, "constellation.list");
5989 assert!(conf > 0.0);
5990 }
5991
5992 #[tokio::test]
5993 async fn nlu_routes_galaxy_stats() {
5994 let (tool, conf) = WmMetaTool::classify("galaxy stats");
5995 assert_eq!(tool, "galaxy.stats");
5996 assert!(conf > 0.0);
5997 }
5998
5999 #[tokio::test]
6000 async fn nlu_routes_galaxy_export() {
6001 let (tool, conf) = WmMetaTool::classify("export galaxy codex");
6002 assert_eq!(tool, "galaxy.export");
6003 assert!(conf > 0.0);
6004 }
6005
6006 #[tokio::test]
6007 async fn nlu_routes_galaxy_import() {
6008 let (tool, conf) = WmMetaTool::classify("import galaxy codex");
6009 assert_eq!(tool, "galaxy.import");
6010 assert!(conf > 0.0);
6011 }
6012
6013 #[tokio::test]
6014 async fn nlu_routes_karma_history() {
6015 let (tool, conf) = WmMetaTool::classify("karma history");
6016 assert_eq!(tool, "karma.history");
6017 assert!(conf > 0.0);
6018 }
6019
6020 #[tokio::test]
6021 async fn nlu_routes_karma_clear() {
6022 let (tool, conf) = WmMetaTool::classify("clear karma");
6023 assert_eq!(tool, "karma.clear");
6024 assert!(conf > 0.0);
6025 }
6026
6027 #[tokio::test]
6028 async fn nlu_routes_dharma_rules() {
6029 let (tool, conf) = WmMetaTool::classify("dharma rules");
6030 assert_eq!(tool, "dharma.rules");
6031 assert!(conf > 0.0);
6032 }
6033
6034 #[tokio::test]
6035 async fn nlu_routes_dharma_audit() {
6036 let (tool, conf) = WmMetaTool::classify("dharma audit");
6037 assert_eq!(tool, "dharma.audit");
6038 assert!(conf > 0.0);
6039 }
6040
6041 #[tokio::test]
6042 async fn nlu_routes_dharma_profiles() {
6043 let (tool, conf) = WmMetaTool::classify("dharma profiles");
6044 assert_eq!(tool, "dharma.profiles");
6045 assert!(conf > 0.0);
6046 }
6047
6048 #[tokio::test]
6049 async fn nlu_routes_agent_register() {
6050 let (tool, conf) = WmMetaTool::classify("register agent worker-1");
6051 assert_eq!(tool, "agent.register");
6052 assert!(conf > 0.0);
6053 }
6054
6055 #[tokio::test]
6056 async fn nlu_routes_agent_list() {
6057 let (tool, conf) = WmMetaTool::classify("list agents");
6058 assert_eq!(tool, "agent.list");
6059 assert!(conf > 0.0);
6060 }
6061
6062 #[tokio::test]
6063 async fn nlu_routes_agent_heartbeat() {
6064 let (tool, conf) = WmMetaTool::classify("heartbeat for agent");
6065 assert_eq!(tool, "agent.heartbeat");
6066 assert!(conf > 0.0);
6067 }
6068
6069 #[tokio::test]
6070 async fn nlu_routes_task_distribute() {
6071 let (tool, conf) = WmMetaTool::classify("distribute task analyze data");
6072 assert_eq!(tool, "task.distribute");
6073 assert!(conf > 0.0);
6074 }
6075
6076 #[tokio::test]
6077 async fn nlu_routes_task_status() {
6078 let (tool, conf) = WmMetaTool::classify("task status");
6079 assert_eq!(tool, "task.status");
6080 assert!(conf > 0.0);
6081 }
6082
6083 #[tokio::test]
6084 async fn nlu_routes_system_health() {
6085 let (tool, conf) = WmMetaTool::classify("system health check");
6086 assert_eq!(tool, "system.health");
6087 assert!(conf > 0.0);
6088 }
6089
6090 #[tokio::test]
6091 async fn nlu_routes_system_config() {
6092 let (tool, conf) = WmMetaTool::classify("system config");
6093 assert_eq!(tool, "system.config");
6094 assert!(conf > 0.0);
6095 }
6096
6097 #[tokio::test]
6098 async fn nlu_routes_system_flush() {
6099 let (tool, conf) = WmMetaTool::classify("flush old memories");
6100 assert_eq!(tool, "system.flush");
6101 assert!(conf > 0.0);
6102 }
6103
6104 #[tokio::test]
6105 async fn nlu_routes_memory_nearby() {
6106 let (tool, conf) = WmMetaTool::classify("nearby memories in codex");
6107 assert_eq!(tool, "memory.nearby");
6108 assert!(conf > 0.0);
6109 }
6110
6111 #[tokio::test]
6112 async fn nlu_routes_empty_to_gnosis() {
6113 let (tool, conf) = WmMetaTool::classify("");
6114 assert_eq!(tool, "gnosis");
6115 assert_eq!(conf, 0.0);
6116 }
6117
6118 #[tokio::test]
6119 async fn nlu_routes_unknown_to_gnosis() {
6120 let (tool, conf) = WmMetaTool::classify("xyzzy frobnicate");
6121 assert_eq!(tool, "gnosis");
6122 assert_eq!(conf, 0.0);
6123 }
6124
6125 #[tokio::test]
6126 async fn nlu_extract_payload_memory_search() {
6127 let (param, value) =
6128 WmMetaTool::extract_payload("search for rust patterns", "memory.search").unwrap();
6129 assert_eq!(param, "query");
6130 assert_eq!(value, "rust patterns");
6131 }
6132
6133 #[tokio::test]
6134 async fn nlu_extract_payload_session_start() {
6135 let (param, value) =
6139 WmMetaTool::extract_payload("start session research", "session.start").unwrap();
6140 assert_eq!(param, "title");
6141 assert_eq!(value, "research");
6142 }
6143
6144 #[tokio::test]
6145 async fn nlu_extract_payload_agent_register() {
6146 let (param, value) =
6147 WmMetaTool::extract_payload("register agent worker-1", "agent.register").unwrap();
6148 assert_eq!(param, "name");
6149 assert_eq!(value, "worker-1");
6150 }
6151
6152 #[tokio::test]
6153 async fn nlu_extract_payload_task_distribute() {
6154 let (param, value) =
6155 WmMetaTool::extract_payload("distribute task analyze data", "task.distribute").unwrap();
6156 assert_eq!(param, "task");
6157 assert_eq!(value, "analyze data");
6158 }
6159
6160 #[tokio::test]
6161 async fn nlu_count_unique_patterns() {
6162 let inputs = [
6164 "remember",
6165 "recall",
6166 "list memories",
6167 "delete memory",
6168 "search",
6169 "query",
6170 "associate",
6171 "associations",
6172 "consolidate",
6173 "decay",
6174 "batch read",
6175 "update memory",
6176 "tag memory",
6177 "memory stats",
6178 "hybrid recall",
6179 "count memories",
6180 "list tags",
6181 "mine associations",
6182 "start session",
6183 "checkpoint",
6184 "recall session",
6185 "end session",
6186 "list sessions",
6187 "citta status",
6188 "reflect",
6189 "coherence",
6190 "dream status",
6191 "trigger dream",
6192 "effectiveness",
6193 "retire tool",
6194 "pattern search",
6195 "salience",
6196 "serendipity",
6197 "detect clusters",
6198 "list constellations",
6199 "galaxy stats",
6200 "export galaxy",
6201 "import galaxy",
6202 "karma",
6203 "karma history",
6204 "clear karma",
6205 "dharma rules",
6206 "dharma audit",
6207 "dharma profiles",
6208 "dharma",
6209 "register agent",
6210 "list agents",
6211 "heartbeat",
6212 "distribute task",
6213 "task status",
6214 "system health",
6215 "system config",
6216 "flush",
6217 "tools",
6218 "nearby memories",
6219 ];
6220 let mut tools: std::collections::HashSet<&str> = std::collections::HashSet::new();
6221 for input in &inputs {
6222 let (tool, _) = WmMetaTool::classify(input);
6223 tools.insert(tool);
6224 }
6225 assert!(
6227 tools.len() >= 30,
6228 "Expected 30+ unique NLU targets, got {}",
6229 tools.len()
6230 );
6231 }
6232
6233 #[tokio::test]
6234 async fn nlu_routes_shadow_report() {
6235 let (tool, conf) = WmMetaTool::classify("shadow mode disagreement report");
6236 assert_eq!(tool, "nlu.shadow_report");
6237 assert!(conf > 0.0);
6238 }
6239
6240 #[tokio::test]
6241 async fn nlu_routes_oats_report() {
6242 let (tool, conf) = WmMetaTool::classify("oats disagreement nlu router");
6243 assert_eq!(tool, "nlu.shadow_report");
6244 assert!(conf > 0.0);
6245 }
6246
6247 #[test]
6250 fn glyph_roundtrip_known_codes() {
6251 let raw = json!({"route": "memory.search", "args": {"query": "x", "limit": 3}});
6252 let encoded = encode_glyph("memory.search", &json!({"query": "x", "limit": 3}));
6253 assert_eq!(encoded["r"], "Ms");
6254 assert_eq!(encoded["a"]["q"], "x");
6255 assert_eq!(encoded["a"]["n"], 3);
6256 let decoded = decode_glyph(&encoded).expect("glyph input must decode");
6257 assert_eq!(decoded["route"], raw["route"]);
6258 assert_eq!(decoded["args"]["query"], "x");
6259 assert_eq!(decoded["args"]["limit"], 3);
6260 }
6261
6262 #[test]
6263 fn glyph_unknown_codes_pass_through() {
6264 let weird = json!({"r": "not-a-code", "a": {"zzz": 1}});
6265 assert!(decode_glyph(&weird).is_none(), "unknown route code refuses");
6266 let partial = json!({"r": "Ms", "a": {"zzz": 1}});
6267 let decoded = decode_glyph(&partial).expect("known route decodes");
6268 assert_eq!(decoded["args"]["zzz"], 1, "unknown arg code passes through");
6269 assert_eq!(decode_glyph(&json!({"thought": "hi"})), None);
6270 }
6271
6272 #[test]
6273 fn glyph_book_covers_measured_routes() {
6274 for route in [
6276 "memory.search",
6277 "memory.create",
6278 "session.record",
6279 "session.continuity",
6280 "dharma.escalate",
6281 "graph.walk",
6282 "tools.list",
6283 "citta.status",
6284 ] {
6285 assert!(
6286 glyph_lookup(GLYPH_ROUTES, route).is_some(),
6287 "missing {route}"
6288 );
6289 }
6290 }
6291
6292 #[test]
6293 fn glyph_logographic_ideograms_decode_losslessly() {
6294 let search_call = json!({
6296 "r": "忆",
6297 "a": {
6298 "问": "auth failure",
6299 "数": 5
6300 }
6301 });
6302 let decoded = decode_glyph(&search_call).expect("logographic search decodes");
6303 assert_eq!(decoded["route"], "memory.search");
6304 assert_eq!(decoded["args"]["query"], "auth failure");
6305 assert_eq!(decoded["args"]["limit"], 5);
6306
6307 let checkpoint_call = json!({
6308 "r": "契",
6309 "a": {
6310 "文": "v9.3 milestone reached"
6311 }
6312 });
6313 let decoded_cp = decode_glyph(&checkpoint_call).expect("checkpoint decodes");
6314 assert_eq!(decoded_cp["route"], "session.checkpoint");
6315 assert_eq!(decoded_cp["args"]["content"], "v9.3 milestone reached");
6316
6317 let status_call = json!({"r": "心", "a": {}});
6318 let decoded_st = decode_glyph(&status_call).expect("citta status decodes");
6319 assert_eq!(decoded_st["route"], "citta.status");
6320 }
6321
6322 #[test]
6323 fn lkep_expression_decodes_and_normalizes() {
6324 let (route, args) =
6326 decode_lkep(&json!("忆(问=\"deadlock\", 数=3)")).expect("LKEP string decodes");
6327 assert_eq!(route, "memory.search");
6328 assert_eq!(args["query"], "deadlock");
6329 assert_eq!(args["limit"], 3);
6330
6331 let (route2, args2) =
6333 decode_lkep(&json!("忆: memory corruption")).expect("colon syntax decodes");
6334 assert_eq!(route2, "memory.search");
6335 assert_eq!(args2["query"], "memory corruption");
6336
6337 let (route3, args3) = decode_lkep(&json!("律")).expect("bare route decodes");
6339 assert_eq!(route3, "dharma.rules");
6340 assert_eq!(args3, json!({}));
6341
6342 let (route4, args4) =
6344 decode_lkep(&json!({"忆": "fast lookup"})).expect("root ideogram decodes");
6345 assert_eq!(route4, "memory.search");
6346 assert_eq!(args4["query"], "fast lookup");
6347 }
6348}