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