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