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