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