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