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