1#![forbid(unsafe_code)]
4
5use async_trait::async_trait;
6
7use serde_json::{Value, json};
8use std::sync::Arc;
9use wm_core::{Context, EffectRow, Gana, Resource, Tool, ToolStats};
10use wm_memory::{AssociationStore, MemoryStore};
11
12pub struct SpiralReportTool {
13 tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
14 stats: ToolStats,
15 effects: EffectRow,
16}
17
18impl SpiralReportTool {
19 pub fn new(tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>) -> Self {
20 Self {
21 tracker,
22 stats: ToolStats::default(),
23 effects: EffectRow::pure(),
24 }
25 }
26}
27
28#[async_trait]
29impl Tool for SpiralReportTool {
30 fn name(&self) -> &str {
31 "spiral.report"
32 }
33 fn gana(&self) -> Gana {
34 Gana::Encampment
35 }
36 fn effects(&self) -> &EffectRow {
37 &self.effects
38 }
39 fn description(&self) -> &str {
40 "Report on autonomy expansion or circling (spiral direction, novelty, suspensions)"
41 }
42 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
43 let report = {
44 let tracker = self
45 .tracker
46 .lock()
47 .map_err(|e| wm_core::CoreError::Internal(format!("spiral tracker lock: {e}")))?;
48 tracker.report()
49 };
50 Ok(report.to_json())
51 }
52 fn stats(&self) -> &ToolStats {
53 &self.stats
54 }
55}
56
57pub struct ConsolidationConnectTool {
63 store: Arc<MemoryStore>,
64 associations: Arc<AssociationStore>,
65 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
66 stats: ToolStats,
67 effects: EffectRow,
68}
69
70impl ConsolidationConnectTool {
71 pub fn new(
72 store: Arc<MemoryStore>,
73 associations: Arc<AssociationStore>,
74 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
75 ) -> Self {
76 Self {
77 store,
78 associations,
79 spiral_tracker,
80 stats: ToolStats::default(),
81 effects: EffectRow {
82 reads: super::common::memory_galaxy_reads(),
85 writes: vec![Resource::Galaxy("substrate".into())],
86 ..Default::default()
87 },
88 }
89 }
90}
91
92#[async_trait]
93impl Tool for ConsolidationConnectTool {
94 fn name(&self) -> &str {
95 "consolidation.connect"
96 }
97 fn gana(&self) -> Gana {
98 Gana::Encampment
99 }
100 fn effects(&self) -> &EffectRow {
101 &self.effects
102 }
103 fn description(&self) -> &str {
104 "Propose typed associations for disconnected memories (gated, human review)"
105 }
106 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
107 let health_score = args
108 .get("health_score")
109 .and_then(Value::as_f64)
110 .unwrap_or(0.8) as f32;
111
112 let mut runner = wm_cognitive::AutonomousCycleRunner::default();
113 let cycle_ctx =
114 wm_cognitive::CycleContext::new(&self.store, &self.associations, health_score);
115 let result = runner.run_cycle(wm_cognitive::CycleType::Connect, &cycle_ctx);
116
117 if let Ok(mut tracker) = self.spiral_tracker.lock() {
119 tracker.record(&result);
120 }
121
122 Ok(json!({
123 "status": "success",
124 "cycle": result.cycle.name(),
125 "cycle_status": format!("{:?}", result.status),
126 "purpose": result.purpose,
127 "memories_scanned": result.memories_scanned,
128 "proposals_generated": result.proposals_generated,
129 "duration_ms": result.duration_ms,
130 "requires_human_review": true,
131 "notes": result.notes,
132 "connections": result.connections,
133 }))
134 }
135 fn stats(&self) -> &ToolStats {
136 &self.stats
137 }
138}
139
140pub struct ConsolidationCompressTool {
146 store: Arc<MemoryStore>,
147 associations: Arc<AssociationStore>,
148 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
149 stats: ToolStats,
150 effects: EffectRow,
151}
152
153impl ConsolidationCompressTool {
154 pub fn new(
155 store: Arc<MemoryStore>,
156 associations: Arc<AssociationStore>,
157 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
158 ) -> Self {
159 Self {
160 store,
161 associations,
162 spiral_tracker,
163 stats: ToolStats::default(),
164 effects: EffectRow {
165 reads: super::common::memory_galaxy_reads(),
168 writes: vec![Resource::Galaxy("substrate".into())],
169 ..Default::default()
170 },
171 }
172 }
173}
174
175#[async_trait]
176impl Tool for ConsolidationCompressTool {
177 fn name(&self) -> &str {
178 "consolidation.compress"
179 }
180 fn gana(&self) -> Gana {
181 Gana::Encampment
182 }
183 fn effects(&self) -> &EffectRow {
184 &self.effects
185 }
186 fn description(&self) -> &str {
187 "Propose merging semantically overlapping memories (gated, human review)"
188 }
189 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
190 let health_score = args
191 .get("health_score")
192 .and_then(Value::as_f64)
193 .unwrap_or(0.8) as f32;
194
195 let mut runner = wm_cognitive::AutonomousCycleRunner::default();
196 let cycle_ctx =
197 wm_cognitive::CycleContext::new(&self.store, &self.associations, health_score);
198 let result = runner.run_cycle(wm_cognitive::CycleType::Compress, &cycle_ctx);
199
200 if let Ok(mut tracker) = self.spiral_tracker.lock() {
202 tracker.record(&result);
203 }
204
205 Ok(json!({
206 "status": "success",
207 "cycle": result.cycle.name(),
208 "cycle_status": format!("{:?}", result.status),
209 "purpose": result.purpose,
210 "memories_scanned": result.memories_scanned,
211 "proposals_generated": result.proposals_generated,
212 "duration_ms": result.duration_ms,
213 "requires_human_review": true,
214 "notes": result.notes,
215 "compressions": result.compressions,
216 }))
217 }
218 fn stats(&self) -> &ToolStats {
219 &self.stats
220 }
221}
222
223pub struct EmergenceScanTool {
229 store: Arc<MemoryStore>,
230 associations: Arc<AssociationStore>,
231 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
232 stats: ToolStats,
233 effects: EffectRow,
234}
235
236impl EmergenceScanTool {
237 pub fn new(
238 store: Arc<MemoryStore>,
239 associations: Arc<AssociationStore>,
240 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
241 ) -> Self {
242 Self {
243 store,
244 associations,
245 spiral_tracker,
246 stats: ToolStats::default(),
247 effects: EffectRow {
248 reads: super::common::memory_galaxy_reads(),
251 writes: vec![Resource::Galaxy("substrate".into())],
252 ..Default::default()
253 },
254 }
255 }
256}
257
258#[async_trait]
259impl Tool for EmergenceScanTool {
260 fn name(&self) -> &str {
261 "emergence.scan"
262 }
263 fn gana(&self) -> Gana {
264 Gana::Encampment
265 }
266 fn effects(&self) -> &EffectRow {
267 &self.effects
268 }
269 fn description(&self) -> &str {
270 "Detect tag/topic emergence patterns across memories (gated, logged)"
271 }
272 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
273 let health_score = args
274 .get("health_score")
275 .and_then(Value::as_f64)
276 .unwrap_or(0.8) as f32;
277
278 let mut runner = wm_cognitive::AutonomousCycleRunner::default();
279 let cycle_ctx =
280 wm_cognitive::CycleContext::new(&self.store, &self.associations, health_score);
281 let result = runner.run_cycle(wm_cognitive::CycleType::Emergence, &cycle_ctx);
282
283 if let Ok(mut tracker) = self.spiral_tracker.lock() {
285 tracker.record(&result);
286 }
287
288 Ok(json!({
289 "status": "success",
290 "cycle": result.cycle.name(),
291 "cycle_status": format!("{:?}", result.status),
292 "purpose": result.purpose,
293 "memories_scanned": result.memories_scanned,
294 "proposals_generated": result.proposals_generated,
295 "duration_ms": result.duration_ms,
296 "requires_human_review": false,
297 "notes": result.notes,
298 "emergences": result.emergences,
299 }))
300 }
301 fn stats(&self) -> &ToolStats {
302 &self.stats
303 }
304}
305
306pub struct RetentionPruneTool {
312 store: Arc<MemoryStore>,
313 associations: Arc<AssociationStore>,
314 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
315 stats: ToolStats,
316 effects: EffectRow,
317}
318
319impl RetentionPruneTool {
320 pub fn new(
321 store: Arc<MemoryStore>,
322 associations: Arc<AssociationStore>,
323 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
324 ) -> Self {
325 Self {
326 store,
327 associations,
328 spiral_tracker,
329 stats: ToolStats::default(),
330 effects: EffectRow {
331 reads: super::common::memory_galaxy_reads(),
334 writes: vec![Resource::Galaxy("substrate".into())],
335 ..Default::default()
336 },
337 }
338 }
339}
340
341#[async_trait]
342impl Tool for RetentionPruneTool {
343 fn name(&self) -> &str {
344 "retention.prune"
345 }
346 fn gana(&self) -> Gana {
347 Gana::Encampment
348 }
349 fn effects(&self) -> &EffectRow {
350 &self.effects
351 }
352 fn description(&self) -> &str {
353 "Identify memories ready for forgetting based on decay + neuro_score (gated, human review)"
354 }
355 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
356 let health_score = args
357 .get("health_score")
358 .and_then(Value::as_f64)
359 .unwrap_or(0.8) as f32;
360
361 let mut runner = wm_cognitive::AutonomousCycleRunner::default();
362 let cycle_ctx =
363 wm_cognitive::CycleContext::new(&self.store, &self.associations, health_score);
364 let result = runner.run_cycle(wm_cognitive::CycleType::Prune, &cycle_ctx);
365
366 if let Ok(mut tracker) = self.spiral_tracker.lock() {
368 tracker.record(&result);
369 }
370
371 Ok(json!({
372 "status": "success",
373 "cycle": result.cycle.name(),
374 "cycle_status": format!("{:?}", result.status),
375 "purpose": result.purpose,
376 "memories_scanned": result.memories_scanned,
377 "proposals_generated": result.proposals_generated,
378 "duration_ms": result.duration_ms,
379 "requires_human_review": true,
380 "notes": result.notes,
381 "prunes": result.prunes,
382 }))
383 }
384 fn stats(&self) -> &ToolStats {
385 &self.stats
386 }
387}
388
389pub struct SensorimotorScanTool {
396 store: Arc<MemoryStore>,
397 associations: Arc<AssociationStore>,
398 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
399 sensorimotor_bus: Arc<std::sync::Mutex<wm_substrate::sensorimotor::SensorimotorBus>>,
400 reflex_loop: Arc<std::sync::Mutex<wm_substrate::sensorimotor::ReflexLoop>>,
401 stats: ToolStats,
402 effects: EffectRow,
403}
404
405impl SensorimotorScanTool {
406 pub fn new(
407 store: Arc<MemoryStore>,
408 associations: Arc<AssociationStore>,
409 spiral_tracker: Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
410 sensorimotor_bus: Arc<std::sync::Mutex<wm_substrate::sensorimotor::SensorimotorBus>>,
411 reflex_loop: Arc<std::sync::Mutex<wm_substrate::sensorimotor::ReflexLoop>>,
412 ) -> Self {
413 Self {
414 store,
415 associations,
416 spiral_tracker,
417 sensorimotor_bus,
418 reflex_loop,
419 stats: ToolStats::default(),
420 effects: EffectRow {
421 reads: super::common::memory_galaxy_reads(),
424 writes: vec![Resource::Galaxy("substrate".into())],
425 ..Default::default()
426 },
427 }
428 }
429}
430
431#[async_trait]
432impl Tool for SensorimotorScanTool {
433 fn name(&self) -> &str {
434 "sensorimotor.scan"
435 }
436 fn gana(&self) -> Gana {
437 Gana::Encampment
438 }
439 fn effects(&self) -> &EffectRow {
440 &self.effects
441 }
442 fn description(&self) -> &str {
443 "Poll sensors, evaluate reflex rules, and execute triggered actuator commands (gated, logged)"
444 }
445 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
446 let health_score = args
447 .get("health_score")
448 .and_then(Value::as_f64)
449 .unwrap_or(0.8) as f32;
450
451 let mut runner = wm_cognitive::AutonomousCycleRunner::default();
452 let cycle_ctx =
453 wm_cognitive::CycleContext::new(&self.store, &self.associations, health_score)
454 .with_sensorimotor(&self.sensorimotor_bus, &self.reflex_loop);
455
456 let result = runner.run_cycle(wm_cognitive::CycleType::Sensorimotor, &cycle_ctx);
457
458 if let Ok(mut tracker) = self.spiral_tracker.lock() {
459 tracker.record(&result);
460 }
461
462 Ok(json!({
463 "status": "success",
464 "cycle": result.cycle.name(),
465 "cycle_status": format!("{:?}", result.status),
466 "purpose": result.purpose,
467 "memories_scanned": result.memories_scanned,
468 "proposals_generated": result.proposals_generated,
469 "duration_ms": result.duration_ms,
470 "requires_human_review": false,
471 "notes": result.notes,
472 "sensorimotor": result.sensorimotor,
473 }))
474 }
475 fn stats(&self) -> &ToolStats {
476 &self.stats
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use wm_core::Galaxy;
484 use wm_memory::Memory;
485
486 fn parts() -> (
487 tempfile::TempDir,
488 Arc<MemoryStore>,
489 Arc<AssociationStore>,
490 Arc<std::sync::Mutex<wm_cognitive::SpiralTracker>>,
491 ) {
492 let tmp = tempfile::tempdir().unwrap();
493 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
494 let assoc = Arc::new(AssociationStore::open(store.env()).unwrap());
495 let tracker = Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
496 (tmp, store, assoc, tracker)
497 }
498
499 #[tokio::test]
500 async fn spiral_report_reflects_recorded_cycles() {
501 let tracker = Arc::new(std::sync::Mutex::new(wm_cognitive::SpiralTracker::default()));
502 tracker
503 .lock()
504 .unwrap()
505 .record(&wm_cognitive::CycleResult::new(
506 wm_cognitive::CycleType::Connect,
507 wm_cognitive::CycleStatus::Completed,
508 ));
509
510 let result = SpiralReportTool::new(tracker)
511 .call(&mut Context::default(), json!({}))
512 .await
513 .unwrap();
514 assert_eq!(result["total_cycles_run"], 1);
515 assert_eq!(result["cycles"][0]["cycle"], "consolidation.connect");
516 }
517
518 #[tokio::test]
519 async fn connect_tool_plumbs_the_health_gate_before_touching_memories() {
520 let (_tmp, store, assoc, tracker) = parts();
521 let mut mem1 = Memory::new(Galaxy::Codex, "Rust algorithm data structure".into());
522 mem1.metadata.tags = vec!["rust".into(), "algorithm".into()];
523 store.put_semantic(Galaxy::Codex, &mut mem1).unwrap();
524 let mut mem2 = Memory::new(Galaxy::Codex, "Rust algorithm data method".into());
525 mem2.metadata.tags = vec!["rust".into(), "algorithm".into()];
526 store.put_semantic(Galaxy::Codex, &mut mem2).unwrap();
527
528 let tool = ConsolidationConnectTool::new(store, assoc, tracker);
529 let gated = tool
530 .call(&mut Context::default(), json!({"health_score": 0.1}))
531 .await
532 .unwrap();
533 assert_eq!(gated["cycle_status"], "SkippedHealth");
534 assert_eq!(gated["memories_scanned"], 0);
535 assert_eq!(gated["connections"].as_array().unwrap().len(), 0);
536 assert_eq!(gated["requires_human_review"], true);
537
538 let ran = tool
539 .call(&mut Context::default(), json!({"health_score": 0.9}))
540 .await
541 .unwrap();
542 assert_eq!(ran["cycle_status"], "Completed");
543 assert!(!ran["connections"].as_array().unwrap().is_empty());
544 assert_eq!(ran["requires_human_review"], true);
545 }
546
547 #[tokio::test]
548 async fn compress_tool_reports_primary_by_importance() {
549 let (_tmp, store, assoc, tracker) = parts();
550 let mut mem1 = Memory::new(Galaxy::Codex, "algorithm data structure rust".into());
551 mem1.metadata.importance = 0.8;
552 mem1.metadata.tags = vec!["rust".into(), "algorithm".into()];
553 store.put_semantic(Galaxy::Codex, &mut mem1).unwrap();
554 let mut mem2 = Memory::new(Galaxy::Codex, "algorithm data method rust".into());
555 mem2.metadata.importance = 0.3;
556 mem2.metadata.tags = vec!["rust".into(), "algorithm".into()];
557 store.put_semantic(Galaxy::Codex, &mut mem2).unwrap();
558
559 let result = ConsolidationCompressTool::new(store, assoc, tracker)
560 .call(&mut Context::default(), json!({"health_score": 0.9}))
561 .await
562 .unwrap();
563 assert_eq!(result["cycle_status"], "Completed");
564 assert_eq!(result["requires_human_review"], true);
565 assert_eq!(
566 result["compressions"][0]["primary_id"].as_str().unwrap(),
567 mem1.metadata.id.to_string()
568 );
569 }
570
571 #[tokio::test]
572 async fn emergence_tool_reports_frequent_tags_without_review() {
573 let (_tmp, store, assoc, tracker) = parts();
574 for i in 0..5 {
575 let mut mem = Memory::new(Galaxy::Codex, format!("rust memory item {i}"));
576 mem.metadata.tags = vec!["rust".into(), "memory".into()];
577 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
578 }
579
580 let result = EmergenceScanTool::new(store, assoc, tracker)
581 .call(&mut Context::default(), json!({"health_score": 0.9}))
582 .await
583 .unwrap();
584 assert_eq!(result["cycle_status"], "Completed");
585 assert_eq!(result["requires_human_review"], false);
586 let rust = result["emergences"]
587 .as_array()
588 .unwrap()
589 .iter()
590 .find(|e| e["tag"] == "rust")
591 .expect("rust emergence expected");
592 assert!(rust["frequency"].as_u64().unwrap() >= 3);
593 }
594
595 #[tokio::test]
596 async fn prune_tool_marks_low_retention_and_skips_protected() {
597 let (_tmp, store, assoc, tracker) = parts();
598 let mut low = Memory::new(Galaxy::Codex, "unimportant old memory".into())
599 .with_importance(0.05)
600 .with_neuro_score(0.05);
601 low.metadata.accessed_at = chrono::Utc::now() - chrono::Duration::days(365);
602 store.put(Galaxy::Codex, &low).unwrap();
603 let mut protected = Memory::new(Galaxy::Codex, "protected old memory".into())
604 .with_importance(0.05)
605 .with_neuro_score(0.05)
606 .with_protection(true);
607 protected.metadata.accessed_at = chrono::Utc::now() - chrono::Duration::days(365);
608 store.put(Galaxy::Codex, &protected).unwrap();
609
610 let result = RetentionPruneTool::new(store, assoc, tracker)
611 .call(&mut Context::default(), json!({"health_score": 0.9}))
612 .await
613 .unwrap();
614 assert_eq!(result["cycle_status"], "Completed");
615 let prunes = result["prunes"].as_array().unwrap();
616 let ids: Vec<&str> = prunes
617 .iter()
618 .map(|p| p["memory_id"].as_str().unwrap())
619 .collect();
620 let low_id = low.metadata.id.to_string();
621 let protected_id = protected.metadata.id.to_string();
622 assert!(ids.contains(&low_id.as_str()), "low-retention id expected");
623 assert!(
624 !ids.contains(&protected_id.as_str()),
625 "protected memory must not be pruned"
626 );
627 }
628
629 #[tokio::test]
630 async fn sensorimotor_tool_fails_closed_without_an_actuation_mask() {
631 use wm_substrate::sensorimotor::{
632 ActuatorKind, ReflexRule, SensorKind, SensorimotorBus, StubActuator, StubSensor,
633 };
634
635 let (_tmp, store, assoc, tracker) = parts();
636 let mut bus = SensorimotorBus::new(100);
637 bus.register_sensor(Box::new(StubSensor::new(
638 "test_temp",
639 SensorKind::Temperature,
640 80.0,
641 )));
642 bus.register_actuator(Box::new(StubActuator::new("fan0", ActuatorKind::Motor)));
643 let bus = Arc::new(std::sync::Mutex::new(bus));
644 let reflex = Arc::new(std::sync::Mutex::new(
645 wm_substrate::sensorimotor::ReflexLoop::new(),
646 ));
647 reflex.lock().unwrap().add_rule(ReflexRule::above(
648 "test_temp",
649 "fan0",
650 ActuatorKind::Motor,
651 50.0,
652 1.0,
653 0.0,
654 ));
655
656 let result = SensorimotorScanTool::new(store, assoc, tracker, bus, reflex)
657 .call(&mut Context::default(), json!({"health_score": 0.9}))
658 .await
659 .unwrap();
660 assert_eq!(result["cycle_status"], "Completed");
661 assert_eq!(result["requires_human_review"], false);
662 assert!(
663 result["notes"]
664 .as_str()
665 .unwrap()
666 .contains("actuation denied"),
667 "fail-closed refusal must be visible: {}",
668 result["notes"]
669 );
670 assert_eq!(result["proposals_generated"], 1);
673 assert_eq!(result["sensorimotor"][0]["reflex_triggered"], false);
674 }
675}