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(route='memory.read', args={\"id\": \"<uuid>\"}). To search by content instead, use wm(thought='find <text>') or wm(route='memory.search', args={\"query\": \"...\"}). To list memories, use wm(route='memory.list', args={\"galaxy\": \"codex\", \"limit\": 10})".into(),
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(route='memory.vector.search', args={\"memory_id\": \"<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 let mut text: &str = thought;
3171 if let Some((phrase, _, _)) = crate::nlu::PHRASE_ROUTES
3172 .iter()
3173 .find(|(phrase, tool, _)| *tool == "memory.search" && lower.starts_with(phrase))
3174 {
3175 text = &thought[phrase.len()..];
3176 } else if lower.starts_with("search for ") {
3177 text = &thought["search for ".len()..];
3178 } else if lower.starts_with("search ") {
3179 text = &thought["search ".len()..];
3180 } else {
3181 for (verb, tool, _) in crate::nlu::PREFIX_ROUTES {
3182 if *tool != "memory.search" {
3183 continue;
3184 }
3185 if let Some(rest) = lower.strip_prefix(verb) {
3186 if rest.is_empty() || rest.starts_with(' ') || rest.starts_with(':') {
3187 text = thought[verb.len()..].trim_start_matches([' ', ':']);
3188 break;
3189 }
3190 }
3191 }
3192 }
3193 let lower_text = text.to_lowercase();
3196 for filler in ["memory for ", "memories for ", "memory ", "memories "] {
3197 if lower_text.starts_with(filler) {
3198 text = &text[filler.len()..];
3199 break;
3200 }
3201 }
3202 let query = text
3203 .trim()
3204 .trim_end_matches(['?', '!'])
3205 .trim()
3206 .trim_end_matches(" in memory")
3207 .trim();
3208 if !query.is_empty() {
3209 return Some(("query".into(), query.to_string()));
3210 }
3211 }
3212 "memory.chat" => {
3213 for prefix in &[
3214 "chat about ",
3215 "chat ",
3216 "ask about ",
3217 "ask ",
3218 "discuss ",
3219 "explore ",
3220 "converse about ",
3221 ] {
3222 if lower.starts_with(prefix) {
3223 let query = thought[prefix.len()..].trim().to_string();
3224 if !query.is_empty() {
3225 return Some(("query".into(), query));
3226 }
3227 }
3228 }
3229 if !thought.is_empty() {
3230 return Some(("query".into(), thought.to_string()));
3231 }
3232 }
3233 "memory.vector.search" => {
3234 for prefix in &[
3235 "find similar to ",
3236 "similar to memory ",
3237 "vector search ",
3238 "semantic search ",
3239 "embedding search ",
3240 ] {
3241 if lower.starts_with(prefix) {
3242 let id = thought[prefix.len()..].trim().to_string();
3243 if !id.is_empty() {
3244 return Some(("memory_id".into(), id));
3245 }
3246 }
3247 }
3248 }
3249 "memory.count" => {
3250 for prefix in &[
3251 "count memories in ",
3252 "how many memories in ",
3253 "memory count ",
3254 ] {
3255 if lower.starts_with(prefix) {
3256 let galaxy = thought[prefix.len()..].trim().to_string();
3257 if !galaxy.is_empty() {
3258 return Some(("galaxy".into(), galaxy));
3259 }
3260 }
3261 }
3262 }
3263 "session.start" => {
3264 for prefix in &["start session ", "new session ", "begin session "] {
3265 if lower.starts_with(prefix) {
3266 let title = thought[prefix.len()..].trim().to_string();
3267 if !title.is_empty() {
3268 return Some(("title".into(), title));
3272 }
3273 }
3274 }
3275 }
3276 "session.end" => {
3277 for prefix in &["end session ", "close session ", "stop session "] {
3278 if lower.starts_with(prefix) {
3279 let id = thought[prefix.len()..].trim().to_string();
3280 if !id.is_empty() {
3281 return Some(("session_id".into(), id));
3282 }
3283 }
3284 }
3285 }
3286 "agent.register" => {
3287 for prefix in &[
3288 "register agent ",
3289 "new agent ",
3290 "create agent ",
3291 "add agent ",
3292 ] {
3293 if lower.starts_with(prefix) {
3294 let name = thought[prefix.len()..].trim().to_string();
3295 if !name.is_empty() {
3296 return Some(("name".into(), name));
3297 }
3298 }
3299 }
3300 }
3301 "agent.trust"
3302 | "agent.descriptions"
3303 | "agent.capabilities"
3304 | "agent.heartbeat.history"
3305 | "agent.deregister" => {
3306 for prefix in &[
3307 "trust agent ",
3308 "describe agent ",
3309 "capabilities agent ",
3310 "heartbeat history agent ",
3311 "deregister agent ",
3312 "unregister agent ",
3313 "remove agent ",
3314 ] {
3315 if lower.starts_with(prefix) {
3316 let id = thought[prefix.len()..].trim().to_string();
3317 if !id.is_empty() {
3318 return Some(("agent_id".into(), id));
3319 }
3320 }
3321 }
3322 }
3323 "galaxy.purge" => {
3324 for prefix in &["purge galaxy ", "wipe galaxy ", "clear galaxy "] {
3325 if lower.starts_with(prefix) {
3326 let galaxy = thought[prefix.len()..].trim().to_string();
3327 if !galaxy.is_empty() {
3328 return Some(("galaxy".into(), galaxy));
3329 }
3330 }
3331 }
3332 }
3333 "task.distribute" => {
3334 for prefix in &["distribute task ", "assign task ", "dispatch task "] {
3335 if lower.starts_with(prefix) {
3336 let task = thought[prefix.len()..].trim().to_string();
3337 if !task.is_empty() {
3338 return Some(("task".into(), task));
3339 }
3340 }
3341 }
3342 }
3343 "memory.sort" => {
3344 for prefix in &["sort memories ", "sort memory ", "order memories "] {
3345 if lower.starts_with(prefix) {
3346 let galaxy = thought[prefix.len()..].trim().to_string();
3347 if !galaxy.is_empty() {
3348 return Some(("galaxy".into(), galaxy));
3349 }
3350 }
3351 }
3352 }
3353 "memory.filter" => {
3354 for prefix in &["filter memories ", "filter memory "] {
3355 if lower.starts_with(prefix) {
3356 let galaxy = thought[prefix.len()..].trim().to_string();
3357 if !galaxy.is_empty() {
3358 return Some(("galaxy".into(), galaxy));
3359 }
3360 }
3361 }
3362 }
3363 "memory.deduplicate" => {
3364 for prefix in &[
3365 "deduplicate memories ",
3366 "deduplicate memory ",
3367 "dedup memories ",
3368 ] {
3369 if lower.starts_with(prefix) {
3370 let galaxy = thought[prefix.len()..].trim().to_string();
3371 if !galaxy.is_empty() {
3372 return Some(("galaxy".into(), galaxy));
3373 }
3374 }
3375 }
3376 }
3377 "memory.export" => {
3378 for prefix in &["export memories ", "export memory "] {
3379 if lower.starts_with(prefix) {
3380 let galaxy = thought[prefix.len()..].trim().to_string();
3381 if !galaxy.is_empty() {
3382 return Some(("galaxy".into(), galaxy));
3383 }
3384 }
3385 }
3386 }
3387 "speculative.decode" => {
3388 for prefix in &[
3389 "speculative decode ",
3390 "speculative ",
3391 "decode ",
3392 "draft and verify ",
3393 "accelerate inference ",
3394 ] {
3395 if lower.starts_with(prefix) {
3396 let prompt = thought[prefix.len()..].trim().to_string();
3397 if !prompt.is_empty() {
3398 return Some(("prompt".into(), prompt));
3399 }
3400 }
3401 }
3402 }
3403 "meta.enhance" => {
3404 for prefix in &[
3405 "enhance ",
3406 "enhance prompt ",
3407 "grounded inference ",
3408 "self-correct ",
3409 "meta enhance ",
3410 "cognitive enhance ",
3411 "augment ",
3412 ] {
3413 if lower.starts_with(prefix) {
3414 let prompt = thought[prefix.len()..].trim().to_string();
3415 if !prompt.is_empty() {
3416 return Some(("prompt".into(), prompt));
3417 }
3418 }
3419 }
3420 }
3421 "dense.encode" => {
3422 for prefix in &["dense encode ", "compress ", "encode ", "compact "] {
3423 if lower.starts_with(prefix) {
3424 let text = thought[prefix.len()..].trim().to_string();
3425 if !text.is_empty() {
3426 return Some(("text".into(), text));
3427 }
3428 }
3429 }
3430 }
3431 "dream.trigger" => {
3432 for prefix in &[
3433 "dream trigger ",
3434 "trigger dream ",
3435 "start dream ",
3436 "force dream ",
3437 "initiate dream ",
3438 ] {
3439 if lower.starts_with(prefix) {
3440 let rest = thought[prefix.len()..].trim();
3441 if !rest.is_empty() {
3442 return Some(("force".into(), rest.to_string()));
3443 }
3444 }
3445 }
3446 }
3447 _ => {}
3448 }
3449 None
3450 }
3451}
3452
3453#[async_trait]
3454impl Tool for WmMetaTool {
3455 fn input_schema(&self) -> Value {
3456 schema(
3457 &json!({
3458 "route": str_prop("Explicit canonical route, e.g. \"memory.search\" (preferred for agents)"),
3459 "thought": str_prop("Natural-language convenience routing (least reliable; prefer route)"),
3460 "args": json!({"type": "object", "description": "Arguments passed through to the target tool"}),
3461 }),
3462 &[],
3463 )
3464 }
3465 fn name(&self) -> &str {
3466 "wm"
3467 }
3468 fn gana(&self) -> Gana {
3469 Gana::Horn
3470 }
3471 fn effects(&self) -> &EffectRow {
3472 &self.effects
3473 }
3474 async fn call(&self, ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
3475 let thought = args.get("thought").and_then(|v| v.as_str()).unwrap_or("");
3476 let (route, passthrough_args) = if glyph_mode_from_env() {
3482 if let Some((r, a)) = decode_lkep(&args) {
3483 (Some(r), a)
3484 } else if let Some(Value::Object(map)) = decode_glyph(&args) {
3485 (
3486 map.get("route").and_then(Value::as_str).map(String::from),
3487 map.get("args").cloned().unwrap_or(Value::Null),
3488 )
3489 } else {
3490 let r = args
3491 .get("route")
3492 .and_then(Value::as_str)
3493 .map(|s| resolve_route(s).unwrap_or(s).to_string());
3494 let a = args.get("args").cloned().unwrap_or(Value::Null);
3495 (r, a)
3496 }
3497 } else {
3498 (
3499 args.get("route").and_then(Value::as_str).map(|s| {
3500 expansion::common::canonical_tool_alias(s)
3501 .unwrap_or(s)
3502 .to_string()
3503 }),
3504 args.get("args").cloned().unwrap_or(Value::Null),
3505 )
3506 };
3507 let route = route.as_deref();
3508
3509 if thought.is_empty() && route.is_none() {
3510 let received: Vec<String> = args
3516 .as_object()
3517 .map(|o| o.keys().cloned().collect())
3518 .unwrap_or_default();
3519 let detail = if received.is_empty() {
3520 String::new()
3521 } else {
3522 format!("; received argument keys: {received:?}")
3523 };
3524 return Ok(json!({
3525 "status": "error",
3526 "message": format!(
3527 "Either 'thought' (natural language) or 'route' (explicit) is required{detail}"
3528 ),
3529 "hint": "wm(thought='remember that X is Y') or wm(route='memory.create', args={\"content\": \"...\"})"
3530 }));
3531 }
3532
3533 let (tool_name, confidence, query_emb) = if let Some(r) = route {
3535 (r.to_string(), 1.0, None)
3536 } else {
3537 self.classify_async(thought).await
3538 };
3539
3540 if route.is_none() && tool_name == "gnosis" && confidence < NLU_ABSTENTION_THRESHOLD {
3545 return Ok(json!({
3546 "status": "error",
3547 "message": "Could not confidently match your request to a tool.",
3548 "confidence": confidence,
3549 "hint": "Use explicit routing: wm(route='tool.name', args={...}). Use wm(route='tools.list') to see available tools.",
3550 "_wm_route": { "tool": tool_name, "confidence": confidence, "abstained": true }
3551 }));
3552 }
3553
3554 let mut tool_args = if passthrough_args.is_object() {
3556 let mut args = passthrough_args;
3560 if let Some(obj) = args.as_object_mut() {
3561 obj.remove("_meta");
3562 }
3563 args
3564 } else {
3565 Value::Null
3566 };
3567
3568 if route.is_none() && !thought.is_empty() && tool_args.is_null() {
3570 if let Some((param, value)) = Self::extract_payload(thought, &tool_name) {
3571 tool_args = json!({ param: value });
3572 }
3573 }
3574
3575 let tool = self.registry.get(&tool_name);
3577 match tool {
3578 Some(t) => {
3579 if route.is_none() && t.effects().destructive {
3586 return Ok(json!({
3587 "status": "error",
3588 "message": format!(
3589 "tool '{tool_name}' is destructive and cannot be reached via natural language — use wm(route='{tool_name}', args={{...}}) with \"confirm\": true"
3590 ),
3591 "_wm_route": { "tool": tool_name, "confidence": confidence },
3592 }));
3593 }
3594
3595 if let Some(required) = Self::required_arg(&tool_name) {
3597 let has_arg = tool_args.is_object()
3598 && tool_args.get(required).is_some()
3599 && !tool_args
3600 .get(required)
3601 .is_some_and(serde_json::Value::is_null);
3602 if !has_arg {
3603 return Ok(json!({
3604 "status": "error",
3605 "message": format!("Missing required argument: '{required}' for tool '{tool_name}'"),
3606 "hint": Self::missing_arg_hint(&tool_name, required),
3607 "_wm_route": { "tool": tool_name, "confidence": confidence },
3608 }));
3609 }
3610 }
3611
3612 let result = match &self.pipeline {
3618 Some(p) => p.dispatch(t.as_ref(), ctx, tool_args).await,
3619 None => t.call(ctx, tool_args).await,
3620 };
3621 if let Some(ref router) = self.embedding_router {
3628 let success = result.is_ok();
3629 if let Some(emb) = &query_emb {
3630 router.record_outcome_with_embedding(&tool_name, thought, success, emb);
3631 } else {
3632 let router = Arc::clone(router);
3633 let tool_name_owned = tool_name.clone();
3634 let thought_owned = thought.to_string();
3635 tokio::task::spawn_blocking(move || {
3636 router.record_outcome(&tool_name_owned, &thought_owned, success);
3637 });
3638 }
3639 }
3640 match result {
3641 Ok(mut output) => {
3642 if let Value::Object(ref mut map) = output {
3644 map.insert(
3645 "_wm_route".into(),
3646 json!({
3647 "input": thought.chars().take(200).collect::<String>(),
3648 "tool": tool_name,
3649 "confidence": confidence,
3650 }),
3651 );
3652 }
3653 Ok(output)
3654 }
3655 Err(e) => Ok(json!({
3656 "status": "error",
3657 "error": e.to_string(),
3658 "_wm_route": { "tool": tool_name, "confidence": confidence },
3659 })),
3660 }
3661 }
3662 None => Ok(json!({
3663 "status": "error",
3664 "message": format!("Unknown tool: '{tool_name}'"),
3665 "_wm_route": { "tool": tool_name, "confidence": confidence },
3666 })),
3667 }
3668 }
3669 fn stats(&self) -> &ToolStats {
3670 &self.stats
3671 }
3672}
3673
3674#[must_use]
3682pub fn required_arg_for(tool_name: &str) -> Option<&'static str> {
3683 WmMetaTool::required_arg(tool_name)
3684}
3685
3686fn parse_galaxy(s: &str) -> wm_core::Result<Galaxy> {
3688 expansion::common::parse_galaxy(s)
3689}
3690
3691#[allow(clippy::too_many_arguments)]
3698pub fn register_all(
3699 registry: &ToolRegistry,
3700 store: &Arc<MemoryStore>,
3701 search: Option<Arc<SearchEngine>>,
3702 karma: Option<Arc<KarmaLedger>>,
3703 dharma: &Option<Arc<DharmaGate>>,
3704 substrate: Option<Arc<SubstrateMonitor>>,
3705 resource_rules: &Option<Arc<ResourceRules>>,
3706 associations: Arc<AssociationStore>,
3707 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
3708 vector_store: Arc<std::sync::Mutex<VectorStore>>,
3709 conversational: Option<ConversationalSearch>,
3710 recall: Option<Arc<RecallEngine>>,
3711 homeostatic_loop: Option<Arc<std::sync::Mutex<HomeostaticLoop>>>,
3712 anomaly_detector: Option<Arc<std::sync::Mutex<AnomalyDetector>>>,
3713 sensorimotor_bus: Option<Arc<std::sync::Mutex<SensorimotorBus>>>,
3714 reflex_loop: Option<Arc<std::sync::Mutex<ReflexLoop>>>,
3715 gan_ying_bus: Option<&Arc<std::sync::Mutex<GanYingBus>>>,
3716 transaction_state: expansion::TransactionState,
3717 escalation_queue: Option<&Arc<std::sync::Mutex<wm_governance::EscalationQueue>>>,
3718 firewall: Option<&Arc<expansion::firewall::TxFirewall>>,
3719 code_graph: Option<&Arc<std::sync::Mutex<expansion::code::CodeGraph>>>,
3720 registry_persistence: expansion::RegistryPersistenceMode,
3721 circuit_breakers: Arc<wm_dispatch::CircuitBreakerRegistry>,
3722) -> ToolRegistry {
3723 let reg = registry
3724 .register(Arc::new(MemoryCreateTool::new(
3725 store.clone(),
3726 search.clone(),
3727 recall.clone(),
3728 )))
3729 .register(Arc::new(MemoryBatchCreateTool::new(
3730 store.clone(),
3731 search.clone(),
3732 recall.clone(),
3733 )))
3734 .register(Arc::new(MemoryReadTool::new(store.clone())))
3735 .register(Arc::new(MemoryListTool::new(store.clone())))
3736 .register(Arc::new(MemoryDeleteTool::new(
3737 store.clone(),
3738 search.clone(),
3739 )))
3740 .register(Arc::new(MemoryBatchDeleteTool::new(
3741 store.clone(),
3742 search.clone(),
3743 )))
3744 .register(Arc::new(MemoryQueryTool::new(store.clone())))
3745 .register(Arc::new(MemoryAssociateTool::new(store.clone())))
3746 .register(Arc::new(MemoryAssociationsTool::new(store.clone())))
3747 .register(Arc::new(MemoryVectorSearchTool::new(
3748 store.clone(),
3749 vector_store,
3750 )))
3751 .register(Arc::new(GnosisTool::new(store.clone())))
3752 .register(Arc::new(expansion::MemoryReembedTool::new(recall.clone())));
3754
3755 let mut reg = expansion::breaker_tools::register_breakers(®, circuit_breakers);
3758
3759 if let Some(conv) = conversational {
3760 reg = reg.register(Arc::new(MemoryChatTool::new(conv)));
3761 }
3762
3763 if let Some(s) = search {
3764 reg = reg.register(Arc::new(
3768 expansion::MemoryHybridRecallTool::as_search(
3769 store.clone(),
3770 Some(s.clone()),
3771 recall.clone(),
3772 )
3773 .with_associations(Some(associations.clone())),
3774 ));
3775 reg = expansion::register_expansion(
3777 ®,
3778 store,
3779 Some(s),
3780 recall,
3781 associations,
3782 spiral_tracker,
3783 karma.clone(),
3784 substrate.clone(),
3785 homeostatic_loop,
3786 anomaly_detector,
3787 sensorimotor_bus,
3788 reflex_loop,
3789 gan_ying_bus,
3790 transaction_state,
3791 resource_rules.as_ref(),
3792 escalation_queue,
3793 dharma.as_ref(),
3794 firewall,
3795 code_graph,
3796 registry_persistence,
3797 );
3798 } else {
3799 reg = expansion::register_expansion(
3800 ®,
3801 store,
3802 None,
3803 recall,
3804 associations,
3805 spiral_tracker,
3806 karma.clone(),
3807 substrate.clone(),
3808 homeostatic_loop,
3809 anomaly_detector,
3810 sensorimotor_bus,
3811 reflex_loop,
3812 gan_ying_bus,
3813 transaction_state,
3814 resource_rules.as_ref(),
3815 escalation_queue,
3816 dharma.as_ref(),
3817 firewall,
3818 code_graph,
3819 registry_persistence,
3820 );
3821 }
3822 if let Some(k) = karma {
3823 reg = reg.register(Arc::new(KarmaReportTool::new(k)));
3824 }
3825 if let Some(d) = dharma {
3826 reg = reg.register(Arc::new(DharmaStatusTool::new(d.clone())));
3827 }
3828 if let Some(s) = substrate {
3829 reg = reg
3830 .register(Arc::new(HarmonyVectorTool::new(s.clone())))
3831 .register(Arc::new(HarmonyHistoryTool::new(s.clone())));
3832 if let Some(d) = dharma {
3833 if let Some(r) = resource_rules {
3834 reg = reg
3835 .register(Arc::new(GnosisStatusTool::new(
3836 d.clone(),
3837 r.clone(),
3838 s.clone(),
3839 )))
3840 .register(Arc::new(GnosisHistoryTool::new(s)))
3841 .register(Arc::new(GnosisExplainTool::new(d.clone(), r.clone())));
3842 }
3843 }
3844 }
3845
3846 reg
3847}
3848
3849pub fn register_meta_tools(
3855 registry: &ToolRegistry,
3856 store: &Arc<MemoryStore>,
3857 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3858) -> ToolRegistry {
3859 register_meta_tools_with_router(registry, store, shadow_stats, None).0
3860}
3861
3862#[must_use]
3872pub fn register_meta_tools_with_router(
3873 registry: &ToolRegistry,
3874 store: &Arc<MemoryStore>,
3875 shadow_stats: Arc<std::sync::RwLock<embedding_router::ShadowModeStats>>,
3876 pipeline: Option<Arc<DispatchPipeline>>,
3877) -> (ToolRegistry, Option<Arc<embedding_router::EmbeddingRouter>>) {
3878 let base_snapshot: Vec<Arc<dyn Tool>> = registry.all();
3879 let tool_count = base_snapshot.len();
3881
3882 let non_gnosis: Vec<Arc<dyn Tool>> = base_snapshot
3883 .iter()
3884 .filter(|t| t.name() != "gnosis")
3885 .cloned()
3886 .collect();
3887
3888 let mut list_builder = ToolRegistryBuilder::new();
3890 for tool in &non_gnosis {
3891 list_builder.register(tool.clone());
3892 }
3893 let list_registry = Arc::new(list_builder.build());
3894 let tools_list = Arc::new(ToolsListTool::new(Arc::clone(&list_registry)));
3895
3896 let usage_report = Arc::new(expansion::ToolsUsageReportTool::new(list_registry));
3900
3901 let gnosis = Arc::new(GnosisTool::with_tool_count(Arc::clone(store), tool_count));
3903 let mut wm_builder = ToolRegistryBuilder::new();
3904 for tool in &non_gnosis {
3905 wm_builder.register(tool.clone());
3906 }
3907 wm_builder.register(tools_list.clone());
3908 wm_builder.register(usage_report.clone());
3909 wm_builder.register(gnosis.clone());
3910
3911 let shadow_report = Arc::new(expansion::NluShadowReportTool::new(Arc::clone(
3916 &shadow_stats,
3917 )));
3918 wm_builder.register(shadow_report.clone());
3919 let wm = Arc::new(WmMetaTool::with_router_shadow_stats_and_pipeline(
3920 Arc::new(wm_builder.build()),
3921 wm_memory::create_embedder(),
3922 shadow_stats,
3923 pipeline,
3924 ));
3925 let router = wm.embedding_router().cloned();
3926
3927 let mut final_builder = ToolRegistryBuilder::new();
3929 for tool in non_gnosis {
3930 final_builder.register(tool);
3931 }
3932 final_builder.register(tools_list);
3933 final_builder.register(usage_report);
3934 final_builder.register(wm);
3935 final_builder.register(gnosis);
3936 final_builder.register(shadow_report);
3937 (final_builder.build(), router)
3938}
3939
3940#[cfg(test)]
3941mod tests {
3942 use super::*;
3943 use std::collections::BTreeMap;
3944 use std::path::{Path, PathBuf};
3945 use wm_core::BrainWave;
3946
3947 fn test_store() -> Arc<MemoryStore> {
3948 let tmp = tempfile::tempdir().unwrap();
3949 Arc::new(MemoryStore::open_default(tmp.path()).unwrap())
3950 }
3951
3952 fn cold_factors() -> wm_memory::cold_storage::OuterRimFactors {
3953 wm_memory::cold_storage::OuterRimFactors {
3954 age_factor: 1.0,
3955 access_factor: 1.0,
3956 resonance_factor: 1.0,
3957 emotional_factor: 1.0,
3958 importance_factor: 1.0,
3959 distance: 1.0,
3960 }
3961 }
3962
3963 fn freeze_for_read_test(
3964 store: &MemoryStore,
3965 galaxy: Galaxy,
3966 content: &str,
3967 is_private: bool,
3968 ) -> (uuid::Uuid, wm_memory::cold_storage::ColdRecord) {
3969 let mut memory = wm_memory::Memory::new(galaxy, content.to_string());
3970 memory.metadata.is_private = is_private;
3971 let id = memory.metadata.id;
3972 store.put(galaxy, &memory).unwrap();
3973 let record = store
3974 .freeze_to_cold(
3975 None,
3976 id,
3977 1.0,
3978 cold_factors(),
3979 None,
3980 None,
3981 wm_memory::cold_storage::CompressionCodec::Gzip,
3982 )
3983 .unwrap();
3984 (id, record)
3985 }
3986
3987 fn readonly_tree_snapshot(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
3988 fn visit(root: &Path, path: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
3989 for entry in std::fs::read_dir(path).unwrap() {
3990 let entry = entry.unwrap();
3991 let entry_path = entry.path();
3992 let relative = entry_path.strip_prefix(root).unwrap().to_path_buf();
3993 if relative == Path::new("lock.mdb") {
3994 continue;
3995 }
3996 if entry.file_type().unwrap().is_dir() {
3997 out.insert(relative.clone(), Vec::new());
3998 visit(root, &entry_path, out);
3999 } else {
4000 out.insert(relative, std::fs::read(entry_path).unwrap());
4001 }
4002 }
4003 }
4004
4005 let mut snapshot = BTreeMap::new();
4006 visit(root, root, &mut snapshot);
4007 snapshot
4008 }
4009
4010 #[tokio::test]
4011 async fn memory_create_warns_on_credential_shaped_content() {
4012 let store = test_store();
4013 let tool = MemoryCreateTool::new(store, None, None);
4014 let mut ctx = Context::default();
4015
4016 let clean = tool
4017 .call(
4018 &mut ctx,
4019 json!({"content": "the password policy requires rotation"}),
4020 )
4021 .await
4022 .unwrap();
4023 assert!(clean.get("warnings").is_none(), "clean content: {clean}");
4024
4025 let flagged = tool
4026 .call(
4027 &mut ctx,
4028 json!({"content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEow\n-----END RSA PRIVATE KEY-----"}),
4029 )
4030 .await
4031 .unwrap();
4032 assert_eq!(
4033 flagged["status"], "success",
4034 "warning, not refusal: {flagged}"
4035 );
4036 let warnings = flagged["warnings"].as_array().unwrap();
4037 assert!(
4038 warnings[0].as_str().unwrap().contains("private_key_pem"),
4039 "got: {warnings:?}"
4040 );
4041 assert!(warnings[0].as_str().unwrap().contains("keyring"));
4042 }
4043
4044 #[tokio::test]
4045 async fn memory_batch_create_aggregates_credential_warnings() {
4046 let store = test_store();
4047 let tool = MemoryBatchCreateTool::new(store, None, None);
4048 let mut ctx = Context::default();
4049 let r = tool
4050 .call(
4051 &mut ctx,
4052 json!({"items": [
4053 {"content": "ordinary note"},
4054 {"content": "AKIAIOSFODNN7EXAMPLE"},
4055 ]}),
4056 )
4057 .await
4058 .unwrap();
4059 assert_eq!(r["count"], 2);
4060 let warnings = r["warnings"].as_array().unwrap();
4061 assert!(warnings[0].as_str().unwrap().contains("aws_access_key_id"));
4062 }
4063
4064 fn test_registry_with(store: &Arc<MemoryStore>) -> ToolRegistry {
4065 let registry = ToolRegistry::new();
4066 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
4067 let spiral_tracker =
4068 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
4069 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4070 register_all(
4071 ®istry,
4072 store,
4073 None,
4074 None,
4075 &None,
4076 None,
4077 &None,
4078 associations,
4079 spiral_tracker,
4080 vector_store,
4081 None,
4082 None,
4083 None,
4084 None,
4085 None,
4086 None,
4087 None,
4088 std::sync::Arc::new(std::sync::Mutex::new(None)),
4089 None,
4090 None,
4091 None,
4092 expansion::RegistryPersistenceMode::Normal,
4093 Arc::new(wm_dispatch::CircuitBreakerRegistry::default()),
4094 )
4095 }
4096
4097 #[tokio::test]
4098 async fn memory_create_and_read() {
4099 let store = test_store();
4100 let tool = MemoryCreateTool::new(store.clone(), None, None);
4101 let mut ctx = Context::new(BrainWave::Gamma);
4102
4103 let args = json!({"content": "test memory content", "galaxy": "codex"});
4104 let result = tool.call(&mut ctx, args).await.unwrap();
4105 assert_eq!(result["status"], "success");
4106 let id = result["id"].as_str().unwrap();
4107
4108 let read_tool = MemoryReadTool::new(store.clone());
4109 let result = read_tool.call(&mut ctx, json!({"id": id})).await.unwrap();
4110 assert_eq!(result["status"], "success");
4111 assert_eq!(result["content"], "test memory content");
4112
4113 let episodic = store
4114 .episodic()
4115 .get(uuid::Uuid::parse_str(id).unwrap())
4116 .unwrap()
4117 .expect("explicit memory writes mirror into episodic storage");
4118 assert_eq!(episodic.content, "test memory content");
4119 }
4120
4121 #[tokio::test]
4122 async fn memory_read_recovers_cold_content_after_reopen_without_thawing() {
4123 let directory = tempfile::tempdir().unwrap();
4124 let path = directory.path().to_path_buf();
4125 let content = "cold UTF-8: cafe\u{301} \u{1f980}\nsecond line — exact".repeat(128);
4126 let (id, before) = {
4127 let store = MemoryStore::open_default(&path).unwrap();
4128 freeze_for_read_test(&store, Galaxy::Codex, &content, false)
4129 };
4130
4131 let store = Arc::new(MemoryStore::open_default(&path).unwrap());
4132 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
4133 assert_eq!(store.get_cold_record(id).unwrap().as_ref(), Some(&before));
4134 let before_read_tree = readonly_tree_snapshot(&path);
4135
4136 let mut ctx = Context::default();
4137 let result = MemoryReadTool::new(store.clone())
4138 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4139 .await
4140 .unwrap();
4141 assert_eq!(result["status"], "success");
4142 assert_eq!(result["content"], content);
4143
4144 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
4147 assert_eq!(store.get_cold_record(id).unwrap().as_ref(), Some(&before));
4148 assert_eq!(readonly_tree_snapshot(&path), before_read_tree);
4149 drop(store);
4150 let reopened = MemoryStore::open_default(&path).unwrap();
4151 assert!(reopened.get(Galaxy::Codex, id).unwrap().is_none());
4152 assert_eq!(
4153 reopened.get_cold_record(id).unwrap().as_ref(),
4154 Some(&before)
4155 );
4156 }
4157
4158 #[tokio::test]
4159 async fn memory_read_cold_fallback_is_galaxy_bound_and_missing_is_not_found() {
4160 let store = test_store();
4161 let (id, _) = freeze_for_read_test(&store, Galaxy::Codex, "cold codex only", false);
4162 let mut ctx = Context::default();
4163 let tool = MemoryReadTool::new(store);
4164
4165 let wrong_galaxy = tool
4166 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4167 .await
4168 .unwrap();
4169 assert_eq!(wrong_galaxy["status"], "not_found");
4170 assert_eq!(wrong_galaxy["galaxy"], "sessions");
4171 assert!(wrong_galaxy.get("content").is_none());
4172
4173 let missing = tool
4174 .call(
4175 &mut ctx,
4176 json!({"id": uuid::Uuid::new_v4(), "galaxy": "codex"}),
4177 )
4178 .await
4179 .unwrap();
4180 assert_eq!(missing["status"], "not_found");
4181 assert!(missing.get("content").is_none());
4182 }
4183
4184 #[tokio::test]
4185 async fn memory_read_private_cold_record_is_not_found_without_headers() {
4186 let store = test_store();
4187 let (id, _) = freeze_for_read_test(&store, Galaxy::Codex, "private cold content", true);
4188 let mut ctx = Context::default();
4189 let result = MemoryReadTool::new(store)
4190 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4191 .await
4192 .unwrap();
4193 assert_eq!(result["status"], "not_found");
4194 assert!(result.get("content").is_none());
4195 assert!(result.get("tags").is_none());
4196 assert!(result.get("created_at").is_none());
4197 }
4198
4199 #[tokio::test]
4200 async fn memory_read_refuses_corrupt_cold_payload_or_header_mismatch() {
4201 let store = test_store();
4202 let (payload_id, mut payload_record) =
4203 freeze_for_read_test(&store, Galaxy::Codex, "payload integrity", false);
4204 payload_record.compressed_payload[0] ^= 0xff;
4205 store.put_cold_record(&payload_record).unwrap();
4206
4207 let mut ctx = Context::default();
4208 let tool = MemoryReadTool::new(store.clone());
4209 assert!(
4210 tool.call(&mut ctx, json!({"id": payload_id, "galaxy": "codex"}))
4211 .await
4212 .is_err()
4213 );
4214 assert!(store.get(Galaxy::Codex, payload_id).unwrap().is_none());
4215
4216 let (header_id, mut header_record) =
4217 freeze_for_read_test(&store, Galaxy::Codex, "header integrity", false);
4218 header_record.content_hash = "wrong-header-hash".into();
4219 store.put_cold_record(&header_record).unwrap();
4220 assert!(
4221 tool.call(&mut ctx, json!({"id": header_id, "galaxy": "codex"}))
4222 .await
4223 .is_err()
4224 );
4225 assert!(store.get(Galaxy::Codex, header_id).unwrap().is_none());
4226 }
4227
4228 #[tokio::test]
4233 async fn memory_create_attestation_disclosure() {
4234 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
4235 let mut ctx = Context::new(BrainWave::Gamma);
4236
4237 let store = test_store();
4239 let tool = MemoryCreateTool::with_attestation_key(store.clone(), None, None, None);
4240 let result = tool
4241 .call(
4242 &mut ctx,
4243 json!({"content": "unattested create", "galaxy": "codex"}),
4244 )
4245 .await
4246 .unwrap();
4247 assert_eq!(result["status"], "success");
4248 assert_eq!(result["attested"], false);
4249 assert_eq!(result["attested_reason"], "node key unavailable");
4250
4251 let tool = MemoryCreateTool::with_attestation_key(
4253 store.clone(),
4254 None,
4255 None,
4256 Some("not-hex".to_string()),
4257 );
4258 let result = tool
4259 .call(
4260 &mut ctx,
4261 json!({"content": "bad key create", "galaxy": "codex"}),
4262 )
4263 .await
4264 .unwrap();
4265 assert_eq!(result["attested"], false);
4266 assert_eq!(result["attested_reason"], "node key invalid");
4267
4268 let tool = MemoryCreateTool::with_attestation_key(
4270 store.clone(),
4271 None,
4272 None,
4273 Some(TEST_KEY.to_string()),
4274 );
4275 let result = tool
4276 .call(
4277 &mut ctx,
4278 json!({"content": "attested create", "galaxy": "codex"}),
4279 )
4280 .await
4281 .unwrap();
4282 assert_eq!(result["attested"], true);
4283 assert!(result.get("attested_reason").is_none());
4284 let id = uuid::Uuid::parse_str(result["id"].as_str().unwrap()).unwrap();
4285 let report = store.verify_attestation(Galaxy::Codex, id).unwrap();
4286 assert!(report.attested, "{:?}", report.breaks);
4287 assert!(report.signature_valid, "{:?}", report.breaks);
4288 assert!(report.matches_head, "{:?}", report.breaks);
4289 assert!(report.memory_present);
4290 assert!(report.breaks.is_empty());
4291
4292 let mut memory = store.get(Galaxy::Codex, id).unwrap().unwrap();
4296 memory.content = "edited after attestation".to_string();
4297 memory.metadata.content_hash = wm_memory::content_hash(&memory.content);
4298 store.put(Galaxy::Codex, &memory).unwrap();
4299 let stale = store.verify_attestation(Galaxy::Codex, id).unwrap();
4300 assert!(stale.attested);
4301 assert!(stale.signature_valid);
4302 assert!(!stale.matches_head);
4303
4304 let scanned = store.scan_attestations().unwrap();
4306 assert_eq!(scanned.len(), 1);
4307 assert_eq!(scanned[0].memory_id, id.to_string());
4308 }
4309
4310 #[tokio::test]
4311 async fn memory_batch_create_attests_each_item() {
4312 const TEST_KEY: &str = "0bd1c44170ca3d916648a983dcdb8583d22f2da5b29fdd5ede4b38e805435577";
4313 let store = test_store();
4314 let tool = MemoryBatchCreateTool::with_attestation_key(
4315 store.clone(),
4316 None,
4317 None,
4318 Some(TEST_KEY.to_string()),
4319 );
4320 let mut ctx = Context::new(BrainWave::Gamma);
4321 let result = tool
4322 .call(
4323 &mut ctx,
4324 json!({"items": [{"content": "batch one"}, {"content": "batch two"}]}),
4325 )
4326 .await
4327 .unwrap();
4328 assert_eq!(result["attested_count"], 2);
4329 assert_eq!(store.scan_attestations().unwrap().len(), 2);
4330
4331 let tool = MemoryBatchCreateTool::with_attestation_key(store.clone(), None, None, None);
4333 let result = tool
4334 .call(&mut ctx, json!({"items": [{"content": "batch three"}]}))
4335 .await
4336 .unwrap();
4337 assert_eq!(result["attested_count"], 0);
4338 assert_eq!(result["count"], 1);
4339 }
4340
4341 #[tokio::test]
4342 async fn memory_batch_create_mirrors_into_episodic_lane() {
4343 let store = test_store();
4344 let tool = MemoryBatchCreateTool::new(store.clone(), None, None);
4345 let mut ctx = Context::new(BrainWave::Gamma);
4346 let result = tool
4347 .call(
4348 &mut ctx,
4349 json!({
4350 "items": [
4351 {"content": "batch rust retrieval"},
4352 {"content": "batch grocery list"}
4353 ]
4354 }),
4355 )
4356 .await
4357 .unwrap();
4358 assert_eq!(result["status"], "success");
4359 let ids = result["ids"].as_array().unwrap();
4360 let first = uuid::Uuid::parse_str(ids[0].as_str().unwrap()).unwrap();
4361 let hits = store
4362 .episodic()
4363 .search("rust retrieval", 10, false)
4364 .unwrap();
4365 assert_eq!(hits.len(), 1);
4366 assert_eq!(hits[0].record.id, first);
4367 }
4368
4369 #[tokio::test]
4370 async fn memory_list_returns_entries() {
4371 let store = test_store();
4372 let create = MemoryCreateTool::new(store.clone(), None, None);
4373 let mut ctx = Context::new(BrainWave::Gamma);
4374
4375 for i in 0..3 {
4376 create
4377 .call(&mut ctx, json!({"content": format!("item-{i}")}))
4378 .await
4379 .unwrap();
4380 }
4381
4382 let list = MemoryListTool::new(store);
4383 let result = list.call(&mut ctx, json!({"limit": 10})).await.unwrap();
4384 assert_eq!(result["status"], "success");
4385 assert_eq!(result["total"], 3);
4386 assert_eq!(result["returned"], 3);
4387 }
4388
4389 #[tokio::test]
4393 async fn memory_list_offset_and_exclude_tags_page_visible_surface() {
4394 let store = test_store();
4395 let mut ctx = Context::new(BrainWave::Gamma);
4396
4397 for i in 0..5 {
4398 let mut m = wm_memory::Memory::new(wm_core::Galaxy::Codex, format!("page note {i}"));
4399 if i == 1 {
4400 m.metadata.tags = vec!["noise".into()];
4401 }
4402 if i == 3 {
4403 m.metadata.is_private = true;
4404 }
4405 store.put(wm_core::Galaxy::Codex, &m).unwrap();
4406 }
4407
4408 let list = MemoryListTool::new(store);
4409
4410 let all = list
4413 .call(
4414 &mut ctx,
4415 json!({"galaxy": "codex", "limit": 50, "exclude_tags": ["noise"]}),
4416 )
4417 .await
4418 .unwrap();
4419 assert_eq!(all["total"], 5, "total counts the whole galaxy");
4420 assert_eq!(all["matched"], 3, "private + excluded are invisible");
4421 assert_eq!(all["returned"], 3);
4422 assert_eq!(all["offset"], 0);
4423
4424 let page1 = list
4426 .call(
4427 &mut ctx,
4428 json!({"galaxy": "codex", "limit": 2, "offset": 0, "exclude_tags": ["noise"]}),
4429 )
4430 .await
4431 .unwrap();
4432 assert_eq!(page1["returned"], 2);
4433 let page2 = list
4434 .call(
4435 &mut ctx,
4436 json!({"galaxy": "codex", "limit": 2, "offset": 2, "exclude_tags": ["noise"]}),
4437 )
4438 .await
4439 .unwrap();
4440 assert_eq!(
4441 page2["returned"], 1,
4442 "matched is 3 — the tail page is short"
4443 );
4444 assert_eq!(page2["offset"], 2);
4445
4446 let ids_of = |v: &Value| -> Vec<String> {
4447 v["memories"]
4448 .as_array()
4449 .unwrap()
4450 .iter()
4451 .filter_map(|m| m["id"].as_str().map(String::from))
4452 .collect()
4453 };
4454 let (p1, p2, everything) = (ids_of(&page1), ids_of(&page2), ids_of(&all));
4455 assert_eq!(p1.len(), 2);
4456 let mut union = p1;
4457 union.extend(p2);
4458 let mut sorted_union = union.clone();
4459 sorted_union.sort();
4460 let mut sorted_all = everything;
4461 sorted_all.sort();
4462 assert_eq!(sorted_union, sorted_all, "pages must partition the surface");
4463 }
4464
4465 #[tokio::test]
4470 async fn memory_create_stamps_provenance_by_claim() {
4471 let store = test_store();
4472 let create = MemoryCreateTool::new(store.clone(), None, None);
4473 let mut ctx = Context::new(BrainWave::Gamma);
4474
4475 let silent = create
4476 .call(&mut ctx, json!({"content": "no claim"}))
4477 .await
4478 .unwrap();
4479 assert_eq!(silent["source"], "agent");
4480 assert!((silent["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
4481
4482 let claimed = create
4483 .call(
4484 &mut ctx,
4485 json!({"content": "user dictated this", "source": "user"}),
4486 )
4487 .await
4488 .unwrap();
4489 assert_eq!(claimed["source"], "user");
4490 assert!((claimed["source_trust"].as_f64().unwrap() - 1.0).abs() < 1e-5);
4491
4492 let custom = create
4493 .call(&mut ctx, json!({"content": "web import", "source": "web"}))
4494 .await
4495 .unwrap();
4496 assert_eq!(custom["source"], "web");
4497 assert!((custom["source_trust"].as_f64().unwrap() - 0.7).abs() < 1e-5);
4498
4499 let fetch = |id: &str| {
4500 store
4501 .get(wm_core::Galaxy::Codex, uuid::Uuid::parse_str(id).unwrap())
4502 .expect("stored")
4503 .expect("present")
4504 };
4505 assert_eq!(
4506 fetch(silent["id"].as_str().unwrap()).metadata.source,
4507 "agent"
4508 );
4509 assert_eq!(
4510 fetch(claimed["id"].as_str().unwrap()).metadata.source,
4511 "user"
4512 );
4513 }
4514
4515 #[tokio::test]
4516 async fn gnosis_returns_system_info() {
4517 let store = test_store();
4518 let tool = GnosisTool::new(store);
4519 let mut ctx = Context::new(BrainWave::Gamma);
4520 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4521 assert_eq!(result["status"], "success");
4522 assert!(result["version"].is_string());
4523 }
4524
4525 #[tokio::test]
4526 async fn memory_delete_removes_entry() {
4527 let store = test_store();
4528 let create = MemoryCreateTool::new(store.clone(), None, None);
4529 let mut ctx = Context::new(BrainWave::Gamma);
4530
4531 let result = create
4532 .call(&mut ctx, json!({"content": "to be deleted"}))
4533 .await
4534 .unwrap();
4535 let id = result["id"].as_str().unwrap();
4536
4537 let delete = MemoryDeleteTool::new(store.clone(), None);
4538 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4539 assert_eq!(result["status"], "success");
4540
4541 let read = MemoryReadTool::new(store);
4542 let result = read.call(&mut ctx, json!({"id": id})).await.unwrap();
4543 assert_eq!(result["status"], "not_found");
4544 }
4545
4546 #[tokio::test]
4547 async fn memory_delete_without_galaxy_resolves_across_memory_galaxies() {
4548 let store = test_store();
4549 let create = MemoryCreateTool::new(store.clone(), None, None);
4550 let mut ctx = Context::new(BrainWave::Gamma);
4551
4552 let result = create
4554 .call(
4555 &mut ctx,
4556 json!({"content": "session decision", "galaxy": "sessions"}),
4557 )
4558 .await
4559 .unwrap();
4560 let id = result["id"].as_str().unwrap();
4561
4562 let delete = MemoryDeleteTool::new(store.clone(), None);
4564 let result = delete.call(&mut ctx, json!({"id": id})).await.unwrap();
4565 assert_eq!(result["status"], "success");
4566 assert!(
4567 result["galaxies"]
4568 .as_array()
4569 .unwrap()
4570 .contains(&json!("sessions"))
4571 );
4572
4573 let read = MemoryReadTool::new(store.clone());
4574 let result = read
4575 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4576 .await
4577 .unwrap();
4578 assert_eq!(result["status"], "not_found");
4579 }
4580
4581 #[tokio::test]
4582 async fn memory_delete_explicit_galaxy_does_not_miss_other_galaxies() {
4583 let store = test_store();
4584 let create = MemoryCreateTool::new(store.clone(), None, None);
4585 let mut ctx = Context::new(BrainWave::Gamma);
4586
4587 let result = create
4588 .call(
4589 &mut ctx,
4590 json!({"content": "in sessions", "galaxy": "sessions"}),
4591 )
4592 .await
4593 .unwrap();
4594 let id = result["id"].as_str().unwrap();
4595
4596 let delete = MemoryDeleteTool::new(store.clone(), None);
4598 let result = delete
4599 .call(&mut ctx, json!({"id": id, "galaxy": "codex"}))
4600 .await
4601 .unwrap();
4602 assert_eq!(result["status"], "not_found");
4603 assert!(result["hint"].is_string());
4604
4605 let read = MemoryReadTool::new(store.clone());
4606 let result = read
4607 .call(&mut ctx, json!({"id": id, "galaxy": "sessions"}))
4608 .await
4609 .unwrap();
4610 assert_eq!(result["status"], "success");
4611 }
4612
4613 #[tokio::test]
4614 async fn memory_query_filters_by_tags() {
4615 let store = test_store();
4616 let create = MemoryCreateTool::new(store.clone(), None, None);
4617 let mut ctx = Context::new(BrainWave::Gamma);
4618
4619 create
4620 .call(&mut ctx, json!({"content": "tagged", "tags": ["rust"]}))
4621 .await
4622 .unwrap();
4623 create
4624 .call(&mut ctx, json!({"content": "untagged"}))
4625 .await
4626 .unwrap();
4627
4628 let query = MemoryQueryTool::new(store);
4629 let result = query
4630 .call(&mut ctx, json!({"tags": ["rust"]}))
4631 .await
4632 .unwrap();
4633 assert_eq!(result["status"], "success");
4634 assert_eq!(result["total"], 1);
4635 }
4636
4637 #[tokio::test]
4640 async fn memory_query_time_range_passthrough() {
4641 let store = test_store();
4642 let mut ctx = Context::new(BrainWave::Gamma);
4643
4644 let mut old = wm_memory::Memory::new(wm_core::Galaxy::Codex, "old relic".into());
4645 old.metadata.created_at = chrono::Utc::now() - chrono::Duration::days(60);
4646 store.put(wm_core::Galaxy::Codex, &old).unwrap();
4647 let mut recent = wm_memory::Memory::new(wm_core::Galaxy::Codex, "recent note".into());
4648 recent.metadata.created_at = chrono::Utc::now() - chrono::Duration::hours(1);
4649 store.put(wm_core::Galaxy::Codex, &recent).unwrap();
4650
4651 let query = MemoryQueryTool::new(store);
4652 let cutoff = (chrono::Utc::now() - chrono::Duration::days(1))
4653 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
4654
4655 let only_recent = query
4656 .call(&mut ctx, json!({"created_after": cutoff}))
4657 .await
4658 .unwrap();
4659 assert_eq!(only_recent["total"], 1);
4660 assert_eq!(only_recent["memories"][0]["content_preview"], "recent note");
4661 assert_eq!(
4662 only_recent["time_range"]["created_after"], cutoff,
4663 "the applied time range must be disclosed"
4664 );
4665
4666 let only_old = query
4667 .call(&mut ctx, json!({"created_before": cutoff}))
4668 .await
4669 .unwrap();
4670 assert_eq!(only_old["total"], 1);
4671 assert_eq!(only_old["memories"][0]["content_preview"], "old relic");
4672
4673 let both = query
4675 .call(
4676 &mut ctx,
4677 json!({
4678 "created_after": (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
4679 "created_before": cutoff,
4680 }),
4681 )
4682 .await
4683 .unwrap();
4684 assert_eq!(both["total"], 1);
4685 assert_eq!(both["memories"][0]["content_preview"], "old relic");
4686
4687 let bad = query
4689 .call(&mut ctx, json!({"created_after": "not-a-timestamp"}))
4690 .await;
4691 assert!(bad.is_err(), "invalid RFC 3339 must be refused");
4692 }
4693
4694 #[tokio::test]
4695 async fn memory_vector_search_by_embedding() {
4696 let store = test_store();
4697 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4698
4699 {
4701 let mut vs = vector_store.lock().unwrap();
4702 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![1.0, 0.0, 0.0]);
4703 vs.add(uuid::Uuid::new_v4(), Galaxy::Codex, vec![0.9, 0.1, 0.0]);
4704 vs.add(uuid::Uuid::new_v4(), Galaxy::Research, vec![0.0, 1.0, 0.0]);
4705 }
4706
4707 let tool = MemoryVectorSearchTool::new(store, vector_store);
4708 let mut ctx = Context::new(BrainWave::Gamma);
4709
4710 let result = tool
4712 .call(&mut ctx, json!({"embedding": [1.0, 0.0, 0.0], "limit": 2}))
4713 .await
4714 .unwrap();
4715 assert_eq!(result["status"], "success");
4716 assert_eq!(result["total"], 2);
4717 }
4718
4719 #[tokio::test]
4720 async fn memory_vector_search_missing_args() {
4721 let store = test_store();
4722 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
4723
4724 let tool = MemoryVectorSearchTool::new(store, vector_store);
4725 let mut ctx = Context::new(BrainWave::Gamma);
4726
4727 let result = tool.call(&mut ctx, json!({"limit": 5})).await;
4728 assert!(result.is_err());
4729 }
4730
4731 #[tokio::test]
4732 async fn wm_routes_vector_search_to_memory_vector_search() {
4733 let store = test_store();
4734 let registry = test_registry_with(&store);
4735 let registry = register_meta_tools(
4736 ®istry,
4737 &store,
4738 std::sync::Arc::new(std::sync::RwLock::new(
4739 embedding_router::ShadowModeStats::default(),
4740 )),
4741 );
4742
4743 let wm = registry.get("wm").unwrap();
4744 let mut ctx = Context::new(BrainWave::Gamma);
4745 let result = wm
4746 .call(
4747 &mut ctx,
4748 json!({"route": "memory.vector.search", "args": {"embedding": [1.0, 0.0, 0.0]}}),
4749 )
4750 .await
4751 .unwrap();
4752
4753 assert_eq!(result["status"], "success");
4754 assert_eq!(result["_wm_route"]["tool"], "memory.vector.search");
4755 }
4756
4757 #[tokio::test]
4758 async fn wm_routes_shadow_report_inside_meta_tool() {
4759 let store = test_store();
4763 let registry = test_registry_with(&store);
4764 let registry = register_meta_tools(
4765 ®istry,
4766 &store,
4767 std::sync::Arc::new(std::sync::RwLock::new(
4768 embedding_router::ShadowModeStats::default(),
4769 )),
4770 );
4771
4772 let wm = registry.get("wm").unwrap();
4773 let mut ctx = Context::new(BrainWave::Gamma);
4774 let result = wm
4775 .call(&mut ctx, json!({"route": "nlu.shadow_report"}))
4776 .await
4777 .unwrap();
4778
4779 assert_eq!(result["_wm_route"]["tool"], "nlu.shadow_report");
4780 assert!(
4781 result.get("total_queries").is_some(),
4782 "expected shadow report payload"
4783 );
4784 }
4785
4786 #[tokio::test]
4787 async fn memory_associate_and_find() {
4788 let store = test_store();
4789 let create = MemoryCreateTool::new(store.clone(), None, None);
4790 let mut ctx = Context::new(BrainWave::Gamma);
4791
4792 let r1 = create
4793 .call(&mut ctx, json!({"content": "source mem"}))
4794 .await
4795 .unwrap();
4796 let r2 = create
4797 .call(&mut ctx, json!({"content": "target mem"}))
4798 .await
4799 .unwrap();
4800 let id1 = r1["id"].as_str().unwrap();
4801 let id2 = r2["id"].as_str().unwrap();
4802
4803 let assoc = MemoryAssociateTool::new(store.clone());
4804 let result = assoc
4805 .call(
4806 &mut ctx,
4807 json!({"source": id1, "target": id2, "weight": 0.8}),
4808 )
4809 .await
4810 .unwrap();
4811 assert_eq!(result["status"], "success");
4812
4813 let find = MemoryAssociationsTool::new(store);
4814 let result = find
4815 .call(&mut ctx, json!({"id": id1, "direction": "from"}))
4816 .await
4817 .unwrap();
4818 assert_eq!(result["status"], "success");
4819 assert_eq!(result["returned"], 1);
4820 }
4821
4822 #[tokio::test]
4823 async fn karma_report_shows_entries() {
4824 let tmp = tempfile::tempdir().unwrap();
4825 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
4826 let ledger = Arc::new(KarmaLedger::new(store).unwrap());
4827
4828 ledger.record("test_tool", false, 0, true).unwrap();
4830 ledger.record("wasteful_tool", true, 0, true).unwrap();
4831
4832 let tool = KarmaReportTool::new(ledger);
4833 let mut ctx = Context::new(BrainWave::Gamma);
4834 let result = tool.call(&mut ctx, json!({"limit": 5})).await.unwrap();
4835 assert_eq!(result["status"], "success");
4836 assert_eq!(result["entry_count"], 2);
4837 assert_eq!(result["recent_entries"].as_array().unwrap().len(), 2);
4838 }
4839
4840 #[tokio::test]
4841 async fn dharma_status_returns_homeostasis() {
4842 let gate = Arc::new(DharmaGate::default());
4843 let tool = DharmaStatusTool::new(gate);
4844 let mut ctx = Context::new(BrainWave::Gamma);
4845 let result = tool.call(&mut ctx, json!({})).await.unwrap();
4846 assert_eq!(result["status"], "success");
4847 assert!(result["homeostasis"]["health_score"].is_f64());
4848 assert!(result["sutras"]["ahimsa"].is_string());
4849 assert!(result["decisions"]["total"].is_u64());
4850 assert!(result["decisions"]["blocked_ratio"].is_number());
4851 }
4852
4853 #[tokio::test]
4854 async fn wm_routes_remember_to_memory_create() {
4855 let store = test_store();
4856 let registry = test_registry_with(&store);
4857 let registry = register_meta_tools(
4858 ®istry,
4859 &store,
4860 std::sync::Arc::new(std::sync::RwLock::new(
4861 embedding_router::ShadowModeStats::default(),
4862 )),
4863 );
4864
4865 let wm = registry.get("wm").unwrap();
4866 let mut ctx = Context::new(BrainWave::Gamma);
4867 let result = wm
4868 .call(
4869 &mut ctx,
4870 json!({"thought": "remember that the API uses X-User-Id headers"}),
4871 )
4872 .await
4873 .unwrap();
4874
4875 assert_eq!(result["status"], "success");
4876 assert_eq!(result["_wm_route"]["tool"], "memory.create");
4877 assert!(result["id"].is_string());
4878 }
4879
4880 #[tokio::test]
4881 async fn wm_explicit_route() {
4882 let store = test_store();
4883 let registry = test_registry_with(&store);
4884 let registry = register_meta_tools(
4885 ®istry,
4886 &store,
4887 std::sync::Arc::new(std::sync::RwLock::new(
4888 embedding_router::ShadowModeStats::default(),
4889 )),
4890 );
4891
4892 let wm = registry.get("wm").unwrap();
4893 let mut ctx = Context::new(BrainWave::Gamma);
4894 let result = wm
4895 .call(
4896 &mut ctx,
4897 json!({
4898 "route": "gnosis"
4899 }),
4900 )
4901 .await
4902 .unwrap();
4903
4904 assert_eq!(result["status"], "success");
4905 assert_eq!(result["_wm_route"]["tool"], "gnosis");
4906 }
4907
4908 #[tokio::test]
4909 async fn wm_no_input_returns_error() {
4910 let store = test_store();
4911 let registry = test_registry_with(&store);
4912 let registry = register_meta_tools(
4913 ®istry,
4914 &store,
4915 std::sync::Arc::new(std::sync::RwLock::new(
4916 embedding_router::ShadowModeStats::default(),
4917 )),
4918 );
4919
4920 let wm = registry.get("wm").unwrap();
4921 let mut ctx = Context::new(BrainWave::Gamma);
4922 let result = wm.call(&mut ctx, json!({})).await.unwrap();
4923
4924 assert_eq!(result["status"], "error");
4925 }
4926
4927 #[tokio::test]
4928 async fn wm_missing_route_echoes_received_keys() {
4929 let store = test_store();
4934 let registry = test_registry_with(&store);
4935 let registry = register_meta_tools(
4936 ®istry,
4937 &store,
4938 std::sync::Arc::new(std::sync::RwLock::new(
4939 embedding_router::ShadowModeStats::default(),
4940 )),
4941 );
4942
4943 let wm = registry.get("wm").unwrap();
4944 let mut ctx = Context::new(BrainWave::Gamma);
4945 let result = wm
4946 .call(
4947 &mut ctx,
4948 json!({"content": "x", "turn_type": "summary", "importance": 0.5}),
4949 )
4950 .await
4951 .unwrap();
4952
4953 assert_eq!(result["status"], "error");
4954 let message = result["message"].as_str().unwrap();
4955 assert!(
4956 message.contains("received argument keys"),
4957 "error must disclose received keys, got: {message}"
4958 );
4959 for key in ["content", "turn_type", "importance"] {
4960 assert!(
4961 message.contains(key),
4962 "error must list received key '{key}', got: {message}"
4963 );
4964 }
4965 let empty = wm.call(&mut ctx, json!({})).await.unwrap();
4967 assert!(
4968 !empty["message"]
4969 .as_str()
4970 .unwrap()
4971 .contains("received argument keys: ["),
4972 "empty input must not list keys, got: {}",
4973 empty["message"]
4974 );
4975 }
4976
4977 #[tokio::test]
4978 async fn wm_unknown_tool_returns_error() {
4979 let store = test_store();
4980 let registry = test_registry_with(&store);
4981 let registry = register_meta_tools(
4982 ®istry,
4983 &store,
4984 std::sync::Arc::new(std::sync::RwLock::new(
4985 embedding_router::ShadowModeStats::default(),
4986 )),
4987 );
4988
4989 let wm = registry.get("wm").unwrap();
4990 let mut ctx = Context::new(BrainWave::Gamma);
4991 let result = wm
4992 .call(&mut ctx, json!({"route": "nonexistent.tool"}))
4993 .await
4994 .unwrap();
4995
4996 assert_eq!(result["status"], "error");
4997 assert!(result["message"].as_str().unwrap().contains("Unknown tool"));
4998 }
4999
5000 #[tokio::test]
5001 async fn memory_query_tags_only_is_allowed() {
5002 let store = test_store();
5006 let mut mem = Memory::new(Galaxy::Codex, "atlas constraint note".into());
5007 mem.metadata.tags = vec!["atlas".into(), "constraint".into()];
5008 store.put(Galaxy::Codex, &mem).unwrap();
5009
5010 let registry = test_registry_with(&store);
5011 let registry = register_meta_tools(
5012 ®istry,
5013 &store,
5014 std::sync::Arc::new(std::sync::RwLock::new(
5015 embedding_router::ShadowModeStats::default(),
5016 )),
5017 );
5018 let wm = registry.get("wm").unwrap();
5019 let mut ctx = Context::new(BrainWave::Gamma);
5020 let result = wm
5021 .call(
5022 &mut ctx,
5023 json!({"route": "memory.query", "args": {"tags": ["atlas", "constraint"]}}),
5024 )
5025 .await
5026 .unwrap();
5027 assert_eq!(result["status"], "success", "{result}");
5028 assert_eq!(result["total"], 1, "{result}");
5029 assert!(
5030 result["memories"][0]
5031 .to_string()
5032 .contains("atlas constraint"),
5033 "{result}"
5034 );
5035 }
5036
5037 #[tokio::test]
5038 async fn memory_search_cold_discovery_is_opt_in_and_verified() {
5039 let store = test_store();
5040 let factors = wm_memory::cold_storage::OuterRimFactors {
5041 age_factor: 0.5,
5042 access_factor: 0.5,
5043 resonance_factor: 0.5,
5044 emotional_factor: 0.5,
5045 importance_factor: 0.5,
5046 distance: 0.5,
5047 };
5048 let mem = Memory::new(
5049 Galaxy::Codex,
5050 "cold original zxquniquehotcold999 deep".into(),
5051 );
5052 let rec = wm_memory::cold_storage::ColdRecord::new(
5053 &mem,
5054 0.5,
5055 factors,
5056 None,
5057 None,
5058 wm_memory::cold_storage::CompressionCodec::Gzip,
5059 )
5060 .unwrap();
5061 store.put_cold_record(&rec).unwrap();
5062
5063 let registry = test_registry_with(&store);
5064 let _ = ®istry;
5067 let search = expansion::MemoryHybridRecallTool::as_search(store.clone(), None, None);
5068 let mut ctx = Context::new(BrainWave::Gamma);
5069
5070 let without = search
5072 .call(
5073 &mut ctx,
5074 json!({"query": "zxquniquehotcold999", "limit": 5}),
5075 )
5076 .await
5077 .unwrap();
5078 assert_eq!(without["count"], 0, "{without}");
5079
5080 let with = search
5082 .call(
5083 &mut ctx,
5084 json!({"query": "zxquniquehotcold999", "limit": 5, "include_cold": true}),
5085 )
5086 .await
5087 .unwrap();
5088 assert_eq!(with["cold_discovery"]["no_thaw"], true, "{with}");
5089 assert!(
5090 with["results"]
5091 .as_array()
5092 .unwrap()
5093 .iter()
5094 .any(|r| r["source"] == "cold" && r["integrity"] == "verified"),
5095 "{with}"
5096 );
5097 }
5098
5099 #[tokio::test]
5100 async fn wm_missing_arg_returns_hint() {
5101 let store = test_store();
5102 let registry = test_registry_with(&store);
5103 let registry = register_meta_tools(
5104 ®istry,
5105 &store,
5106 std::sync::Arc::new(std::sync::RwLock::new(
5107 embedding_router::ShadowModeStats::default(),
5108 )),
5109 );
5110
5111 let wm = registry.get("wm").unwrap();
5112 let mut ctx = Context::new(BrainWave::Gamma);
5113
5114 let result = wm
5116 .call(&mut ctx, json!({"route": "memory.read"}))
5117 .await
5118 .unwrap();
5119
5120 assert_eq!(result["status"], "error");
5121 assert!(
5122 result["message"]
5123 .as_str()
5124 .unwrap()
5125 .contains("Missing required argument")
5126 );
5127 assert!(result["hint"].as_str().unwrap().contains("uuid"));
5128 }
5129
5130 #[test]
5131 fn search_payload_extracts_curated_intents() {
5132 let cases = [
5133 (
5134 "find BETA quartz submarine in memory",
5135 "BETA quartz submarine",
5136 ),
5137 (
5138 "What do you remember about BETA quartz submarine?",
5139 "BETA quartz submarine",
5140 ),
5141 (
5142 "What did we decide about BETA quartz submarine?",
5143 "BETA quartz submarine",
5144 ),
5145 ("recall BETA quartz submarine", "BETA quartz submarine"),
5146 ("look up BETA quartz submarine", "BETA quartz submarine"),
5147 ("search for rust", "rust"),
5148 ("search memory for rust", "rust"),
5149 ];
5150 for (thought, expected) in cases {
5151 let got = WmMetaTool::extract_payload(thought, "memory.search");
5152 assert_eq!(
5153 got,
5154 Some(("query".to_string(), expected.to_string())),
5155 "for {thought:?}"
5156 );
5157 }
5158 }
5159
5160 #[tokio::test]
5161 async fn wm_auto_route_missing_arg_returns_hint() {
5162 let store = test_store();
5163 let registry = test_registry_with(&store);
5164 let registry = register_meta_tools(
5165 ®istry,
5166 &store,
5167 std::sync::Arc::new(std::sync::RwLock::new(
5168 embedding_router::ShadowModeStats::default(),
5169 )),
5170 );
5171
5172 let wm = registry.get("wm").unwrap();
5173 let mut ctx = Context::new(BrainWave::Gamma);
5174
5175 let result = wm
5180 .call(&mut ctx, json!({"thought": "fetch memory"}))
5181 .await
5182 .unwrap();
5183
5184 assert_eq!(result["status"], "error");
5185 assert!(
5186 result["hint"].as_str().is_some_and(|h| h.contains("uuid")),
5187 "expected a read hint, got {result}"
5188 );
5189 }
5190
5191 #[tokio::test]
5192 async fn wm_routes_karma_to_karma_report() {
5193 let tmp = tempfile::tempdir().unwrap();
5194 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
5195 let ledger = Arc::new(KarmaLedger::new(store.clone()).unwrap());
5196 let gate = Arc::new(DharmaGate::default());
5197
5198 let registry = ToolRegistry::new();
5199 let associations = Arc::new(AssociationStore::open(store.env()).unwrap());
5200 let spiral_tracker =
5201 Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
5202 let vector_store = Arc::new(std::sync::Mutex::new(wm_memory::VectorStore::new()));
5203 let registry = register_all(
5204 ®istry,
5205 &store,
5206 None,
5207 Some(ledger),
5208 &Some(gate),
5209 None,
5210 &None,
5211 associations,
5212 spiral_tracker,
5213 vector_store,
5214 None,
5215 None,
5216 None,
5217 None,
5218 None,
5219 None,
5220 None,
5221 std::sync::Arc::new(std::sync::Mutex::new(None)),
5222 None,
5223 None,
5224 None,
5225 expansion::RegistryPersistenceMode::Normal,
5226 Arc::new(wm_dispatch::CircuitBreakerRegistry::default()),
5227 );
5228 let registry = register_meta_tools(
5229 ®istry,
5230 &store,
5231 std::sync::Arc::new(std::sync::RwLock::new(
5232 embedding_router::ShadowModeStats::default(),
5233 )),
5234 );
5235
5236 let wm = registry.get("wm").unwrap();
5237 let mut ctx = Context::new(BrainWave::Gamma);
5238 let result = wm
5239 .call(&mut ctx, json!({"thought": "show me the karma report"}))
5240 .await
5241 .unwrap();
5242
5243 assert_eq!(result["status"], "success");
5244 assert_eq!(result["_wm_route"]["tool"], "karma.report");
5245 }
5246
5247 fn test_registry_with_pipeline(
5250 store: &Arc<MemoryStore>,
5251 ) -> (ToolRegistry, Arc<DispatchPipeline>) {
5252 let registry = test_registry_with(store);
5253 let pipeline = Arc::new(DispatchPipeline::with_defaults());
5254 let (registry, _router) = register_meta_tools_with_router(
5255 ®istry,
5256 store,
5257 std::sync::Arc::new(std::sync::RwLock::new(
5258 embedding_router::ShadowModeStats::default(),
5259 )),
5260 Some(pipeline.clone()),
5261 );
5262 (registry, pipeline)
5263 }
5264
5265 #[tokio::test]
5266 async fn wm_route_destructive_without_confirm_blocked_by_pipeline() {
5267 let store = test_store();
5268 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5269
5270 let wm = registry.get("wm").unwrap();
5271 let mut ctx = Context::new(BrainWave::Gamma);
5272 let result = wm
5273 .call(
5274 &mut ctx,
5275 json!({"route": "memory.delete", "args": {"id": "00000000-0000-0000-0000-000000000001"}}),
5276 )
5277 .await
5278 .unwrap();
5279
5280 assert_eq!(result["status"], "error");
5281 assert!(
5282 result["error"].as_str().unwrap().contains("destructive"),
5283 "expected destructive-gate message, got: {result}"
5284 );
5285 assert!(result["error"].as_str().unwrap().contains("confirm"));
5286 }
5287
5288 #[tokio::test]
5289 async fn wm_route_destructive_with_confirm_proceeds() {
5290 let store = test_store();
5291 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5292
5293 let memory = Memory::new(Galaxy::Codex, "delete me via wm route".into());
5295 let id = memory.metadata.id;
5296 store.put(Galaxy::Codex, &memory).unwrap();
5297
5298 let wm = registry.get("wm").unwrap();
5299 let mut ctx = Context::new(BrainWave::Gamma);
5300 let result = wm
5301 .call(
5302 &mut ctx,
5303 json!({"route": "memory.delete", "args": {"id": id.to_string(), "galaxy": "codex", "confirm": true}}),
5304 )
5305 .await
5306 .unwrap();
5307
5308 assert_eq!(result["status"], "success");
5309 assert_eq!(result["_wm_route"]["tool"], "memory.delete");
5310 assert!(store.get(Galaxy::Codex, id).unwrap().is_none());
5311 }
5312
5313 #[tokio::test]
5314 async fn wm_thought_cannot_reach_destructive_tool() {
5315 let store = test_store();
5316 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5317
5318 let wm = registry.get("wm").unwrap();
5319 let mut ctx = Context::new(BrainWave::Gamma);
5320 let result = wm
5323 .call(
5324 &mut ctx,
5325 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001"}),
5326 )
5327 .await
5328 .unwrap();
5329
5330 assert_eq!(result["status"], "error");
5331 assert!(
5332 result["message"]
5333 .as_str()
5334 .unwrap()
5335 .contains("cannot be reached via natural language"),
5336 "expected NLU hard-block message, got: {result}"
5337 );
5338 }
5339
5340 #[tokio::test]
5341 async fn wm_thought_cannot_reach_destructive_tool_even_with_confirm() {
5342 let store = test_store();
5343 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5344
5345 let wm = registry.get("wm").unwrap();
5346 let mut ctx = Context::new(BrainWave::Gamma);
5347 let result = wm
5351 .call(
5352 &mut ctx,
5353 json!({"thought": "delete memory 00000000-0000-0000-0000-000000000001", "args": {"confirm": true, "id": "00000000-0000-0000-0000-000000000001"}}),
5354 )
5355 .await
5356 .unwrap();
5357
5358 assert_eq!(result["status"], "error");
5359 assert!(
5360 result["message"]
5361 .as_str()
5362 .unwrap()
5363 .contains("cannot be reached via natural language")
5364 );
5365 }
5366
5367 #[tokio::test]
5372 async fn nlu_cannot_reach_any_destructive_tool() {
5373 let store = test_store();
5374 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5375 let wm = registry.get("wm").unwrap();
5376
5377 let destructive_tools: Vec<String> = registry
5380 .all_ref()
5381 .iter()
5382 .filter(|t| t.effects().destructive)
5383 .map(|t| t.name().to_string())
5384 .collect();
5385
5386 assert!(
5387 !destructive_tools.is_empty(),
5388 "registry must contain at least one destructive tool for this test to be meaningful"
5389 );
5390
5391 let mut ctx = Context::new(BrainWave::Gamma);
5392 for tool_name in &destructive_tools {
5393 let result = wm
5397 .call(
5398 &mut ctx,
5399 json!({
5400 "thought": tool_name,
5401 "args": {"confirm": true}
5402 }),
5403 )
5404 .await
5405 .unwrap();
5406
5407 let routed_tool = result
5412 .get("_wm_route")
5413 .and_then(|r| r.get("tool"))
5414 .and_then(|t| t.as_str())
5415 .unwrap_or("");
5416 let resolved_destructive = registry
5417 .get(routed_tool)
5418 .is_some_and(|t| t.effects().destructive);
5419 assert!(
5420 result["status"] != "success" || !resolved_destructive,
5421 "destructive tool '{tool_name}' executed via NLU (resolved as '{routed_tool}') — structural gate failed"
5422 );
5423
5424 if routed_tool == tool_name {
5427 assert!(
5428 result
5429 .get("message")
5430 .and_then(|m| m.as_str())
5431 .is_some_and(|m| m.contains("cannot be reached via natural language")),
5432 "destructive tool '{tool_name}' was routed to but gate message missing: {result}"
5433 );
5434 }
5435
5436 let nl_phrase = match tool_name.as_str() {
5441 "memory.delete" => "delete memory 00000000-0000-0000-0000-000000000001",
5442 "transaction.rollback" => "rollback the transaction",
5443 "galaxy.purge" => "purge galaxy codex",
5444 "galaxy.transfer" => "transfer galaxy codex to archive",
5445 "galaxy.restore" => "restore galaxy codex from snapshot",
5446 "memory.consolidate" => "consolidate memories in codex",
5447 "memory.deduplicate" => "deduplicate memories in codex",
5448 "karma.purge" => "purge karma ledger",
5449 "system.flush" => "flush low importance memories",
5450 "galaxy.cold_rotate" => "rotate telemetry noise to cold storage",
5451 _ => tool_name.as_str(),
5452 };
5453 let result2 = wm
5454 .call(&mut ctx, json!({"thought": nl_phrase}))
5455 .await
5456 .unwrap();
5457
5458 let routed_tool2 = result2
5459 .get("_wm_route")
5460 .and_then(|r| r.get("tool"))
5461 .and_then(|t| t.as_str())
5462 .unwrap_or("");
5463 let resolved_destructive2 = registry
5464 .get(routed_tool2)
5465 .is_some_and(|t| t.effects().destructive);
5466 assert!(
5467 result2["status"] != "success" || !resolved_destructive2,
5468 "destructive tool '{tool_name}' executed via NLU phrase '{nl_phrase}' (resolved as '{routed_tool2}') — structural gate failed"
5469 );
5470 if routed_tool2 == tool_name {
5471 assert!(
5472 result2
5473 .get("message")
5474 .and_then(|m| m.as_str())
5475 .is_some_and(|m| m.contains("cannot be reached via natural language")),
5476 "destructive tool '{tool_name}' was routed to via '{nl_phrase}' but gate message missing: {result2}"
5477 );
5478 }
5479 }
5480 }
5481
5482 #[tokio::test]
5483 async fn nlu_abstention_returns_error_for_unmatched_query() {
5484 let store = test_store();
5485 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5486 let wm = registry.get("wm").unwrap();
5487 let mut ctx = Context::new(BrainWave::Gamma);
5488
5489 let result = wm
5492 .call(&mut ctx, json!({"thought": "xyzzy quux blargh frobnicate"}))
5493 .await
5494 .unwrap();
5495
5496 assert_eq!(result["status"], "error");
5497 assert!(
5498 result
5499 .get("_wm_route")
5500 .and_then(|r| r.get("abstained"))
5501 .and_then(serde_json::Value::as_bool)
5502 .unwrap_or(false),
5503 "expected abstained=true, got: {result}"
5504 );
5505 assert!(
5506 result["message"]
5507 .as_str()
5508 .unwrap()
5509 .contains("Could not confidently match"),
5510 "expected abstention message, got: {result}"
5511 );
5512 }
5513
5514 #[tokio::test]
5515 async fn nlu_abstention_does_not_fire_for_explicit_route() {
5516 let store = test_store();
5517 let (registry, _pipeline) = test_registry_with_pipeline(&store);
5518 let wm = registry.get("wm").unwrap();
5519 let mut ctx = Context::new(BrainWave::Gamma);
5520
5521 let result = wm.call(&mut ctx, json!({"route": "gnosis"})).await.unwrap();
5524
5525 assert_eq!(result["status"], "success");
5526 assert!(
5527 !result
5528 .get("_wm_route")
5529 .and_then(|r| r.get("abstained"))
5530 .and_then(serde_json::Value::as_bool)
5531 .unwrap_or(false),
5532 "explicit route should not abstain, got: {result}"
5533 );
5534 }
5535
5536 struct FakeVecEmbedder;
5539
5540 impl wm_memory::Embedder for FakeVecEmbedder {
5541 fn embed_batch(&self, texts: &[&str]) -> wm_core::Result<Vec<Vec<f32>>> {
5542 Ok(texts
5543 .iter()
5544 .map(|t| {
5545 let mut v = vec![0.0_f32; 16];
5546 for (i, b) in t.bytes().take(16).enumerate() {
5547 v[i] = f32::from(b) / 255.0;
5548 }
5549 v
5550 })
5551 .collect())
5552 }
5553 fn dimension(&self) -> usize {
5554 16
5555 }
5556 fn is_available(&self) -> bool {
5557 true
5558 }
5559 fn backend_name(&self) -> &'static str {
5560 "fake"
5561 }
5562 }
5563
5564 #[tokio::test]
5565 async fn wm_classify_async_routes_off_thread_with_embedding_router() {
5566 let store = test_store();
5567 let registry = test_registry_with(&store);
5568 let shadow = std::sync::Arc::new(std::sync::RwLock::new(
5569 embedding_router::ShadowModeStats::default(),
5570 ));
5571 let router = embedding_router::EmbeddingRouter::with_descriptions(
5572 Box::new(FakeVecEmbedder),
5573 embedding_router::tool_descriptions(),
5574 )
5575 .expect("fake-embedder router should build");
5576 let mut meta = WmMetaTool::with_router_shadow_stats_and_pipeline(
5577 std::sync::Arc::new(registry),
5578 wm_memory::create_embedder(),
5579 shadow,
5580 None,
5581 );
5582 meta.embedding_router = Some(std::sync::Arc::new(router));
5583
5584 let (tool, conf, emb) = meta.classify_async("remember the meeting notes").await;
5587 assert!(!tool.is_empty());
5588 assert!(conf >= 0.0);
5589 assert!(
5590 emb.is_some(),
5591 "query embedding should be returned for OATS reuse"
5592 );
5593 }
5594
5595 #[tokio::test]
5596 async fn tools_list_shows_all() {
5597 let store = test_store();
5598 let registry = test_registry_with(&store);
5599 let registry = register_meta_tools(
5600 ®istry,
5601 &store,
5602 std::sync::Arc::new(std::sync::RwLock::new(
5603 embedding_router::ShadowModeStats::default(),
5604 )),
5605 );
5606
5607 let list = registry.get("tools.list").unwrap();
5608 let mut ctx = Context::new(BrainWave::Gamma);
5609 let result = list.call(&mut ctx, json!({})).await.unwrap();
5610
5611 assert_eq!(result["status"], "success");
5612 assert!(result["total"].as_u64().unwrap() >= 7);
5613 }
5614
5615 #[tokio::test]
5616 async fn tools_list_exposes_curated_argument_schemas() {
5617 let store = test_store();
5618 let registry = test_registry_with(&store);
5619 let registry = register_meta_tools(
5620 ®istry,
5621 &store,
5622 std::sync::Arc::new(std::sync::RwLock::new(
5623 embedding_router::ShadowModeStats::default(),
5624 )),
5625 );
5626
5627 let list = registry.get("tools.list").unwrap();
5628 let mut ctx = Context::new(BrainWave::Gamma);
5629 let result = list.call(&mut ctx, json!({})).await.unwrap();
5630
5631 let tools = result["tools"].as_array().unwrap();
5632 let create = tools
5633 .iter()
5634 .find(|t| t["name"] == "memory.create")
5635 .expect("tools.list must include memory.create");
5636 let schema = &create["input_schema"];
5637 assert_eq!(schema["type"], "object");
5638 assert!(
5639 schema["properties"].get("content").is_some(),
5640 "memory.create schema must describe content, got: {schema}"
5641 );
5642 assert!(
5643 schema["required"]
5644 .as_array()
5645 .unwrap()
5646 .iter()
5647 .any(|r| r == "content"),
5648 "memory.create schema must require content"
5649 );
5650
5651 let rollback = tools
5652 .iter()
5653 .find(|t| t["name"] == "transaction.rollback")
5654 .expect("tools.list must include transaction.rollback");
5655 assert!(
5656 rollback["input_schema"]["required"]
5657 .as_array()
5658 .unwrap()
5659 .iter()
5660 .any(|r| r == "confirm"),
5661 "transaction.rollback schema must require confirm"
5662 );
5663
5664 let annotations = &create["annotations"];
5666 assert_eq!(annotations["readOnlyHint"], false, "memory.create writes");
5667 assert_eq!(annotations["destructiveHint"], false);
5668 assert_eq!(
5669 rollback["annotations"]["destructiveHint"], true,
5670 "transaction.rollback is destructive"
5671 );
5672 let list_tool = tools
5673 .iter()
5674 .find(|t| t["name"] == "memory.list")
5675 .expect("tools.list must include memory.list");
5676 assert_eq!(
5677 list_tool["annotations"]["readOnlyHint"], true,
5678 "memory.list is read-only"
5679 );
5680 }
5681
5682 #[tokio::test]
5683 async fn tools_list_filters_by_brain_wave() {
5684 let store = test_store();
5685 let registry = test_registry_with(&store);
5686 let registry = register_meta_tools(
5687 ®istry,
5688 &store,
5689 std::sync::Arc::new(std::sync::RwLock::new(
5690 embedding_router::ShadowModeStats::default(),
5691 )),
5692 );
5693
5694 let list = registry.get("tools.list").unwrap();
5695
5696 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5698 let result_gamma = list.call(&mut ctx_gamma, json!({})).await.unwrap();
5699 let gamma_count = result_gamma["total"].as_u64().unwrap();
5700 assert!(gamma_count >= 7);
5701
5702 let mut ctx_alpha = Context::new(BrainWave::Alpha);
5704 let result_alpha = list.call(&mut ctx_alpha, json!({})).await.unwrap();
5705 let alpha_count = result_alpha["total"].as_u64().unwrap();
5706 assert!(alpha_count < gamma_count);
5707 assert!(alpha_count > 0);
5708
5709 let mut ctx_delta = Context::new(BrainWave::Delta);
5711 let result_delta = list.call(&mut ctx_delta, json!({})).await.unwrap();
5712 assert_eq!(result_delta["total"], 0);
5713 }
5714
5715 #[tokio::test]
5716 async fn gnosis_includes_brain_wave_and_tool_count() {
5717 let store = test_store();
5718 let registry = test_registry_with(&store);
5719 let registry = register_meta_tools(
5720 ®istry,
5721 &store,
5722 std::sync::Arc::new(std::sync::RwLock::new(
5723 embedding_router::ShadowModeStats::default(),
5724 )),
5725 );
5726
5727 let gnosis = registry.get("gnosis").unwrap();
5728 let mut ctx = Context::new(BrainWave::Gamma);
5729 let result = gnosis.call(&mut ctx, json!({})).await.unwrap();
5730
5731 assert_eq!(result["status"], "success");
5732 assert_eq!(result["brain_wave"], "Gamma");
5733 assert!(result["available_tools"].as_u64().unwrap() >= 9);
5734 }
5735
5736 #[tokio::test]
5737 async fn gnosis_available_tools_is_total_registered() {
5738 let store = test_store();
5739 let registry = test_registry_with(&store);
5740 let registry = register_meta_tools(
5741 ®istry,
5742 &store,
5743 std::sync::Arc::new(std::sync::RwLock::new(
5744 embedding_router::ShadowModeStats::default(),
5745 )),
5746 );
5747
5748 let gnosis = registry.get("gnosis").unwrap();
5749
5750 let mut ctx_gamma = Context::new(BrainWave::Gamma);
5753 let result_gamma = gnosis.call(&mut ctx_gamma, json!({})).await.unwrap();
5754 let gamma_tools = result_gamma["available_tools"].as_u64().unwrap();
5755
5756 let mut ctx_delta = Context::new(BrainWave::Delta);
5757 let result_delta = gnosis.call(&mut ctx_delta, json!({})).await.unwrap();
5758 let delta_tools = result_delta["available_tools"].as_u64().unwrap();
5759
5760 assert_eq!(gamma_tools, delta_tools);
5761 assert!(
5762 gamma_tools >= 9,
5763 "expected at least 9 registered tools, got {gamma_tools}"
5764 );
5765 }
5766
5767 #[tokio::test]
5768 async fn expansion_brings_tool_count_to_50() {
5769 let store = test_store();
5770 let registry = test_registry_with(&store);
5771 let registry = register_meta_tools(
5772 ®istry,
5773 &store,
5774 std::sync::Arc::new(std::sync::RwLock::new(
5775 embedding_router::ShadowModeStats::default(),
5776 )),
5777 );
5778
5779 let list = registry.get("tools.list").unwrap();
5780 let mut ctx = Context::new(BrainWave::Gamma);
5781 let result = list.call(&mut ctx, json!({})).await.unwrap();
5782
5783 let total = result["total"].as_u64().unwrap();
5784 assert!(
5785 total >= 50,
5786 "Expected 50+ tools after expansion, got {total}"
5787 );
5788 }
5789
5790 #[tokio::test]
5793 async fn nlu_routes_consolidate() {
5794 let (tool, conf) = WmMetaTool::classify("consolidate memories in codex");
5795 assert_eq!(tool, "memory.consolidate");
5796 assert!(conf > 0.0);
5797 }
5798
5799 #[tokio::test]
5800 async fn nlu_routes_decay() {
5801 let (tool, conf) = WmMetaTool::classify("decay old memories");
5802 assert_eq!(tool, "memory.decay");
5803 assert!(conf > 0.0);
5804 }
5805
5806 #[tokio::test]
5807 async fn nlu_routes_batch_read() {
5808 let (tool, conf) = WmMetaTool::classify("batch read these memories");
5809 assert_eq!(tool, "memory.batch_read");
5810 assert!(conf > 0.0);
5811 }
5812
5813 #[tokio::test]
5814 async fn nlu_routes_update() {
5815 let (tool, conf) = WmMetaTool::classify("update memory tags");
5816 assert_eq!(tool, "memory.update");
5817 assert!(conf > 0.0);
5818 }
5819
5820 #[tokio::test]
5821 async fn nlu_routes_tag() {
5822 let (tool, conf) = WmMetaTool::classify("add tag to memory");
5823 assert_eq!(tool, "memory.tag");
5824 assert!(conf > 0.0);
5825 }
5826
5827 #[tokio::test]
5828 async fn nlu_routes_memory_stats() {
5829 let (tool, conf) = WmMetaTool::classify("memory stats for codex");
5830 assert_eq!(tool, "memory.stats");
5831 assert!(conf > 0.0);
5832 }
5833
5834 #[tokio::test]
5835 async fn nlu_routes_hybrid_recall() {
5836 let (tool, conf) = WmMetaTool::classify("hybrid recall for rust");
5837 assert_eq!(tool, "memory.hybrid_recall");
5838 assert!(conf > 0.0);
5839 }
5840
5841 #[tokio::test]
5842 async fn nlu_routes_count() {
5843 let (tool, conf) = WmMetaTool::classify("count memories in codex");
5844 assert_eq!(tool, "memory.count");
5845 assert!(conf > 0.0);
5846 }
5847
5848 #[tokio::test]
5849 async fn nlu_routes_tags() {
5850 let (tool, conf) = WmMetaTool::classify("list tags in codex");
5851 assert_eq!(tool, "memory.tags");
5852 assert!(conf > 0.0);
5853 }
5854
5855 #[tokio::test]
5856 async fn nlu_routes_associate_mine() {
5857 let (tool, conf) = WmMetaTool::classify("mine associations in codex");
5858 assert_eq!(tool, "memory.associate_mine");
5859 assert!(conf > 0.0);
5860 }
5861
5862 #[tokio::test]
5863 async fn nlu_routes_session_start() {
5864 let (tool, conf) = WmMetaTool::classify("start session research");
5865 assert_eq!(tool, "session.start");
5866 assert!(conf > 0.0);
5867 }
5868
5869 #[tokio::test]
5870 async fn nlu_routes_session_end() {
5871 let (tool, conf) = WmMetaTool::classify("end session 12345");
5872 assert_eq!(tool, "session.end");
5873 assert!(conf > 0.0);
5874 }
5875
5876 #[tokio::test]
5877 async fn nlu_routes_session_list() {
5878 let (tool, conf) = WmMetaTool::classify("list sessions");
5879 assert_eq!(tool, "session.list");
5880 assert!(conf > 0.0);
5881 }
5882
5883 #[tokio::test]
5884 async fn nlu_routes_citta_status() {
5885 let (tool, conf) = WmMetaTool::classify("citta status");
5886 assert_eq!(tool, "citta.status");
5887 assert!(conf > 0.0);
5888 }
5889
5890 #[tokio::test]
5891 async fn nlu_routes_citta_reflect() {
5892 let (tool, conf) = WmMetaTool::classify("reflect on recent events");
5893 assert_eq!(tool, "citta.reflect");
5894 assert!(conf > 0.0);
5895 }
5896
5897 #[tokio::test]
5898 async fn nlu_routes_coherence() {
5899 let (tool, conf) = WmMetaTool::classify("check coherence");
5900 assert_eq!(tool, "citta.coherence");
5901 assert!(conf > 0.0);
5902 }
5903
5904 #[tokio::test]
5905 async fn nlu_routes_dream_status() {
5906 let (tool, conf) = WmMetaTool::classify("dream cycle status");
5907 assert_eq!(tool, "dream.status");
5908 assert!(conf > 0.0);
5909 }
5910
5911 #[tokio::test]
5912 async fn nlu_routes_dream_trigger() {
5913 let (tool, conf) = WmMetaTool::classify("trigger dream cycle");
5914 assert_eq!(tool, "dream.trigger");
5915 assert!(conf > 0.0);
5916 }
5917
5918 #[tokio::test]
5919 async fn nlu_routes_effectiveness() {
5920 let (tool, conf) = WmMetaTool::classify("tool effectiveness report");
5921 assert_eq!(tool, "tools.effectiveness_report");
5922 assert!(conf > 0.0);
5923 }
5924
5925 #[tokio::test]
5926 async fn nlu_routes_retire() {
5927 let (tool, conf) = WmMetaTool::classify("retire tool memory.old");
5928 assert_eq!(tool, "tools.retire");
5929 assert!(conf > 0.0);
5930 }
5931
5932 #[tokio::test]
5933 async fn nlu_routes_pattern_search() {
5934 let (tool, conf) = WmMetaTool::classify("pattern search for rust");
5935 assert_eq!(tool, "pattern.search");
5936 assert!(conf > 0.0);
5937 }
5938
5939 #[tokio::test]
5940 async fn nlu_routes_salience() {
5941 let (tool, conf) = WmMetaTool::classify("salience spotlight");
5942 assert_eq!(tool, "salience.spotlight");
5943 assert!(conf > 0.0);
5944 }
5945
5946 #[tokio::test]
5947 async fn nlu_routes_serendipity() {
5948 let (tool, conf) = WmMetaTool::classify("serendipity surface");
5949 assert_eq!(tool, "serendipity.surface");
5950 assert!(conf > 0.0);
5951 }
5952
5953 #[tokio::test]
5954 async fn nlu_routes_constellation_detect() {
5955 let (tool, conf) = WmMetaTool::classify("detect clusters");
5956 assert_eq!(tool, "constellation.detect");
5957 assert!(conf > 0.0);
5958 }
5959
5960 #[tokio::test]
5961 async fn nlu_routes_constellation_list() {
5962 let (tool, conf) = WmMetaTool::classify("list constellations");
5963 assert_eq!(tool, "constellation.list");
5964 assert!(conf > 0.0);
5965 }
5966
5967 #[tokio::test]
5968 async fn nlu_routes_galaxy_stats() {
5969 let (tool, conf) = WmMetaTool::classify("galaxy stats");
5970 assert_eq!(tool, "galaxy.stats");
5971 assert!(conf > 0.0);
5972 }
5973
5974 #[tokio::test]
5975 async fn nlu_routes_galaxy_export() {
5976 let (tool, conf) = WmMetaTool::classify("export galaxy codex");
5977 assert_eq!(tool, "galaxy.export");
5978 assert!(conf > 0.0);
5979 }
5980
5981 #[tokio::test]
5982 async fn nlu_routes_galaxy_import() {
5983 let (tool, conf) = WmMetaTool::classify("import galaxy codex");
5984 assert_eq!(tool, "galaxy.import");
5985 assert!(conf > 0.0);
5986 }
5987
5988 #[tokio::test]
5989 async fn nlu_routes_karma_history() {
5990 let (tool, conf) = WmMetaTool::classify("karma history");
5991 assert_eq!(tool, "karma.history");
5992 assert!(conf > 0.0);
5993 }
5994
5995 #[tokio::test]
5996 async fn nlu_routes_karma_clear() {
5997 let (tool, conf) = WmMetaTool::classify("clear karma");
5998 assert_eq!(tool, "karma.clear");
5999 assert!(conf > 0.0);
6000 }
6001
6002 #[tokio::test]
6003 async fn nlu_routes_dharma_rules() {
6004 let (tool, conf) = WmMetaTool::classify("dharma rules");
6005 assert_eq!(tool, "dharma.rules");
6006 assert!(conf > 0.0);
6007 }
6008
6009 #[tokio::test]
6010 async fn nlu_routes_dharma_audit() {
6011 let (tool, conf) = WmMetaTool::classify("dharma audit");
6012 assert_eq!(tool, "dharma.audit");
6013 assert!(conf > 0.0);
6014 }
6015
6016 #[tokio::test]
6017 async fn nlu_routes_dharma_profiles() {
6018 let (tool, conf) = WmMetaTool::classify("dharma profiles");
6019 assert_eq!(tool, "dharma.profiles");
6020 assert!(conf > 0.0);
6021 }
6022
6023 #[tokio::test]
6024 async fn nlu_routes_agent_register() {
6025 let (tool, conf) = WmMetaTool::classify("register agent worker-1");
6026 assert_eq!(tool, "agent.register");
6027 assert!(conf > 0.0);
6028 }
6029
6030 #[tokio::test]
6031 async fn nlu_routes_agent_list() {
6032 let (tool, conf) = WmMetaTool::classify("list agents");
6033 assert_eq!(tool, "agent.list");
6034 assert!(conf > 0.0);
6035 }
6036
6037 #[tokio::test]
6038 async fn nlu_routes_agent_heartbeat() {
6039 let (tool, conf) = WmMetaTool::classify("heartbeat for agent");
6040 assert_eq!(tool, "agent.heartbeat");
6041 assert!(conf > 0.0);
6042 }
6043
6044 #[tokio::test]
6045 async fn nlu_routes_task_distribute() {
6046 let (tool, conf) = WmMetaTool::classify("distribute task analyze data");
6047 assert_eq!(tool, "task.distribute");
6048 assert!(conf > 0.0);
6049 }
6050
6051 #[tokio::test]
6052 async fn nlu_routes_task_status() {
6053 let (tool, conf) = WmMetaTool::classify("task status");
6054 assert_eq!(tool, "task.status");
6055 assert!(conf > 0.0);
6056 }
6057
6058 #[tokio::test]
6059 async fn nlu_routes_system_health() {
6060 let (tool, conf) = WmMetaTool::classify("system health check");
6061 assert_eq!(tool, "system.health");
6062 assert!(conf > 0.0);
6063 }
6064
6065 #[tokio::test]
6066 async fn nlu_routes_system_config() {
6067 let (tool, conf) = WmMetaTool::classify("system config");
6068 assert_eq!(tool, "system.config");
6069 assert!(conf > 0.0);
6070 }
6071
6072 #[tokio::test]
6073 async fn nlu_routes_system_flush() {
6074 let (tool, conf) = WmMetaTool::classify("flush old memories");
6075 assert_eq!(tool, "system.flush");
6076 assert!(conf > 0.0);
6077 }
6078
6079 #[tokio::test]
6080 async fn nlu_routes_memory_nearby() {
6081 let (tool, conf) = WmMetaTool::classify("nearby memories in codex");
6082 assert_eq!(tool, "memory.nearby");
6083 assert!(conf > 0.0);
6084 }
6085
6086 #[tokio::test]
6087 async fn nlu_routes_empty_to_gnosis() {
6088 let (tool, conf) = WmMetaTool::classify("");
6089 assert_eq!(tool, "gnosis");
6090 assert_eq!(conf, 0.0);
6091 }
6092
6093 #[tokio::test]
6094 async fn nlu_routes_unknown_to_gnosis() {
6095 let (tool, conf) = WmMetaTool::classify("xyzzy frobnicate");
6096 assert_eq!(tool, "gnosis");
6097 assert_eq!(conf, 0.0);
6098 }
6099
6100 #[tokio::test]
6101 async fn nlu_extract_payload_memory_search() {
6102 let (param, value) =
6103 WmMetaTool::extract_payload("search for rust patterns", "memory.search").unwrap();
6104 assert_eq!(param, "query");
6105 assert_eq!(value, "rust patterns");
6106 }
6107
6108 #[tokio::test]
6109 async fn nlu_extract_payload_session_start() {
6110 let (param, value) =
6114 WmMetaTool::extract_payload("start session research", "session.start").unwrap();
6115 assert_eq!(param, "title");
6116 assert_eq!(value, "research");
6117 }
6118
6119 #[tokio::test]
6120 async fn nlu_extract_payload_agent_register() {
6121 let (param, value) =
6122 WmMetaTool::extract_payload("register agent worker-1", "agent.register").unwrap();
6123 assert_eq!(param, "name");
6124 assert_eq!(value, "worker-1");
6125 }
6126
6127 #[tokio::test]
6128 async fn nlu_extract_payload_task_distribute() {
6129 let (param, value) =
6130 WmMetaTool::extract_payload("distribute task analyze data", "task.distribute").unwrap();
6131 assert_eq!(param, "task");
6132 assert_eq!(value, "analyze data");
6133 }
6134
6135 #[tokio::test]
6136 async fn nlu_count_unique_patterns() {
6137 let inputs = [
6139 "remember",
6140 "recall",
6141 "list memories",
6142 "delete memory",
6143 "search",
6144 "query",
6145 "associate",
6146 "associations",
6147 "consolidate",
6148 "decay",
6149 "batch read",
6150 "update memory",
6151 "tag memory",
6152 "memory stats",
6153 "hybrid recall",
6154 "count memories",
6155 "list tags",
6156 "mine associations",
6157 "start session",
6158 "checkpoint",
6159 "recall session",
6160 "end session",
6161 "list sessions",
6162 "citta status",
6163 "reflect",
6164 "coherence",
6165 "dream status",
6166 "trigger dream",
6167 "effectiveness",
6168 "retire tool",
6169 "pattern search",
6170 "salience",
6171 "serendipity",
6172 "detect clusters",
6173 "list constellations",
6174 "galaxy stats",
6175 "export galaxy",
6176 "import galaxy",
6177 "karma",
6178 "karma history",
6179 "clear karma",
6180 "dharma rules",
6181 "dharma audit",
6182 "dharma profiles",
6183 "dharma",
6184 "register agent",
6185 "list agents",
6186 "heartbeat",
6187 "distribute task",
6188 "task status",
6189 "system health",
6190 "system config",
6191 "flush",
6192 "tools",
6193 "nearby memories",
6194 ];
6195 let mut tools: std::collections::HashSet<&str> = std::collections::HashSet::new();
6196 for input in &inputs {
6197 let (tool, _) = WmMetaTool::classify(input);
6198 tools.insert(tool);
6199 }
6200 assert!(
6202 tools.len() >= 30,
6203 "Expected 30+ unique NLU targets, got {}",
6204 tools.len()
6205 );
6206 }
6207
6208 #[tokio::test]
6209 async fn nlu_routes_shadow_report() {
6210 let (tool, conf) = WmMetaTool::classify("shadow mode disagreement report");
6211 assert_eq!(tool, "nlu.shadow_report");
6212 assert!(conf > 0.0);
6213 }
6214
6215 #[tokio::test]
6216 async fn nlu_routes_oats_report() {
6217 let (tool, conf) = WmMetaTool::classify("oats disagreement nlu router");
6218 assert_eq!(tool, "nlu.shadow_report");
6219 assert!(conf > 0.0);
6220 }
6221
6222 #[test]
6225 fn glyph_roundtrip_known_codes() {
6226 let raw = json!({"route": "memory.search", "args": {"query": "x", "limit": 3}});
6227 let encoded = encode_glyph("memory.search", &json!({"query": "x", "limit": 3}));
6228 assert_eq!(encoded["r"], "Ms");
6229 assert_eq!(encoded["a"]["q"], "x");
6230 assert_eq!(encoded["a"]["n"], 3);
6231 let decoded = decode_glyph(&encoded).expect("glyph input must decode");
6232 assert_eq!(decoded["route"], raw["route"]);
6233 assert_eq!(decoded["args"]["query"], "x");
6234 assert_eq!(decoded["args"]["limit"], 3);
6235 }
6236
6237 #[test]
6238 fn glyph_unknown_codes_pass_through() {
6239 let weird = json!({"r": "not-a-code", "a": {"zzz": 1}});
6240 assert!(decode_glyph(&weird).is_none(), "unknown route code refuses");
6241 let partial = json!({"r": "Ms", "a": {"zzz": 1}});
6242 let decoded = decode_glyph(&partial).expect("known route decodes");
6243 assert_eq!(decoded["args"]["zzz"], 1, "unknown arg code passes through");
6244 assert_eq!(decode_glyph(&json!({"thought": "hi"})), None);
6245 }
6246
6247 #[test]
6248 fn glyph_book_covers_measured_routes() {
6249 for route in [
6251 "memory.search",
6252 "memory.create",
6253 "session.record",
6254 "session.continuity",
6255 "dharma.escalate",
6256 "graph.walk",
6257 "tools.list",
6258 "citta.status",
6259 ] {
6260 assert!(
6261 glyph_lookup(GLYPH_ROUTES, route).is_some(),
6262 "missing {route}"
6263 );
6264 }
6265 }
6266
6267 #[test]
6268 fn glyph_logographic_ideograms_decode_losslessly() {
6269 let search_call = json!({
6271 "r": "忆",
6272 "a": {
6273 "问": "auth failure",
6274 "数": 5
6275 }
6276 });
6277 let decoded = decode_glyph(&search_call).expect("logographic search decodes");
6278 assert_eq!(decoded["route"], "memory.search");
6279 assert_eq!(decoded["args"]["query"], "auth failure");
6280 assert_eq!(decoded["args"]["limit"], 5);
6281
6282 let checkpoint_call = json!({
6283 "r": "契",
6284 "a": {
6285 "文": "v9.3 milestone reached"
6286 }
6287 });
6288 let decoded_cp = decode_glyph(&checkpoint_call).expect("checkpoint decodes");
6289 assert_eq!(decoded_cp["route"], "session.checkpoint");
6290 assert_eq!(decoded_cp["args"]["content"], "v9.3 milestone reached");
6291
6292 let status_call = json!({"r": "心", "a": {}});
6293 let decoded_st = decode_glyph(&status_call).expect("citta status decodes");
6294 assert_eq!(decoded_st["route"], "citta.status");
6295 }
6296
6297 #[test]
6298 fn lkep_expression_decodes_and_normalizes() {
6299 let (route, args) =
6301 decode_lkep(&json!("忆(问=\"deadlock\", 数=3)")).expect("LKEP string decodes");
6302 assert_eq!(route, "memory.search");
6303 assert_eq!(args["query"], "deadlock");
6304 assert_eq!(args["limit"], 3);
6305
6306 let (route2, args2) =
6308 decode_lkep(&json!("忆: memory corruption")).expect("colon syntax decodes");
6309 assert_eq!(route2, "memory.search");
6310 assert_eq!(args2["query"], "memory corruption");
6311
6312 let (route3, args3) = decode_lkep(&json!("律")).expect("bare route decodes");
6314 assert_eq!(route3, "dharma.rules");
6315 assert_eq!(args3, json!({}));
6316
6317 let (route4, args4) =
6319 decode_lkep(&json!({"忆": "fast lookup"})).expect("root ideogram decodes");
6320 assert_eq!(route4, "memory.search");
6321 assert_eq!(args4["query"], "fast lookup");
6322 }
6323}