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