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) -> ToolRegistry {
3556 let mut reg = registry
3557 .register(Arc::new(MemoryCreateTool::new(
3558 store.clone(),
3559 search.clone(),
3560 recall.clone(),
3561 )))
3562 .register(Arc::new(MemoryBatchCreateTool::new(
3563 store.clone(),
3564 search.clone(),
3565 recall.clone(),
3566 )))
3567 .register(Arc::new(MemoryReadTool::new(store.clone())))
3568 .register(Arc::new(MemoryListTool::new(store.clone())))
3569 .register(Arc::new(MemoryDeleteTool::new(
3570 store.clone(),
3571 search.clone(),
3572 )))
3573 .register(Arc::new(MemoryBatchDeleteTool::new(
3574 store.clone(),
3575 search.clone(),
3576 )))
3577 .register(Arc::new(MemoryQueryTool::new(store.clone())))
3578 .register(Arc::new(MemoryAssociateTool::new(store.clone())))
3579 .register(Arc::new(MemoryAssociationsTool::new(store.clone())))
3580 .register(Arc::new(MemoryVectorSearchTool::new(
3581 store.clone(),
3582 vector_store,
3583 )))
3584 .register(Arc::new(GnosisTool::new(store.clone())));
3585
3586 if let Some(conv) = conversational {
3587 reg = reg.register(Arc::new(MemoryChatTool::new(conv)));
3588 }
3589
3590 if let Some(s) = search {
3591 reg = reg.register(Arc::new(
3595 expansion::MemoryHybridRecallTool::as_search(
3596 store.clone(),
3597 Some(s.clone()),
3598 recall.clone(),
3599 )
3600 .with_associations(Some(associations.clone())),
3601 ));
3602 reg = expansion::register_expansion(
3604 ®,
3605 store,
3606 Some(s),
3607 recall,
3608 associations,
3609 spiral_tracker,
3610 karma.clone(),
3611 substrate.clone(),
3612 homeostatic_loop,
3613 anomaly_detector,
3614 sensorimotor_bus,
3615 reflex_loop,
3616 gan_ying_bus,
3617 transaction_state,
3618 resource_rules.as_ref(),
3619 escalation_queue,
3620 dharma.as_ref(),
3621 firewall,
3622 code_graph,
3623 registry_persistence,
3624 );
3625 } else {
3626 reg = expansion::register_expansion(
3627 ®,
3628 store,
3629 None,
3630 recall,
3631 associations,
3632 spiral_tracker,
3633 karma.clone(),
3634 substrate.clone(),
3635 homeostatic_loop,
3636 anomaly_detector,
3637 sensorimotor_bus,
3638 reflex_loop,
3639 gan_ying_bus,
3640 transaction_state,
3641 resource_rules.as_ref(),
3642 escalation_queue,
3643 dharma.as_ref(),
3644 firewall,
3645 code_graph,
3646 registry_persistence,
3647 );
3648 }
3649 if let Some(k) = karma {
3650 reg = reg.register(Arc::new(KarmaReportTool::new(k)));
3651 }
3652 if let Some(d) = dharma {
3653 reg = reg.register(Arc::new(DharmaStatusTool::new(d.clone())));
3654 }
3655 if let Some(s) = substrate {
3656 reg = reg
3657 .register(Arc::new(HarmonyVectorTool::new(s.clone())))
3658 .register(Arc::new(HarmonyHistoryTool::new(s.clone())));
3659 if let Some(d) = dharma {
3660 if let Some(r) = resource_rules {
3661 reg = reg
3662 .register(Arc::new(GnosisStatusTool::new(
3663 d.clone(),
3664 r.clone(),
3665 s.clone(),
3666 )))
3667 .register(Arc::new(GnosisHistoryTool::new(s)))
3668 .register(Arc::new(GnosisExplainTool::new(d.clone(), r.clone())));
3669 }
3670 }
3671 }
3672
3673 reg
3674}
3675
3676pub fn register_meta_tools(
3682 registry: &ToolRegistry,
3683 store: &Arc<MemoryStore>,
3684 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3685) -> ToolRegistry {
3686 register_meta_tools_with_router(registry, store, shadow_stats, None).0
3687}
3688
3689#[must_use]
3699pub fn register_meta_tools_with_router(
3700 registry: &ToolRegistry,
3701 store: &Arc<MemoryStore>,
3702 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3703 pipeline: Option<Arc<DispatchPipeline>>,
3704) -> (ToolRegistry, Option<Arc<embedding_router::EmbeddingRouter>>) {
3705 let base_snapshot: Vec<Arc<dyn Tool>> = registry.all();
3706 let tool_count = base_snapshot.len();
3708
3709 let non_gnosis: Vec<Arc<dyn Tool>> = base_snapshot
3710 .iter()
3711 .filter(|t| t.name() != "gnosis")
3712 .cloned()
3713 .collect();
3714
3715 let mut list_builder = ToolRegistryBuilder::new();
3717 for tool in &non_gnosis {
3718 list_builder.register(tool.clone());
3719 }
3720 let list_registry = Arc::new(list_builder.build());
3721 let tools_list = Arc::new(ToolsListTool::new(Arc::clone(&list_registry)));
3722
3723 let usage_report = Arc::new(expansion::ToolsUsageReportTool::new(list_registry));
3727
3728 let gnosis = Arc::new(GnosisTool::with_tool_count(Arc::clone(store), tool_count));
3730 let mut wm_builder = ToolRegistryBuilder::new();
3731 for tool in &non_gnosis {
3732 wm_builder.register(tool.clone());
3733 }
3734 wm_builder.register(tools_list.clone());
3735 wm_builder.register(usage_report.clone());
3736 wm_builder.register(gnosis.clone());
3737
3738 let shadow_report = Arc::new(expansion::NluShadowReportTool::new(Arc::clone(
3743 &shadow_stats,
3744 )));
3745 wm_builder.register(shadow_report.clone());
3746 let wm = Arc::new(WmMetaTool::with_router_shadow_stats_and_pipeline(
3747 Arc::new(wm_builder.build()),
3748 wm_memory::create_embedder(),
3749 shadow_stats,
3750 pipeline,
3751 ));
3752 let router = wm.embedding_router().cloned();
3753
3754 let mut final_builder = ToolRegistryBuilder::new();
3756 for tool in non_gnosis {
3757 final_builder.register(tool);
3758 }
3759 final_builder.register(tools_list);
3760 final_builder.register(usage_report);
3761 final_builder.register(wm);
3762 final_builder.register(gnosis);
3763 final_builder.register(shadow_report);
3764 (final_builder.build(), router)
3765}
3766
3767#[cfg(test)]
3768mod tests {
3769 use super::*;
3770 use wm_core::BrainWave;
3771
3772 fn test_store() -> Arc<MemoryStore> {
3773 let tmp = tempfile::tempdir().unwrap();
3774 Arc::new(MemoryStore::open_default(tmp.path()).unwrap())
3775 }
3776
3777 #[tokio::test]
3778 async fn memory_create_warns_on_credential_shaped_content() {
3779 let store = test_store();
3780 let tool = MemoryCreateTool::new(store, None, None);
3781 let mut ctx = Context::default();
3782
3783 let clean = tool
3784 .call(
3785 &mut ctx,
3786 json!({"content": "the password policy requires rotation"}),
3787 )
3788 .await
3789 .unwrap();
3790 assert!(clean.get("warnings").is_none(), "clean content: {clean}");
3791
3792 let flagged = tool
3793 .call(
3794 &mut ctx,
3795 json!({"content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEow\n-----END RSA PRIVATE KEY-----"}),
3796 )
3797 .await
3798 .unwrap();
3799 assert_eq!(
3800 flagged["status"], "success",
3801 "warning, not refusal: {flagged}"
3802 );
3803 let warnings = flagged["warnings"].as_array().unwrap();
3804 assert!(
3805 warnings[0].as_str().unwrap().contains("private_key_pem"),
3806 "got: {warnings:?}"
3807 );
3808 assert!(warnings[0].as_str().unwrap().contains("keyring"));
3809 }
3810
3811 #[tokio::test]
3812 async fn memory_batch_create_aggregates_credential_warnings() {
3813 let store = test_store();
3814 let tool = MemoryBatchCreateTool::new(store, None, None);
3815 let mut ctx = Context::default();
3816 let r = tool
3817 .call(
3818 &mut ctx,
3819 json!({"items": [
3820 {"content": "ordinary note"},
3821 {"content": "AKIAIOSFODNN7EXAMPLE"},
3822 ]}),
3823 )
3824 .await
3825 .unwrap();
3826 assert_eq!(r["count"], 2);
3827 let warnings = r["warnings"].as_array().unwrap();
3828 assert!(warnings[0].as_str().unwrap().contains("aws_access_key_id"));
3829 }
3830
3831 fn test_registry_with(store: &Arc<MemoryStore>) -> ToolRegistry {
3832 let registry = ToolRegistry::new();
3833 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
3834 let spiral_tracker =
3835 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
3836 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
3837 register_all(
3838 ®istry,
3839 store,
3840 None,
3841 None,
3842 &None,
3843 None,
3844 &None,
3845 associations,
3846 spiral_tracker,
3847 vector_store,
3848 None,
3849 None,
3850 None,
3851 None,
3852 None,
3853 None,
3854 None,
3855 std::sync::Arc::new(std::sync::Mutex::new(None)),
3856 None,
3857 None,
3858 None,
3859 expansion::RegistryPersistenceMode::Normal,
3860 )
3861 }
3862
3863 #[tokio::test]
3864 async fn memory_create_and_read() {
3865 let store = test_store();
3866 let tool = MemoryCreateTool::new(store.clone(), None, None);
3867 let mut ctx = Context::new(BrainWave::Gamma);
3868
3869 let args = json!({"content": "test memory content", "galaxy": "codex"});
3870 let result = tool.call(&mut ctx, args).await.unwrap();
3871 assert_eq!(result["status"], "success");
3872 let id = result["id"].as_str().unwrap();
3873
3874 let read_tool = MemoryReadTool::new(store.clone());
3875 let result = read_tool.call(&mut ctx, json!({"id": id})).await.unwrap();
3876 assert_eq!(result["status"], "success");
3877 assert_eq!(result["content"], "test memory content");
3878
3879 let episodic = store
3880 .episodic()
3881 .get(uuid::Uuid::parse_str(id).unwrap())
3882 .unwrap()
3883 .expect("explicit memory writes mirror into episodic storage");
3884 assert_eq!(episodic.content, "test memory content");
3885 }
3886
3887 #[tokio::test]
3892 async fn memory_create_attestation_disclosure() {
3893 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
3894 let mut ctx = Context::new(BrainWave::Gamma);
3895
3896 let store = test_store();
3898 let tool = MemoryCreateTool::with_attestation_key(store.clone(), None, None, None);
3899 let result = tool
3900 .call(
3901 &mut ctx,
3902 json!({"content": "unattested create", "galaxy": "codex"}),
3903 )
3904 .await
3905 .unwrap();
3906 assert_eq!(result["status"], "success");
3907 assert_eq!(result["attested"], false);
3908 assert_eq!(result["attested_reason"], "node key unavailable");
3909
3910 let tool = MemoryCreateTool::with_attestation_key(
3912 store.clone(),
3913 None,
3914 None,
3915 Some("not-hex".to_string()),
3916 );
3917 let result = tool
3918 .call(
3919 &mut ctx,
3920 json!({"content": "bad key create", "galaxy": "codex"}),
3921 )
3922 .await
3923 .unwrap();
3924 assert_eq!(result["attested"], false);
3925 assert_eq!(result["attested_reason"], "node key invalid");
3926
3927 let tool = MemoryCreateTool::with_attestation_key(
3929 store.clone(),
3930 None,
3931 None,
3932 Some(TEST_KEY.to_string()),
3933 );
3934 let result = tool
3935 .call(
3936 &mut ctx,
3937 json!({"content": "attested create", "galaxy": "codex"}),
3938 )
3939 .await
3940 .unwrap();
3941 assert_eq!(result["attested"], true);
3942 assert!(result.get("attested_reason").is_none());
3943 let id = uuid::Uuid::parse_str(result["id"].as_str().unwrap()).unwrap();
3944 let report = store.verify_attestation(Galaxy::Codex, id).unwrap();
3945 assert!(report.attested, "{:?}", report.breaks);
3946 assert!(report.signature_valid, "{:?}", report.breaks);
3947 assert!(report.matches_head, "{:?}", report.breaks);
3948 assert!(report.memory_present);
3949 assert!(report.breaks.is_empty());
3950
3951 let mut memory = store.get(Galaxy::Codex, id).unwrap().unwrap();
3955 memory.content = "edited after attestation".to_string();
3956 memory.metadata.content_hash = wm_memory::content_hash(&memory.content);
3957 store.put(Galaxy::Codex, &memory).unwrap();
3958 let stale = store.verify_attestation(Galaxy::Codex, id).unwrap();
3959 assert!(stale.attested);
3960 assert!(stale.signature_valid);
3961 assert!(!stale.matches_head);
3962
3963 let scanned = store.scan_attestations().unwrap();
3965 assert_eq!(scanned.len(), 1);
3966 assert_eq!(scanned[0].memory_id, id.to_string());
3967 }
3968
3969 #[tokio::test]
3970 async fn memory_batch_create_attests_each_item() {
3971 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
3972 let store = test_store();
3973 let tool = MemoryBatchCreateTool::with_attestation_key(
3974 store.clone(),
3975 None,
3976 None,
3977 Some(TEST_KEY.to_string()),
3978 );
3979 let mut ctx = Context::new(BrainWave::Gamma);
3980 let result = tool
3981 .call(
3982 &mut ctx,
3983 json!({"items": [{"content": "batch one"}, {"content": "batch two"}]}),
3984 )
3985 .await
3986 .unwrap();
3987 assert_eq!(result["attested_count"], 2);
3988 assert_eq!(store.scan_attestations().unwrap().len(), 2);
3989
3990 let tool = MemoryBatchCreateTool::with_attestation_key(store.clone(), None, None, None);
3992 let result = tool
3993 .call(&mut ctx, json!({"items": [{"content": "batch three"}]}))
3994 .await
3995 .unwrap();
3996 assert_eq!(result["attested_count"], 0);
3997 assert_eq!(result["count"], 1);
3998 }
3999
4000 #[tokio::test]
4001 async fn memory_batch_create_mirrors_into_episodic_lane() {
4002 let store = test_store();
4003 let tool = MemoryBatchCreateTool::new(store.clone(), None, None);
4004 let mut ctx = Context::new(BrainWave::Gamma);
4005 let result = tool
4006 .call(
4007 &mut ctx,
4008 json!({
4009 "items": [
4010 {"content": "batch rust retrieval"},
4011 {"content": "batch grocery list"}
4012 ]
4013 }),
4014 )
4015 .await
4016 .unwrap();
4017 assert_eq!(result["status"], "success");
4018 let ids = result["ids"].as_array().unwrap();
4019 let first = uuid::Uuid::parse_str(ids[0].as_str().unwrap()).unwrap();
4020 let hits = store
4021 .episodic()
4022 .search("rust retrieval", 10, false)
4023 .unwrap();
4024 assert_eq!(hits.len(), 1);
4025 assert_eq!(hits[0].record.id, first);
4026 }
4027
4028 #[tokio::test]
4029 async fn memory_list_returns_entries() {
4030 let store = test_store();
4031 let create = MemoryCreateTool::new(store.clone(), None, None);
4032 let mut ctx = Context::new(BrainWave::Gamma);
4033
4034 for i in 0..3 {
4035 create
4036 .call(&mut ctx, json!({"content": format!("item-{i}")}))
4037 .await
4038 .unwrap();
4039 }
4040
4041 let list = MemoryListTool::new(store);
4042 let result = list.call(&mut ctx, json!({"limit": 10})).await.unwrap();
4043 assert_eq!(result["status"], "success");
4044 assert_eq!(result["total"], 3);
4045 assert_eq!(result["returned"], 3);
4046 }
4047
4048 #[tokio::test]
4052 async fn memory_list_offset_and_exclude_tags_page_visible_surface() {
4053 let store = test_store();
4054 let mut ctx = Context::new(BrainWave::Gamma);
4055
4056 for i in 0..5 {
4057 let mut m = wm_memory::Memory::new(wm_core::Galaxy::Codex, format!("page note {i}"));
4058 if i == 1 {
4059 m.metadata.tags = vec!["noise".into()];
4060 }
4061 if i == 3 {
4062 m.metadata.is_private = true;
4063 }
4064 store.put(wm_core::Galaxy::Codex, &m).unwrap();
4065 }
4066
4067 let list = MemoryListTool::new(store);
4068
4069 let all = list
4072 .call(
4073 &mut ctx,
4074 json!({"galaxy": "codex", "limit": 50, "exclude_tags": ["noise"]}),
4075 )
4076 .await
4077 .unwrap();
4078 assert_eq!(all["total"], 5, "total counts the whole galaxy");
4079 assert_eq!(all["matched"], 3, "private + excluded are invisible");
4080 assert_eq!(all["returned"], 3);
4081 assert_eq!(all["offset"], 0);
4082
4083 let page1 = list
4085 .call(
4086 &mut ctx,
4087 json!({"galaxy": "codex", "limit": 2, "offset": 0, "exclude_tags": ["noise"]}),
4088 )
4089 .await
4090 .unwrap();
4091 assert_eq!(page1["returned"], 2);
4092 let page2 = list
4093 .call(
4094 &mut ctx,
4095 json!({"galaxy": "codex", "limit": 2, "offset": 2, "exclude_tags": ["noise"]}),
4096 )
4097 .await
4098 .unwrap();
4099 assert_eq!(
4100 page2["returned"], 1,
4101 "matched is 3 — the tail page is short"
4102 );
4103 assert_eq!(page2["offset"], 2);
4104
4105 let ids_of = |v: &Value| -> Vec<String> {
4106 v["memories"]
4107 .as_array()
4108 .unwrap()
4109 .iter()
4110 .filter_map(|m| m["id"].as_str().map(String::from))
4111 .collect()
4112 };
4113 let (p1, p2, everything) = (ids_of(&page1), ids_of(&page2), ids_of(&all));
4114 assert_eq!(p1.len(), 2);
4115 let mut union = p1;
4116 union.extend(p2);
4117 let mut sorted_union = union.clone();
4118 sorted_union.sort();
4119 let mut sorted_all = everything;
4120 sorted_all.sort();
4121 assert_eq!(sorted_union, sorted_all, "pages must partition the surface");
4122 }
4123
4124 #[tokio::test]
4129 async fn memory_create_stamps_provenance_by_claim() {
4130 let store = test_store();
4131 let create = MemoryCreateTool::new(store.clone(), None, None);
4132 let mut ctx = Context::new(BrainWave::Gamma);
4133
4134 let silent = create
4135 .call(&mut ctx, json!({"content": "no claim"}))
4136 .await
4137 .unwrap();
4138 assert_eq!(silent["source"], "agent");
4139 assert!((silent["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
4140
4141 let claimed = create
4142 .call(
4143 &mut ctx,
4144 json!({"content": "user dictated this", "source": "user"}),
4145 )
4146 .await
4147 .unwrap();
4148 assert_eq!(claimed["source"], "user");
4149 assert!((claimed["source_trust"].as_f64().unwrap() - 1.0).abs() < 1e-5);
4150
4151 let custom = create
4152 .call(&mut ctx, json!({"content": "web import", "source": "web"}))
4153 .await
4154 .unwrap();
4155 assert_eq!(custom["source"], "web");
4156 assert!((custom["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
4157
4158 let fetch = |id: &str| {
4159 store
4160 .get(wm_core::Galaxy::Codex, uuid::Uuid::parse_str(id).unwrap())
4161 .expect("stored")
4162 .expect("present")
4163 };
4164 assert_eq!(
4165 fetch(silent["id"].as_str().unwrap()).metadata.source,
4166 "agent"
4167 );
4168 assert_eq!(
4169 fetch(claimed["id"].as_str().unwrap()).metadata.source,
4170 "user"
4171 );
4172 }
4173
4174 #[tokio::test]
4175 async fn gnosis_returns_system_info() {
4176 let store = test_store();
4177 let tool = GnosisTool::new(store);
4178 let mut ctx = Context::new(BrainWave::Gamma);
4179 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4180 assert_eq!(result["status"], "success");
4181 assert!(result["version"].is_string());
4182 }
4183
4184 #[tokio::test]
4185 async fn memory_delete_removes_entry() {
4186 let store = test_store();
4187 let create = MemoryCreateTool::new(store.clone(), None, None);
4188 let mut ctx = Context::new(BrainWave::Gamma);
4189
4190 let result = create
4191 .call(&mut ctx, json!({"content": "to be deleted"}))
4192 .await
4193 .unwrap();
4194 let id = result["id"].as_str().unwrap();
4195
4196 let delete = MemoryDeleteTool::new(store.clone(), None);
4197 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4198 assert_eq!(result["status"], "success");
4199
4200 let read = MemoryReadTool::new(store);
4201 let result = read.call(&mut ctx, json!({"id": id})).await.unwrap();
4202 assert_eq!(result["status"], "not_found");
4203 }
4204
4205 #[tokio::test]
4206 async fn memory_delete_without_galaxy_resolves_across_memory_galaxies() {
4207 let store = test_store();
4208 let create = MemoryCreateTool::new(store.clone(), None, None);
4209 let mut ctx = Context::new(BrainWave::Gamma);
4210
4211 let result = create
4213 .call(
4214 &mut ctx,
4215 json!({"content": "session decision", "galaxy": "sessions"}),
4216 )
4217 .await
4218 .unwrap();
4219 let id = result["id"].as_str().unwrap();
4220
4221 let delete = MemoryDeleteTool::new(store.clone(), None);
4223 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4224 assert_eq!(result["status"], "success");
4225 assert!(
4226 result["galaxies"]
4227 .as_array()
4228 .unwrap()
4229 .contains(&json!("sessions"))
4230 );
4231
4232 let read = MemoryReadTool::new(store.clone());
4233 let result = read
4234 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4235 .await
4236 .unwrap();
4237 assert_eq!(result["status"], "not_found");
4238 }
4239
4240 #[tokio::test]
4241 async fn memory_delete_explicit_galaxy_does_not_miss_other_galaxies() {
4242 let store = test_store();
4243 let create = MemoryCreateTool::new(store.clone(), None, None);
4244 let mut ctx = Context::new(BrainWave::Gamma);
4245
4246 let result = create
4247 .call(
4248 &mut ctx,
4249 json!({"content": "in sessions", "galaxy": "sessions"}),
4250 )
4251 .await
4252 .unwrap();
4253 let id = result["id"].as_str().unwrap();
4254
4255 let delete = MemoryDeleteTool::new(store.clone(), None);
4257 let result = delete
4258 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4259 .await
4260 .unwrap();
4261 assert_eq!(result["status"], "not_found");
4262 assert!(result["hint"].is_string());
4263
4264 let read = MemoryReadTool::new(store.clone());
4265 let result = read
4266 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4267 .await
4268 .unwrap();
4269 assert_eq!(result["status"], "success");
4270 }
4271
4272 #[tokio::test]
4273 async fn memory_query_filters_by_tags() {
4274 let store = test_store();
4275 let create = MemoryCreateTool::new(store.clone(), None, None);
4276 let mut ctx = Context::new(BrainWave::Gamma);
4277
4278 create
4279 .call(&mut ctx, json!({"content": "tagged", "tags": ["rust"]}))
4280 .await
4281 .unwrap();
4282 create
4283 .call(&mut ctx, json!({"content": "untagged"}))
4284 .await
4285 .unwrap();
4286
4287 let query = MemoryQueryTool::new(store);
4288 let result = query
4289 .call(&mut ctx, json!({"tags": ["rust"]}))
4290 .await
4291 .unwrap();
4292 assert_eq!(result["status"], "success");
4293 assert_eq!(result["total"], 1);
4294 }
4295
4296 #[tokio::test]
4299 async fn memory_query_time_range_passthrough() {
4300 let store = test_store();
4301 let mut ctx = Context::new(BrainWave::Gamma);
4302
4303 let mut old = wm_memory::Memory::new(wm_core::Galaxy::Codex, "old relic".into());
4304 old.metadata.created_at = chrono::Utc::now() - chrono::Duration::days(60);
4305 store.put(wm_core::Galaxy::Codex, &old).unwrap();
4306 let mut recent = wm_memory::Memory::new(wm_core::Galaxy::Codex, "recent note".into());
4307 recent.metadata.created_at = chrono::Utc::now() - chrono::Duration::hours(1);
4308 store.put(wm_core::Galaxy::Codex, &recent).unwrap();
4309
4310 let query = MemoryQueryTool::new(store);
4311 let cutoff = (chrono::Utc::now() - chrono::Duration::days(1))
4312 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
4313
4314 let only_recent = query
4315 .call(&mut ctx, json!({"created_after": cutoff}))
4316 .await
4317 .unwrap();
4318 assert_eq!(only_recent["total"], 1);
4319 assert_eq!(only_recent["memories"][0]["content_preview"], "recent note");
4320 assert_eq!(
4321 only_recent["time_range"]["created_after"], cutoff,
4322 "the applied time range must be disclosed"
4323 );
4324
4325 let only_old = query
4326 .call(&mut ctx, json!({"created_before": cutoff}))
4327 .await
4328 .unwrap();
4329 assert_eq!(only_old["total"], 1);
4330 assert_eq!(only_old["memories"][0]["content_preview"], "old relic");
4331
4332 let both = query
4334 .call(
4335 &mut ctx,
4336 json!({
4337 "created_after": (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
4338 "created_before": cutoff,
4339 }),
4340 )
4341 .await
4342 .unwrap();
4343 assert_eq!(both["total"], 1);
4344 assert_eq!(both["memories"][0]["content_preview"], "old relic");
4345
4346 let bad = query
4348 .call(&mut ctx, json!({"created_after": "not-a-timestamp"}))
4349 .await;
4350 assert!(bad.is_err(), "invalid RFC 3339 must be refused");
4351 }
4352
4353 #[tokio::test]
4354 async fn memory_vector_search_by_embedding() {
4355 let store = test_store();
4356 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4357
4358 {
4360 let mut vs = vector_store.lock().unwrap();
4361 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![1.0, 0.0, 0.0]);
4362 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![0.9, 0.1, 0.0]);
4363 vs.add(uuid::Uuid::new_v4(), Galaxy::Research, vec![0.0, 1.0, 0.0]);
4364 }
4365
4366 let tool = MemoryVectorSearchTool::new(store, vector_store);
4367 let mut ctx = Context::new(BrainWave::Gamma);
4368
4369 let result = tool
4371 .call(&mut ctx, json!({"embedding": [1.0, 0.0, 0.0], "limit": 2}))
4372 .await
4373 .unwrap();
4374 assert_eq!(result["status"], "success");
4375 assert_eq!(result["total"], 2);
4376 }
4377
4378 #[tokio::test]
4379 async fn memory_vector_search_missing_args() {
4380 let store = test_store();
4381 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4382
4383 let tool = MemoryVectorSearchTool::new(store, vector_store);
4384 let mut ctx = Context::new(BrainWave::Gamma);
4385
4386 let result = tool.call(&mut ctx, json!({"limit": 5})).await;
4387 assert!(result.is_err());
4388 }
4389
4390 #[tokio::test]
4391 async fn wm_routes_vector_search_to_memory_vector_search() {
4392 let store = test_store();
4393 let registry = test_registry_with(&store);
4394 let registry = register_meta_tools(
4395 ®istry,
4396 &store,
4397 std::sync::Arc::new(std::sync::RwLock::new(
4398 embedding_router::ShadowModeStats::default(),
4399 )),
4400 );
4401
4402 let wm = registry.get("wm").unwrap();
4403 let mut ctx = Context::new(BrainWave::Gamma);
4404 let result = wm
4405 .call(
4406 &mut ctx,
4407 json!({"route": "memory.vector.search", "args": {"embedding": [1.0, 0.0, 0.0]}}),
4408 )
4409 .await
4410 .unwrap();
4411
4412 assert_eq!(result["status"], "success");
4413 assert_eq!(result["_wm_route"]["tool"], "memory.vector.search");
4414 }
4415
4416 #[tokio::test]
4417 async fn wm_routes_shadow_report_inside_meta_tool() {
4418 let store = test_store();
4422 let registry = test_registry_with(&store);
4423 let registry = register_meta_tools(
4424 ®istry,
4425 &store,
4426 std::sync::Arc::new(std::sync::RwLock::new(
4427 embedding_router::ShadowModeStats::default(),
4428 )),
4429 );
4430
4431 let wm = registry.get("wm").unwrap();
4432 let mut ctx = Context::new(BrainWave::Gamma);
4433 let result = wm
4434 .call(&mut ctx, json!({"route": "nlu.shadow_report"}))
4435 .await
4436 .unwrap();
4437
4438 assert_eq!(result["_wm_route"]["tool"], "nlu.shadow_report");
4439 assert!(
4440 result.get("total_queries").is_some(),
4441 "expected shadow report payload"
4442 );
4443 }
4444
4445 #[tokio::test]
4446 async fn memory_associate_and_find() {
4447 let store = test_store();
4448 let create = MemoryCreateTool::new(store.clone(), None, None);
4449 let mut ctx = Context::new(BrainWave::Gamma);
4450
4451 let r1 = create
4452 .call(&mut ctx, json!({"content": "source mem"}))
4453 .await
4454 .unwrap();
4455 let r2 = create
4456 .call(&mut ctx, json!({"content": "target mem"}))
4457 .await
4458 .unwrap();
4459 let id1 = r1["id"].as_str().unwrap();
4460 let id2 = r2["id"].as_str().unwrap();
4461
4462 let assoc = MemoryAssociateTool::new(store.clone());
4463 let result = assoc
4464 .call(
4465 &mut ctx,
4466 json!({"source": id1, "target": id2, "weight": 0.8}),
4467 )
4468 .await
4469 .unwrap();
4470 assert_eq!(result["status"], "success");
4471
4472 let find = MemoryAssociationsTool::new(store);
4473 let result = find
4474 .call(&mut ctx, json!({"id": id1, "direction": "from"}))
4475 .await
4476 .unwrap();
4477 assert_eq!(result["status"], "success");
4478 assert_eq!(result["returned"], 1);
4479 }
4480
4481 #[tokio::test]
4482 async fn karma_report_shows_entries() {
4483 let tmp = tempfile::tempdir().unwrap();
4484 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
4485 let ledger = Arc::new(KarmaLedger::new(store).unwrap());
4486
4487 ledger.record("test_tool", false, 0, true).unwrap();
4489 ledger.record("wasteful_tool", true, 0, true).unwrap();
4490
4491 let tool = KarmaReportTool::new(ledger);
4492 let mut ctx = Context::new(BrainWave::Gamma);
4493 let result = tool.call(&mut ctx, json!({"limit": 5})).await.unwrap();
4494 assert_eq!(result["status"], "success");
4495 assert_eq!(result["entry_count"], 2);
4496 assert_eq!(result["recent_entries"].as_array().unwrap().len(), 2);
4497 }
4498
4499 #[tokio::test]
4500 async fn dharma_status_returns_homeostasis() {
4501 let gate = Arc::new(DharmaGate::default());
4502 let tool = DharmaStatusTool::new(gate);
4503 let mut ctx = Context::new(BrainWave::Gamma);
4504 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4505 assert_eq!(result["status"], "success");
4506 assert!(result["homeostasis"]["health_score"].is_f64());
4507 assert!(result["sutras"]["ahimsa"].is_string());
4508 }
4509
4510 #[tokio::test]
4511 async fn wm_routes_remember_to_memory_create() {
4512 let store = test_store();
4513 let registry = test_registry_with(&store);
4514 let registry = register_meta_tools(
4515 ®istry,
4516 &store,
4517 std::sync::Arc::new(std::sync::RwLock::new(
4518 embedding_router::ShadowModeStats::default(),
4519 )),
4520 );
4521
4522 let wm = registry.get("wm").unwrap();
4523 let mut ctx = Context::new(BrainWave::Gamma);
4524 let result = wm
4525 .call(
4526 &mut ctx,
4527 json!({"thought": "remember that the API uses X-User-Id headers"}),
4528 )
4529 .await
4530 .unwrap();
4531
4532 assert_eq!(result["status"], "success");
4533 assert_eq!(result["_wm_route"]["tool"], "memory.create");
4534 assert!(result["id"].is_string());
4535 }
4536
4537 #[tokio::test]
4538 async fn wm_explicit_route() {
4539 let store = test_store();
4540 let registry = test_registry_with(&store);
4541 let registry = register_meta_tools(
4542 ®istry,
4543 &store,
4544 std::sync::Arc::new(std::sync::RwLock::new(
4545 embedding_router::ShadowModeStats::default(),
4546 )),
4547 );
4548
4549 let wm = registry.get("wm").unwrap();
4550 let mut ctx = Context::new(BrainWave::Gamma);
4551 let result = wm
4552 .call(
4553 &mut ctx,
4554 json!({
4555 "route": "gnosis"
4556 }),
4557 )
4558 .await
4559 .unwrap();
4560
4561 assert_eq!(result["status"], "success");
4562 assert_eq!(result["_wm_route"]["tool"], "gnosis");
4563 }
4564
4565 #[tokio::test]
4566 async fn wm_no_input_returns_error() {
4567 let store = test_store();
4568 let registry = test_registry_with(&store);
4569 let registry = register_meta_tools(
4570 ®istry,
4571 &store,
4572 std::sync::Arc::new(std::sync::RwLock::new(
4573 embedding_router::ShadowModeStats::default(),
4574 )),
4575 );
4576
4577 let wm = registry.get("wm").unwrap();
4578 let mut ctx = Context::new(BrainWave::Gamma);
4579 let result = wm.call(&mut ctx, json!({})).await.unwrap();
4580
4581 assert_eq!(result["status"], "error");
4582 }
4583
4584 #[tokio::test]
4585 async fn wm_missing_route_echoes_received_keys() {
4586 let store = test_store();
4591 let registry = test_registry_with(&store);
4592 let registry = register_meta_tools(
4593 ®istry,
4594 &store,
4595 std::sync::Arc::new(std::sync::RwLock::new(
4596 embedding_router::ShadowModeStats::default(),
4597 )),
4598 );
4599
4600 let wm = registry.get("wm").unwrap();
4601 let mut ctx = Context::new(BrainWave::Gamma);
4602 let result = wm
4603 .call(
4604 &mut ctx,
4605 json!({"content": "x", "turn_type": "summary", "importance": 0.5}),
4606 )
4607 .await
4608 .unwrap();
4609
4610 assert_eq!(result["status"], "error");
4611 let message = result["message"].as_str().unwrap();
4612 assert!(
4613 message.contains("received argument keys"),
4614 "error must disclose received keys, got: {message}"
4615 );
4616 for key in ["content", "turn_type", "importance"] {
4617 assert!(
4618 message.contains(key),
4619 "error must list received key '{key}', got: {message}"
4620 );
4621 }
4622 let empty = wm.call(&mut ctx, json!({})).await.unwrap();
4624 assert!(
4625 !empty["message"]
4626 .as_str()
4627 .unwrap()
4628 .contains("received argument keys: ["),
4629 "empty input must not list keys, got: {}",
4630 empty["message"]
4631 );
4632 }
4633
4634 #[tokio::test]
4635 async fn wm_unknown_tool_returns_error() {
4636 let store = test_store();
4637 let registry = test_registry_with(&store);
4638 let registry = register_meta_tools(
4639 ®istry,
4640 &store,
4641 std::sync::Arc::new(std::sync::RwLock::new(
4642 embedding_router::ShadowModeStats::default(),
4643 )),
4644 );
4645
4646 let wm = registry.get("wm").unwrap();
4647 let mut ctx = Context::new(BrainWave::Gamma);
4648 let result = wm
4649 .call(&mut ctx, json!({"route": "nonexistent.tool"}))
4650 .await
4651 .unwrap();
4652
4653 assert_eq!(result["status"], "error");
4654 assert!(result["message"].as_str().unwrap().contains("Unknown tool"));
4655 }
4656
4657 #[tokio::test]
4658 async fn wm_missing_arg_returns_hint() {
4659 let store = test_store();
4660 let registry = test_registry_with(&store);
4661 let registry = register_meta_tools(
4662 ®istry,
4663 &store,
4664 std::sync::Arc::new(std::sync::RwLock::new(
4665 embedding_router::ShadowModeStats::default(),
4666 )),
4667 );
4668
4669 let wm = registry.get("wm").unwrap();
4670 let mut ctx = Context::new(BrainWave::Gamma);
4671
4672 let result = wm
4674 .call(&mut ctx, json!({"route": "memory.read"}))
4675 .await
4676 .unwrap();
4677
4678 assert_eq!(result["status"], "error");
4679 assert!(
4680 result["message"]
4681 .as_str()
4682 .unwrap()
4683 .contains("Missing required argument")
4684 );
4685 assert!(result["hint"].as_str().unwrap().contains("uuid"));
4686 }
4687
4688 #[tokio::test]
4689 async fn wm_auto_route_missing_arg_returns_hint() {
4690 let store = test_store();
4691 let registry = test_registry_with(&store);
4692 let registry = register_meta_tools(
4693 ®istry,
4694 &store,
4695 std::sync::Arc::new(std::sync::RwLock::new(
4696 embedding_router::ShadowModeStats::default(),
4697 )),
4698 );
4699
4700 let wm = registry.get("wm").unwrap();
4701 let mut ctx = Context::new(BrainWave::Gamma);
4702
4703 let result = wm
4705 .call(&mut ctx, json!({"thought": "recall"}))
4706 .await
4707 .unwrap();
4708
4709 assert_eq!(result["status"], "error");
4710 assert!(result["hint"].as_str().unwrap().contains("uuid"));
4711 }
4712
4713 #[tokio::test]
4714 async fn wm_routes_karma_to_karma_report() {
4715 let tmp = tempfile::tempdir().unwrap();
4716 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
4717 let ledger = Arc::new(KarmaLedger::new(store.clone()).unwrap());
4718 let gate = Arc::new(DharmaGate::default());
4719
4720 let registry = ToolRegistry::new();
4721 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
4722 let spiral_tracker =
4723 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
4724 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4725 let registry = register_all(
4726 ®istry,
4727 &store,
4728 None,
4729 Some(ledger),
4730 &Some(gate),
4731 None,
4732 &None,
4733 associations,
4734 spiral_tracker,
4735 vector_store,
4736 None,
4737 None,
4738 None,
4739 None,
4740 None,
4741 None,
4742 None,
4743 std::sync::Arc::new(std::sync::Mutex::new(None)),
4744 None,
4745 None,
4746 None,
4747 expansion::RegistryPersistenceMode::Normal,
4748 );
4749 let registry = register_meta_tools(
4750 ®istry,
4751 &store,
4752 std::sync::Arc::new(std::sync::RwLock::new(
4753 embedding_router::ShadowModeStats::default(),
4754 )),
4755 );
4756
4757 let wm = registry.get("wm").unwrap();
4758 let mut ctx = Context::new(BrainWave::Gamma);
4759 let result = wm
4760 .call(&mut ctx, json!({"thought": "show me the karma report"}))
4761 .await
4762 .unwrap();
4763
4764 assert_eq!(result["status"], "success");
4765 assert_eq!(result["_wm_route"]["tool"], "karma.report");
4766 }
4767
4768 fn test_registry_with_pipeline(
4771 store: &Arc<MemoryStore>,
4772 ) -> (ToolRegistry, Arc<DispatchPipeline>) {
4773 let registry = test_registry_with(store);
4774 let pipeline = Arc::new(DispatchPipeline::with_defaults());
4775 let (registry, _router) = register_meta_tools_with_router(
4776 ®istry,
4777 store,
4778 std::sync::Arc::new(std::sync::RwLock::new(
4779 embedding_router::ShadowModeStats::default(),
4780 )),
4781 Some(pipeline.clone()),
4782 );
4783 (registry, pipeline)
4784 }
4785
4786 #[tokio::test]
4787 async fn wm_route_destructive_without_confirm_blocked_by_pipeline() {
4788 let store = test_store();
4789 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4790
4791 let wm = registry.get("wm").unwrap();
4792 let mut ctx = Context::new(BrainWave::Gamma);
4793 let result = wm
4794 .call(
4795 &mut ctx,
4796 json!({"route": "memory.delete", "args": {"id": "00000000-0000-0000-0000-000000000001"}}),
4797 )
4798 .await
4799 .unwrap();
4800
4801 assert_eq!(result["status"], "error");
4802 assert!(
4803 result["error"].as_str().unwrap().contains("destructive"),
4804 "expected destructive-gate message, got: {result}"
4805 );
4806 assert!(result["error"].as_str().unwrap().contains("confirm"));
4807 }
4808
4809 #[tokio::test]
4810 async fn wm_route_destructive_with_confirm_proceeds() {
4811 let store = test_store();
4812 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4813
4814 let memory = Memory::new(Galaxy::Codex, "delete me via wm route".into());
4816 let id = memory.metadata.id;
4817 store.put(Galaxy::Codex, &memory).unwrap();
4818
4819 let wm = registry.get("wm").unwrap();
4820 let mut ctx = Context::new(BrainWave::Gamma);
4821 let result = wm
4822 .call(
4823 &mut ctx,
4824 json!({"route": "memory.delete", "args": {"id": id.to_string(), "galaxy": "codex", "confirm": true}}),
4825 )
4826 .await
4827 .unwrap();
4828
4829 assert_eq!(result["status"], "success");
4830 assert_eq!(result["_wm_route"]["tool"], "memory.delete");
4831 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
4832 }
4833
4834 #[tokio::test]
4835 async fn wm_thought_cannot_reach_destructive_tool() {
4836 let store = test_store();
4837 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4838
4839 let wm = registry.get("wm").unwrap();
4840 let mut ctx = Context::new(BrainWave::Gamma);
4841 let result = wm
4844 .call(
4845 &mut ctx,
4846 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001"}),
4847 )
4848 .await
4849 .unwrap();
4850
4851 assert_eq!(result["status"], "error");
4852 assert!(
4853 result["message"]
4854 .as_str()
4855 .unwrap()
4856 .contains("cannot be reached via natural language"),
4857 "expected NLU hard-block message, got: {result}"
4858 );
4859 }
4860
4861 #[tokio::test]
4862 async fn wm_thought_cannot_reach_destructive_tool_even_with_confirm() {
4863 let store = test_store();
4864 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4865
4866 let wm = registry.get("wm").unwrap();
4867 let mut ctx = Context::new(BrainWave::Gamma);
4868 let result = wm
4872 .call(
4873 &mut ctx,
4874 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001", "args": {"confirm": true, "id": "00000000-0000-0000-0000-000000000001"}}),
4875 )
4876 .await
4877 .unwrap();
4878
4879 assert_eq!(result["status"], "error");
4880 assert!(
4881 result["message"]
4882 .as_str()
4883 .unwrap()
4884 .contains("cannot be reached via natural language")
4885 );
4886 }
4887
4888 #[tokio::test]
4893 async fn nlu_cannot_reach_any_destructive_tool() {
4894 let store = test_store();
4895 let (registry, _pipeline) = test_registry_with_pipeline(&store);
4896 let wm = registry.get("wm").unwrap();
4897
4898 let destructive_tools: Vec<String> = registry
4901 .all_ref()
4902 .iter()
4903 .filter(|t| t.effects().destructive)
4904 .map(|t| t.name().to_string())
4905 .collect();
4906
4907 assert!(
4908 !destructive_tools.is_empty(),
4909 "registry must contain at least one destructive tool for this test to be meaningful"
4910 );
4911
4912 let mut ctx = Context::new(BrainWave::Gamma);
4913 for tool_name in &destructive_tools {
4914 let result = wm
4918 .call(
4919 &mut ctx,
4920 json!({
4921 "thought": tool_name,
4922 "args": {"confirm": true}
4923 }),
4924 )
4925 .await
4926 .unwrap();
4927
4928 let routed_tool = result
4933 .get("_wm_route")
4934 .and_then(|r| r.get("tool"))
4935 .and_then(|t| t.as_str())
4936 .unwrap_or("");
4937 let resolved_destructive = registry
4938 .get(routed_tool)
4939 .is_some_and(|t| t.effects().destructive);
4940 assert!(
4941 result["status"] != "success" || !resolved_destructive,
4942 "destructive tool '{tool_name}' executed via NLU (resolved as '{routed_tool}') — structural gate failed"
4943 );
4944
4945 if routed_tool == tool_name {
4948 assert!(
4949 result
4950 .get("message")
4951 .and_then(|m| m.as_str())
4952 .is_some_and(|m| m.contains("cannot be reached via natural language")),
4953 "destructive tool '{tool_name}' was routed to but gate message missing: {result}"
4954 );
4955 }
4956
4957 let nl_phrase = match tool_name.as_str() {
4962 "memory.delete" => "delete memory 00000000-0000-0000-0000-000000000001",
4963 "transaction.rollback" => "rollback the transaction",
4964 "galaxy.purge" => "purge galaxy codex",
4965 "galaxy.transfer" => "transfer galaxy codex to archive",
4966 "galaxy.restore" => "restore galaxy codex from snapshot",
4967 "memory.consolidate" => "consolidate memories in codex",
4968 "memory.deduplicate" => "deduplicate memories in codex",
4969 "karma.purge" => "purge karma ledger",
4970 "system.flush" => "flush low importance memories",
4971 "galaxy.cold_rotate" => "rotate telemetry noise to cold storage",
4972 _ => tool_name.as_str(),
4973 };
4974 let result2 = wm
4975 .call(&mut ctx, json!({"thought": nl_phrase}))
4976 .await
4977 .unwrap();
4978
4979 let routed_tool2 = result2
4980 .get("_wm_route")
4981 .and_then(|r| r.get("tool"))
4982 .and_then(|t| t.as_str())
4983 .unwrap_or("");
4984 let resolved_destructive2 = registry
4985 .get(routed_tool2)
4986 .is_some_and(|t| t.effects().destructive);
4987 assert!(
4988 result2["status"] != "success" || !resolved_destructive2,
4989 "destructive tool '{tool_name}' executed via NLU phrase '{nl_phrase}' (resolved as '{routed_tool2}') — structural gate failed"
4990 );
4991 if routed_tool2 == tool_name {
4992 assert!(
4993 result2
4994 .get("message")
4995 .and_then(|m| m.as_str())
4996 .is_some_and(|m| m.contains("cannot be reached via natural language")),
4997 "destructive tool '{tool_name}' was routed to via '{nl_phrase}' but gate message missing: {result2}"
4998 );
4999 }
5000 }
5001 }
5002
5003 #[tokio::test]
5004 async fn nlu_abstention_returns_error_for_unmatched_query() {
5005 let store = test_store();
5006 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5007 let wm = registry.get("wm").unwrap();
5008 let mut ctx = Context::new(BrainWave::Gamma);
5009
5010 let result = wm
5013 .call(&mut ctx, json!({"thought": "xyzzy quux blargh frobnicate"}))
5014 .await
5015 .unwrap();
5016
5017 assert_eq!(result["status"], "error");
5018 assert!(
5019 result
5020 .get("_wm_route")
5021 .and_then(|r| r.get("abstained"))
5022 .and_then(serde_json::Value::as_bool)
5023 .unwrap_or(false),
5024 "expected abstained=true, got: {result}"
5025 );
5026 assert!(
5027 result["message"]
5028 .as_str()
5029 .unwrap()
5030 .contains("Could not confidently match"),
5031 "expected abstention message, got: {result}"
5032 );
5033 }
5034
5035 #[tokio::test]
5036 async fn nlu_abstention_does_not_fire_for_explicit_route() {
5037 let store = test_store();
5038 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5039 let wm = registry.get("wm").unwrap();
5040 let mut ctx = Context::new(BrainWave::Gamma);
5041
5042 let result = wm.call(&mut ctx, json!({"route": "gnosis"})).await.unwrap();
5045
5046 assert_eq!(result["status"], "success");
5047 assert!(
5048 !result
5049 .get("_wm_route")
5050 .and_then(|r| r.get("abstained"))
5051 .and_then(serde_json::Value::as_bool)
5052 .unwrap_or(false),
5053 "explicit route should not abstain, got: {result}"
5054 );
5055 }
5056
5057 struct FakeVecEmbedder;
5060
5061 impl wm_memory::Embedder for FakeVecEmbedder {
5062 fn embed_batch(&self, texts: &[&str]) -> wm_core::Result<Vec<Vec<f32>>> {
5063 Ok(texts
5064 .iter()
5065 .map(|t| {
5066 let mut v = vec![0.0_f32; 16];
5067 for (i, b) in t.bytes().take(16).enumerate() {
5068 v[i] = f32::from(b) / 255.0;
5069 }
5070 v
5071 })
5072 .collect())
5073 }
5074 fn dimension(&self) -> usize {
5075 16
5076 }
5077 fn is_available(&self) -> bool {
5078 true
5079 }
5080 fn backend_name(&self) -> &'static str {
5081 "fake"
5082 }
5083 }
5084
5085 #[tokio::test]
5086 async fn wm_classify_async_routes_off_thread_with_embedding_router() {
5087 let store = test_store();
5088 let registry = test_registry_with(&store);
5089 let shadow = std::sync::Arc::new(std::sync::RwLock::new(
5090 embedding_router::ShadowModeStats::default(),
5091 ));
5092 let router = embedding_router::EmbeddingRouter::with_descriptions(
5093 Box::new(FakeVecEmbedder),
5094 embedding_router::tool_descriptions(),
5095 )
5096 .expect("fake-embedder router should build");
5097 let mut meta = WmMetaTool::with_router_shadow_stats_and_pipeline(
5098 std::sync::Arc::new(registry),
5099 wm_memory::create_embedder(),
5100 shadow,
5101 None,
5102 );
5103 meta.embedding_router = Some(std::sync::Arc::new(router));
5104
5105 let (tool, conf, emb) = meta.classify_async("remember the meeting notes").await;
5108 assert!(!tool.is_empty());
5109 assert!(conf >= 0.0);
5110 assert!(
5111 emb.is_some(),
5112 "query embedding should be returned for OATS reuse"
5113 );
5114 }
5115
5116 #[tokio::test]
5117 async fn tools_list_shows_all() {
5118 let store = test_store();
5119 let registry = test_registry_with(&store);
5120 let registry = register_meta_tools(
5121 ®istry,
5122 &store,
5123 std::sync::Arc::new(std::sync::RwLock::new(
5124 embedding_router::ShadowModeStats::default(),
5125 )),
5126 );
5127
5128 let list = registry.get("tools.list").unwrap();
5129 let mut ctx = Context::new(BrainWave::Gamma);
5130 let result = list.call(&mut ctx, json!({})).await.unwrap();
5131
5132 assert_eq!(result["status"], "success");
5133 assert!(result["total"].as_u64().unwrap() >= 7);
5134 }
5135
5136 #[tokio::test]
5137 async fn tools_list_exposes_curated_argument_schemas() {
5138 let store = test_store();
5139 let registry = test_registry_with(&store);
5140 let registry = register_meta_tools(
5141 ®istry,
5142 &store,
5143 std::sync::Arc::new(std::sync::RwLock::new(
5144 embedding_router::ShadowModeStats::default(),
5145 )),
5146 );
5147
5148 let list = registry.get("tools.list").unwrap();
5149 let mut ctx = Context::new(BrainWave::Gamma);
5150 let result = list.call(&mut ctx, json!({})).await.unwrap();
5151
5152 let tools = result["tools"].as_array().unwrap();
5153 let create = tools
5154 .iter()
5155 .find(|t| t["name"] == "memory.create")
5156 .expect("tools.list must include memory.create");
5157 let schema = &create["input_schema"];
5158 assert_eq!(schema["type"], "object");
5159 assert!(
5160 schema["properties"].get("content").is_some(),
5161 "memory.create schema must describe content, got: {schema}"
5162 );
5163 assert!(
5164 schema["required"]
5165 .as_array()
5166 .unwrap()
5167 .iter()
5168 .any(|r| r == "content"),
5169 "memory.create schema must require content"
5170 );
5171
5172 let rollback = tools
5173 .iter()
5174 .find(|t| t["name"] == "transaction.rollback")
5175 .expect("tools.list must include transaction.rollback");
5176 assert!(
5177 rollback["input_schema"]["required"]
5178 .as_array()
5179 .unwrap()
5180 .iter()
5181 .any(|r| r == "confirm"),
5182 "transaction.rollback schema must require confirm"
5183 );
5184
5185 let annotations = &create["annotations"];
5187 assert_eq!(annotations["readOnlyHint"], false, "memory.create writes");
5188 assert_eq!(annotations["destructiveHint"], false);
5189 assert_eq!(
5190 rollback["annotations"]["destructiveHint"], true,
5191 "transaction.rollback is destructive"
5192 );
5193 let list_tool = tools
5194 .iter()
5195 .find(|t| t["name"] == "memory.list")
5196 .expect("tools.list must include memory.list");
5197 assert_eq!(
5198 list_tool["annotations"]["readOnlyHint"], true,
5199 "memory.list is read-only"
5200 );
5201 }
5202
5203 #[tokio::test]
5204 async fn tools_list_filters_by_brain_wave() {
5205 let store = test_store();
5206 let registry = test_registry_with(&store);
5207 let registry = register_meta_tools(
5208 ®istry,
5209 &store,
5210 std::sync::Arc::new(std::sync::RwLock::new(
5211 embedding_router::ShadowModeStats::default(),
5212 )),
5213 );
5214
5215 let list = registry.get("tools.list").unwrap();
5216
5217 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5219 let result_gamma = list.call(&mut ctx_gamma, json!({})).await.unwrap();
5220 let gamma_count = result_gamma["total"].as_u64().unwrap();
5221 assert!(gamma_count >= 7);
5222
5223 let mut ctx_alpha = Context::new(BrainWave::Alpha);
5225 let result_alpha = list.call(&mut ctx_alpha, json!({})).await.unwrap();
5226 let alpha_count = result_alpha["total"].as_u64().unwrap();
5227 assert!(alpha_count < gamma_count);
5228 assert!(alpha_count > 0);
5229
5230 let mut ctx_delta = Context::new(BrainWave::Delta);
5232 let result_delta = list.call(&mut ctx_delta, json!({})).await.unwrap();
5233 assert_eq!(result_delta["total"], 0);
5234 }
5235
5236 #[tokio::test]
5237 async fn gnosis_includes_brain_wave_and_tool_count() {
5238 let store = test_store();
5239 let registry = test_registry_with(&store);
5240 let registry = register_meta_tools(
5241 ®istry,
5242 &store,
5243 std::sync::Arc::new(std::sync::RwLock::new(
5244 embedding_router::ShadowModeStats::default(),
5245 )),
5246 );
5247
5248 let gnosis = registry.get("gnosis").unwrap();
5249 let mut ctx = Context::new(BrainWave::Gamma);
5250 let result = gnosis.call(&mut ctx, json!({})).await.unwrap();
5251
5252 assert_eq!(result["status"], "success");
5253 assert_eq!(result["brain_wave"], "Gamma");
5254 assert!(result["available_tools"].as_u64().unwrap() >= 9);
5255 }
5256
5257 #[tokio::test]
5258 async fn gnosis_available_tools_is_total_registered() {
5259 let store = test_store();
5260 let registry = test_registry_with(&store);
5261 let registry = register_meta_tools(
5262 ®istry,
5263 &store,
5264 std::sync::Arc::new(std::sync::RwLock::new(
5265 embedding_router::ShadowModeStats::default(),
5266 )),
5267 );
5268
5269 let gnosis = registry.get("gnosis").unwrap();
5270
5271 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5274 let result_gamma = gnosis.call(&mut ctx_gamma, json!({})).await.unwrap();
5275 let gamma_tools = result_gamma["available_tools"].as_u64().unwrap();
5276
5277 let mut ctx_delta = Context::new(BrainWave::Delta);
5278 let result_delta = gnosis.call(&mut ctx_delta, json!({})).await.unwrap();
5279 let delta_tools = result_delta["available_tools"].as_u64().unwrap();
5280
5281 assert_eq!(gamma_tools, delta_tools);
5282 assert!(
5283 gamma_tools >= 9,
5284 "expected at least 9 registered tools, got {gamma_tools}"
5285 );
5286 }
5287
5288 #[tokio::test]
5289 async fn expansion_brings_tool_count_to_50() {
5290 let store = test_store();
5291 let registry = test_registry_with(&store);
5292 let registry = register_meta_tools(
5293 ®istry,
5294 &store,
5295 std::sync::Arc::new(std::sync::RwLock::new(
5296 embedding_router::ShadowModeStats::default(),
5297 )),
5298 );
5299
5300 let list = registry.get("tools.list").unwrap();
5301 let mut ctx = Context::new(BrainWave::Gamma);
5302 let result = list.call(&mut ctx, json!({})).await.unwrap();
5303
5304 let total = result["total"].as_u64().unwrap();
5305 assert!(
5306 total >= 50,
5307 "Expected 50+ tools after expansion, got {total}"
5308 );
5309 }
5310
5311 #[tokio::test]
5314 async fn nlu_routes_consolidate() {
5315 let (tool, conf) = WmMetaTool::classify("consolidate memories in codex");
5316 assert_eq!(tool, "memory.consolidate");
5317 assert!(conf > 0.0);
5318 }
5319
5320 #[tokio::test]
5321 async fn nlu_routes_decay() {
5322 let (tool, conf) = WmMetaTool::classify("decay old memories");
5323 assert_eq!(tool, "memory.decay");
5324 assert!(conf > 0.0);
5325 }
5326
5327 #[tokio::test]
5328 async fn nlu_routes_batch_read() {
5329 let (tool, conf) = WmMetaTool::classify("batch read these memories");
5330 assert_eq!(tool, "memory.batch_read");
5331 assert!(conf > 0.0);
5332 }
5333
5334 #[tokio::test]
5335 async fn nlu_routes_update() {
5336 let (tool, conf) = WmMetaTool::classify("update memory tags");
5337 assert_eq!(tool, "memory.update");
5338 assert!(conf > 0.0);
5339 }
5340
5341 #[tokio::test]
5342 async fn nlu_routes_tag() {
5343 let (tool, conf) = WmMetaTool::classify("add tag to memory");
5344 assert_eq!(tool, "memory.tag");
5345 assert!(conf > 0.0);
5346 }
5347
5348 #[tokio::test]
5349 async fn nlu_routes_memory_stats() {
5350 let (tool, conf) = WmMetaTool::classify("memory stats for codex");
5351 assert_eq!(tool, "memory.stats");
5352 assert!(conf > 0.0);
5353 }
5354
5355 #[tokio::test]
5356 async fn nlu_routes_hybrid_recall() {
5357 let (tool, conf) = WmMetaTool::classify("hybrid recall for rust");
5358 assert_eq!(tool, "memory.hybrid_recall");
5359 assert!(conf > 0.0);
5360 }
5361
5362 #[tokio::test]
5363 async fn nlu_routes_count() {
5364 let (tool, conf) = WmMetaTool::classify("count memories in codex");
5365 assert_eq!(tool, "memory.count");
5366 assert!(conf > 0.0);
5367 }
5368
5369 #[tokio::test]
5370 async fn nlu_routes_tags() {
5371 let (tool, conf) = WmMetaTool::classify("list tags in codex");
5372 assert_eq!(tool, "memory.tags");
5373 assert!(conf > 0.0);
5374 }
5375
5376 #[tokio::test]
5377 async fn nlu_routes_associate_mine() {
5378 let (tool, conf) = WmMetaTool::classify("mine associations in codex");
5379 assert_eq!(tool, "memory.associate_mine");
5380 assert!(conf > 0.0);
5381 }
5382
5383 #[tokio::test]
5384 async fn nlu_routes_session_start() {
5385 let (tool, conf) = WmMetaTool::classify("start session research");
5386 assert_eq!(tool, "session.start");
5387 assert!(conf > 0.0);
5388 }
5389
5390 #[tokio::test]
5391 async fn nlu_routes_session_end() {
5392 let (tool, conf) = WmMetaTool::classify("end session 12345");
5393 assert_eq!(tool, "session.end");
5394 assert!(conf > 0.0);
5395 }
5396
5397 #[tokio::test]
5398 async fn nlu_routes_session_list() {
5399 let (tool, conf) = WmMetaTool::classify("list sessions");
5400 assert_eq!(tool, "session.list");
5401 assert!(conf > 0.0);
5402 }
5403
5404 #[tokio::test]
5405 async fn nlu_routes_citta_status() {
5406 let (tool, conf) = WmMetaTool::classify("citta status");
5407 assert_eq!(tool, "citta.status");
5408 assert!(conf > 0.0);
5409 }
5410
5411 #[tokio::test]
5412 async fn nlu_routes_citta_reflect() {
5413 let (tool, conf) = WmMetaTool::classify("reflect on recent events");
5414 assert_eq!(tool, "citta.reflect");
5415 assert!(conf > 0.0);
5416 }
5417
5418 #[tokio::test]
5419 async fn nlu_routes_coherence() {
5420 let (tool, conf) = WmMetaTool::classify("check coherence");
5421 assert_eq!(tool, "citta.coherence");
5422 assert!(conf > 0.0);
5423 }
5424
5425 #[tokio::test]
5426 async fn nlu_routes_dream_status() {
5427 let (tool, conf) = WmMetaTool::classify("dream cycle status");
5428 assert_eq!(tool, "dream.status");
5429 assert!(conf > 0.0);
5430 }
5431
5432 #[tokio::test]
5433 async fn nlu_routes_dream_trigger() {
5434 let (tool, conf) = WmMetaTool::classify("trigger dream cycle");
5435 assert_eq!(tool, "dream.trigger");
5436 assert!(conf > 0.0);
5437 }
5438
5439 #[tokio::test]
5440 async fn nlu_routes_effectiveness() {
5441 let (tool, conf) = WmMetaTool::classify("tool effectiveness report");
5442 assert_eq!(tool, "tools.effectiveness_report");
5443 assert!(conf > 0.0);
5444 }
5445
5446 #[tokio::test]
5447 async fn nlu_routes_retire() {
5448 let (tool, conf) = WmMetaTool::classify("retire tool memory.old");
5449 assert_eq!(tool, "tools.retire");
5450 assert!(conf > 0.0);
5451 }
5452
5453 #[tokio::test]
5454 async fn nlu_routes_pattern_search() {
5455 let (tool, conf) = WmMetaTool::classify("pattern search for rust");
5456 assert_eq!(tool, "pattern.search");
5457 assert!(conf > 0.0);
5458 }
5459
5460 #[tokio::test]
5461 async fn nlu_routes_salience() {
5462 let (tool, conf) = WmMetaTool::classify("salience spotlight");
5463 assert_eq!(tool, "salience.spotlight");
5464 assert!(conf > 0.0);
5465 }
5466
5467 #[tokio::test]
5468 async fn nlu_routes_serendipity() {
5469 let (tool, conf) = WmMetaTool::classify("serendipity surface");
5470 assert_eq!(tool, "serendipity.surface");
5471 assert!(conf > 0.0);
5472 }
5473
5474 #[tokio::test]
5475 async fn nlu_routes_constellation_detect() {
5476 let (tool, conf) = WmMetaTool::classify("detect clusters");
5477 assert_eq!(tool, "constellation.detect");
5478 assert!(conf > 0.0);
5479 }
5480
5481 #[tokio::test]
5482 async fn nlu_routes_constellation_list() {
5483 let (tool, conf) = WmMetaTool::classify("list constellations");
5484 assert_eq!(tool, "constellation.list");
5485 assert!(conf > 0.0);
5486 }
5487
5488 #[tokio::test]
5489 async fn nlu_routes_galaxy_stats() {
5490 let (tool, conf) = WmMetaTool::classify("galaxy stats");
5491 assert_eq!(tool, "galaxy.stats");
5492 assert!(conf > 0.0);
5493 }
5494
5495 #[tokio::test]
5496 async fn nlu_routes_galaxy_export() {
5497 let (tool, conf) = WmMetaTool::classify("export galaxy codex");
5498 assert_eq!(tool, "galaxy.export");
5499 assert!(conf > 0.0);
5500 }
5501
5502 #[tokio::test]
5503 async fn nlu_routes_galaxy_import() {
5504 let (tool, conf) = WmMetaTool::classify("import galaxy codex");
5505 assert_eq!(tool, "galaxy.import");
5506 assert!(conf > 0.0);
5507 }
5508
5509 #[tokio::test]
5510 async fn nlu_routes_karma_history() {
5511 let (tool, conf) = WmMetaTool::classify("karma history");
5512 assert_eq!(tool, "karma.history");
5513 assert!(conf > 0.0);
5514 }
5515
5516 #[tokio::test]
5517 async fn nlu_routes_karma_clear() {
5518 let (tool, conf) = WmMetaTool::classify("clear karma");
5519 assert_eq!(tool, "karma.clear");
5520 assert!(conf > 0.0);
5521 }
5522
5523 #[tokio::test]
5524 async fn nlu_routes_dharma_rules() {
5525 let (tool, conf) = WmMetaTool::classify("dharma rules");
5526 assert_eq!(tool, "dharma.rules");
5527 assert!(conf > 0.0);
5528 }
5529
5530 #[tokio::test]
5531 async fn nlu_routes_dharma_audit() {
5532 let (tool, conf) = WmMetaTool::classify("dharma audit");
5533 assert_eq!(tool, "dharma.audit");
5534 assert!(conf > 0.0);
5535 }
5536
5537 #[tokio::test]
5538 async fn nlu_routes_dharma_profiles() {
5539 let (tool, conf) = WmMetaTool::classify("dharma profiles");
5540 assert_eq!(tool, "dharma.profiles");
5541 assert!(conf > 0.0);
5542 }
5543
5544 #[tokio::test]
5545 async fn nlu_routes_agent_register() {
5546 let (tool, conf) = WmMetaTool::classify("register agent worker-1");
5547 assert_eq!(tool, "agent.register");
5548 assert!(conf > 0.0);
5549 }
5550
5551 #[tokio::test]
5552 async fn nlu_routes_agent_list() {
5553 let (tool, conf) = WmMetaTool::classify("list agents");
5554 assert_eq!(tool, "agent.list");
5555 assert!(conf > 0.0);
5556 }
5557
5558 #[tokio::test]
5559 async fn nlu_routes_agent_heartbeat() {
5560 let (tool, conf) = WmMetaTool::classify("heartbeat for agent");
5561 assert_eq!(tool, "agent.heartbeat");
5562 assert!(conf > 0.0);
5563 }
5564
5565 #[tokio::test]
5566 async fn nlu_routes_task_distribute() {
5567 let (tool, conf) = WmMetaTool::classify("distribute task analyze data");
5568 assert_eq!(tool, "task.distribute");
5569 assert!(conf > 0.0);
5570 }
5571
5572 #[tokio::test]
5573 async fn nlu_routes_task_status() {
5574 let (tool, conf) = WmMetaTool::classify("task status");
5575 assert_eq!(tool, "task.status");
5576 assert!(conf > 0.0);
5577 }
5578
5579 #[tokio::test]
5580 async fn nlu_routes_system_health() {
5581 let (tool, conf) = WmMetaTool::classify("system health check");
5582 assert_eq!(tool, "system.health");
5583 assert!(conf > 0.0);
5584 }
5585
5586 #[tokio::test]
5587 async fn nlu_routes_system_config() {
5588 let (tool, conf) = WmMetaTool::classify("system config");
5589 assert_eq!(tool, "system.config");
5590 assert!(conf > 0.0);
5591 }
5592
5593 #[tokio::test]
5594 async fn nlu_routes_system_flush() {
5595 let (tool, conf) = WmMetaTool::classify("flush old memories");
5596 assert_eq!(tool, "system.flush");
5597 assert!(conf > 0.0);
5598 }
5599
5600 #[tokio::test]
5601 async fn nlu_routes_memory_nearby() {
5602 let (tool, conf) = WmMetaTool::classify("nearby memories in codex");
5603 assert_eq!(tool, "memory.nearby");
5604 assert!(conf > 0.0);
5605 }
5606
5607 #[tokio::test]
5608 async fn nlu_routes_empty_to_gnosis() {
5609 let (tool, conf) = WmMetaTool::classify("");
5610 assert_eq!(tool, "gnosis");
5611 assert_eq!(conf, 0.0);
5612 }
5613
5614 #[tokio::test]
5615 async fn nlu_routes_unknown_to_gnosis() {
5616 let (tool, conf) = WmMetaTool::classify("xyzzy frobnicate");
5617 assert_eq!(tool, "gnosis");
5618 assert_eq!(conf, 0.0);
5619 }
5620
5621 #[tokio::test]
5622 async fn nlu_extract_payload_memory_search() {
5623 let (param, value) =
5624 WmMetaTool::extract_payload("search for rust patterns", "memory.search").unwrap();
5625 assert_eq!(param, "query");
5626 assert_eq!(value, "rust patterns");
5627 }
5628
5629 #[tokio::test]
5630 async fn nlu_extract_payload_session_start() {
5631 let (param, value) =
5635 WmMetaTool::extract_payload("start session research", "session.start").unwrap();
5636 assert_eq!(param, "title");
5637 assert_eq!(value, "research");
5638 }
5639
5640 #[tokio::test]
5641 async fn nlu_extract_payload_agent_register() {
5642 let (param, value) =
5643 WmMetaTool::extract_payload("register agent worker-1", "agent.register").unwrap();
5644 assert_eq!(param, "name");
5645 assert_eq!(value, "worker-1");
5646 }
5647
5648 #[tokio::test]
5649 async fn nlu_extract_payload_task_distribute() {
5650 let (param, value) =
5651 WmMetaTool::extract_payload("distribute task analyze data", "task.distribute").unwrap();
5652 assert_eq!(param, "task");
5653 assert_eq!(value, "analyze data");
5654 }
5655
5656 #[tokio::test]
5657 async fn nlu_count_unique_patterns() {
5658 let inputs = [
5660 "remember",
5661 "recall",
5662 "list memories",
5663 "delete memory",
5664 "search",
5665 "query",
5666 "associate",
5667 "associations",
5668 "consolidate",
5669 "decay",
5670 "batch read",
5671 "update memory",
5672 "tag memory",
5673 "memory stats",
5674 "hybrid recall",
5675 "count memories",
5676 "list tags",
5677 "mine associations",
5678 "start session",
5679 "checkpoint",
5680 "recall session",
5681 "end session",
5682 "list sessions",
5683 "citta status",
5684 "reflect",
5685 "coherence",
5686 "dream status",
5687 "trigger dream",
5688 "effectiveness",
5689 "retire tool",
5690 "pattern search",
5691 "salience",
5692 "serendipity",
5693 "detect clusters",
5694 "list constellations",
5695 "galaxy stats",
5696 "export galaxy",
5697 "import galaxy",
5698 "karma",
5699 "karma history",
5700 "clear karma",
5701 "dharma rules",
5702 "dharma audit",
5703 "dharma profiles",
5704 "dharma",
5705 "register agent",
5706 "list agents",
5707 "heartbeat",
5708 "distribute task",
5709 "task status",
5710 "system health",
5711 "system config",
5712 "flush",
5713 "tools",
5714 "nearby memories",
5715 ];
5716 let mut tools: std::collections::HashSet<&str> = std::collections::HashSet::new();
5717 for input in &inputs {
5718 let (tool, _) = WmMetaTool::classify(input);
5719 tools.insert(tool);
5720 }
5721 assert!(
5723 tools.len() >= 30,
5724 "Expected 30+ unique NLU targets, got {}",
5725 tools.len()
5726 );
5727 }
5728
5729 #[tokio::test]
5730 async fn nlu_routes_shadow_report() {
5731 let (tool, conf) = WmMetaTool::classify("shadow mode disagreement report");
5732 assert_eq!(tool, "nlu.shadow_report");
5733 assert!(conf > 0.0);
5734 }
5735
5736 #[tokio::test]
5737 async fn nlu_routes_oats_report() {
5738 let (tool, conf) = WmMetaTool::classify("oats disagreement nlu router");
5739 assert_eq!(tool, "nlu.shadow_report");
5740 assert!(conf > 0.0);
5741 }
5742
5743 #[test]
5746 fn glyph_roundtrip_known_codes() {
5747 let raw = json!({"route": "memory.search", "args": {"query": "x", "limit": 3}});
5748 let encoded = encode_glyph("memory.search", &json!({"query": "x", "limit": 3}));
5749 assert_eq!(encoded["r"], "Ms");
5750 assert_eq!(encoded["a"]["q"], "x");
5751 assert_eq!(encoded["a"]["n"], 3);
5752 let decoded = decode_glyph(&encoded).expect("glyph input must decode");
5753 assert_eq!(decoded["route"], raw["route"]);
5754 assert_eq!(decoded["args"]["query"], "x");
5755 assert_eq!(decoded["args"]["limit"], 3);
5756 }
5757
5758 #[test]
5759 fn glyph_unknown_codes_pass_through() {
5760 let weird = json!({"r": "not-a-code", "a": {"zzz": 1}});
5761 assert!(decode_glyph(&weird).is_none(), "unknown route code refuses");
5762 let partial = json!({"r": "Ms", "a": {"zzz": 1}});
5763 let decoded = decode_glyph(&partial).expect("known route decodes");
5764 assert_eq!(decoded["args"]["zzz"], 1, "unknown arg code passes through");
5765 assert_eq!(decode_glyph(&json!({"thought": "hi"})), None);
5766 }
5767
5768 #[test]
5769 fn glyph_book_covers_measured_routes() {
5770 for route in [
5772 "memory.search",
5773 "memory.create",
5774 "session.record",
5775 "session.continuity",
5776 "dharma.escalate",
5777 "graph.walk",
5778 "tools.list",
5779 "citta.status",
5780 ] {
5781 assert!(
5782 glyph_lookup(GLYPH_ROUTES, route).is_some(),
5783 "missing {route}"
5784 );
5785 }
5786 }
5787
5788 #[test]
5789 fn glyph_logographic_ideograms_decode_losslessly() {
5790 let search_call = json!({
5792 "r": "忆",
5793 "a": {
5794 "问": "auth failure",
5795 "数": 5
5796 }
5797 });
5798 let decoded = decode_glyph(&search_call).expect("logographic search decodes");
5799 assert_eq!(decoded["route"], "memory.search");
5800 assert_eq!(decoded["args"]["query"], "auth failure");
5801 assert_eq!(decoded["args"]["limit"], 5);
5802
5803 let checkpoint_call = json!({
5804 "r": "契",
5805 "a": {
5806 "文": "v9.3 milestone reached"
5807 }
5808 });
5809 let decoded_cp = decode_glyph(&checkpoint_call).expect("checkpoint decodes");
5810 assert_eq!(decoded_cp["route"], "session.checkpoint");
5811 assert_eq!(decoded_cp["args"]["content"], "v9.3 milestone reached");
5812
5813 let status_call = json!({"r": "心", "a": {}});
5814 let decoded_st = decode_glyph(&status_call).expect("citta status decodes");
5815 assert_eq!(decoded_st["route"], "citta.status");
5816 }
5817
5818 #[test]
5819 fn lkep_expression_decodes_and_normalizes() {
5820 let (route, args) =
5822 decode_lkep(&json!("忆(问=\"deadlock\", 数=3)")).expect("LKEP string decodes");
5823 assert_eq!(route, "memory.search");
5824 assert_eq!(args["query"], "deadlock");
5825 assert_eq!(args["limit"], 3);
5826
5827 let (route2, args2) =
5829 decode_lkep(&json!("忆: memory corruption")).expect("colon syntax decodes");
5830 assert_eq!(route2, "memory.search");
5831 assert_eq!(args2["query"], "memory corruption");
5832
5833 let (route3, args3) = decode_lkep(&json!("律")).expect("bare route decodes");
5835 assert_eq!(route3, "dharma.rules");
5836 assert_eq!(args3, json!({}));
5837
5838 let (route4, args4) =
5840 decode_lkep(&json!({"忆": "fast lookup"})).expect("root ideogram decodes");
5841 assert_eq!(route4, "memory.search");
5842 assert_eq!(args4["query"], "fast lookup");
5843 }
5844}