1#![forbid(unsafe_code)]
10#![allow(clippy::significant_drop_tightening)]
11
12use async_trait::async_trait;
13
14use std::sync::{Arc, Mutex};
15
16use serde_json::{Value, json};
17use wm_core::{Context, EffectRow, Gana, Tool, ToolStats};
18use wm_selfmodel::{AlertLevel, MetricKind, SelfModel};
19
20fn parse_metric_kind(s: &str) -> Result<MetricKind, wm_core::CoreError> {
23 match s.to_lowercase().as_str() {
24 "cpu_load" | "cpu" | "load" => Ok(MetricKind::CpuLoad),
25 "memory_pressure" | "memory" | "mem" => Ok(MetricKind::MemoryPressure),
26 "latency" | "lat" => Ok(MetricKind::Latency),
27 "coherence" | "coh" => Ok(MetricKind::Coherence),
28 "error_rate" | "errors" | "error" => Ok(MetricKind::ErrorRate),
29 "disk_io" | "disk" | "io" => Ok(MetricKind::DiskIo),
30 "swap_usage" | "swap" => Ok(MetricKind::SwapUsage),
31 _ => Err(wm_core::CoreError::InvalidArgs(format!(
32 "unknown metric kind: {s}"
33 ))),
34 }
35}
36
37const fn alert_level_as_str(level: AlertLevel) -> &'static str {
38 match level {
39 AlertLevel::Info => "info",
40 AlertLevel::Warning => "warning",
41 AlertLevel::Critical => "critical",
42 }
43}
44
45fn forecast_to_json(forecast: &wm_selfmodel::Forecast) -> Value {
46 json!({
47 "predicted_value": forecast.predicted_value,
48 "slope": forecast.slope,
49 "ewma": forecast.ewma,
50 "confidence": forecast.confidence,
51 "horizon": forecast.horizon,
52 })
53}
54
55fn metric_snapshot_to_json(snap: &wm_selfmodel::MetricSnapshot) -> Value {
56 json!({
57 "kind": snap.kind.as_str(),
58 "current": snap.current,
59 "min": snap.min,
60 "max": snap.max,
61 "avg": snap.avg,
62 "sample_count": snap.sample_count,
63 })
64}
65
66pub struct SelfModelForecastTool {
70 model: Arc<Mutex<SelfModel>>,
71 stats: ToolStats,
72 effects: EffectRow,
73}
74
75impl SelfModelForecastTool {
76 #[must_use]
77 pub fn new(model: Arc<Mutex<SelfModel>>) -> Self {
78 Self {
79 model,
80 stats: ToolStats::default(),
81 effects: EffectRow::pure(),
82 }
83 }
84}
85
86impl Default for SelfModelForecastTool {
87 fn default() -> Self {
88 Self::new(Arc::new(Mutex::new(SelfModel::new())))
89 }
90}
91
92#[async_trait]
93impl Tool for SelfModelForecastTool {
94 fn name(&self) -> &str {
95 "selfmodel.forecast"
96 }
97 fn gana(&self) -> Gana {
98 Gana::Ghost
99 }
100 fn effects(&self) -> &EffectRow {
101 &self.effects
102 }
103 fn stats(&self) -> &ToolStats {
104 &self.stats
105 }
106 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
107 let model = self
108 .model
109 .lock()
110 .map_err(|e| wm_core::CoreError::Tool(format!("self-model lock: {e}")))?;
111
112 let horizon = args.get("horizon").and_then(Value::as_u64).unwrap_or(5) as usize;
113
114 if let Some(metric_str) = args.get("metric").and_then(|m| m.as_str()) {
115 let kind = parse_metric_kind(metric_str)?;
116 match model.forecast(kind, horizon) {
117 Some(forecast) => Ok(json!({
118 "metric": kind.as_str(),
119 "forecast": forecast_to_json(&forecast),
120 })),
121 None => Ok(json!({
122 "metric": kind.as_str(),
123 "forecast": null,
124 "message": "insufficient data for forecast (need at least 2 samples)",
125 })),
126 }
127 } else {
128 let forecasts = model.forecast_all(horizon);
129 if forecasts.is_empty() {
130 return Ok(json!({
131 "forecasts": [],
132 "message": "no metrics tracked yet",
133 }));
134 }
135 let result: Vec<Value> = forecasts
136 .iter()
137 .map(|(kind, f)| {
138 json!({
139 "metric": kind.as_str(),
140 "forecast": forecast_to_json(f),
141 })
142 })
143 .collect();
144 Ok(json!({
145 "forecasts": result,
146 "count": result.len(),
147 }))
148 }
149 }
150}
151
152pub struct SelfModelAlertsTool {
156 model: Arc<Mutex<SelfModel>>,
157 stats: ToolStats,
158 effects: EffectRow,
159}
160
161impl SelfModelAlertsTool {
162 #[must_use]
163 pub fn new(model: Arc<Mutex<SelfModel>>) -> Self {
164 Self {
165 model,
166 stats: ToolStats::default(),
167 effects: EffectRow::pure(),
168 }
169 }
170}
171
172impl Default for SelfModelAlertsTool {
173 fn default() -> Self {
174 Self::new(Arc::new(Mutex::new(SelfModel::new())))
175 }
176}
177
178#[async_trait]
179impl Tool for SelfModelAlertsTool {
180 fn name(&self) -> &str {
181 "selfmodel.alerts"
182 }
183 fn gana(&self) -> Gana {
184 Gana::Ghost
185 }
186 fn effects(&self) -> &EffectRow {
187 &self.effects
188 }
189 fn stats(&self) -> &ToolStats {
190 &self.stats
191 }
192 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
193 let model = self
194 .model
195 .lock()
196 .map_err(|e| wm_core::CoreError::Tool(format!("self-model lock: {e}")))?;
197
198 let alerts = model.check_alerts();
199 let critical_count = alerts
200 .iter()
201 .filter(|a| a.level == AlertLevel::Critical)
202 .count();
203 let warning_count = alerts
204 .iter()
205 .filter(|a| a.level == AlertLevel::Warning)
206 .count();
207
208 let alerts_json: Vec<Value> = alerts
209 .iter()
210 .map(|a| {
211 json!({
212 "metric": a.metric.as_str(),
213 "level": alert_level_as_str(a.level),
214 "predicted_value": a.predicted_value,
215 "threshold": a.threshold,
216 "message": a.message,
217 "confidence": a.confidence,
218 })
219 })
220 .collect();
221
222 Ok(json!({
223 "alerts": alerts_json,
224 "total": alerts.len(),
225 "critical_count": critical_count,
226 "warning_count": warning_count,
227 }))
228 }
229}
230
231pub struct SelfModelSnapshotTool {
235 model: Arc<Mutex<SelfModel>>,
236 stats: ToolStats,
237 effects: EffectRow,
238}
239
240impl SelfModelSnapshotTool {
241 #[must_use]
242 pub fn new(model: Arc<Mutex<SelfModel>>) -> Self {
243 Self {
244 model,
245 stats: ToolStats::default(),
246 effects: EffectRow::pure(),
247 }
248 }
249}
250
251impl Default for SelfModelSnapshotTool {
252 fn default() -> Self {
253 Self::new(Arc::new(Mutex::new(SelfModel::new())))
254 }
255}
256
257#[async_trait]
258impl Tool for SelfModelSnapshotTool {
259 fn name(&self) -> &str {
260 "selfmodel.snapshot"
261 }
262 fn gana(&self) -> Gana {
263 Gana::Ghost
264 }
265 fn effects(&self) -> &EffectRow {
266 &self.effects
267 }
268 fn stats(&self) -> &ToolStats {
269 &self.stats
270 }
271 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
272 let model = self
273 .model
274 .lock()
275 .map_err(|e| wm_core::CoreError::Tool(format!("self-model lock: {e}")))?;
276
277 let snap = model.snapshot();
278
279 let metrics_json: Vec<Value> = snap.metrics.iter().map(metric_snapshot_to_json).collect();
280
281 let alerts_json: Vec<Value> = snap
282 .alerts
283 .iter()
284 .map(|a| {
285 json!({
286 "metric": a.metric.as_str(),
287 "level": alert_level_as_str(a.level),
288 "predicted_value": a.predicted_value,
289 "threshold": a.threshold,
290 "message": a.message,
291 })
292 })
293 .collect();
294
295 let forecasts_json: Vec<Value> = snap
296 .forecasts
297 .iter()
298 .map(|(kind, f)| {
299 json!({
300 "metric": kind.as_str(),
301 "forecast": forecast_to_json(f),
302 })
303 })
304 .collect();
305
306 Ok(json!({
307 "timestamp": snap.timestamp.to_rfc3339(),
308 "confidence": snap.confidence,
309 "conservative_mode": snap.confidence < 0.5,
310 "metrics": metrics_json,
311 "metric_count": snap.metrics.len(),
312 "alerts": alerts_json,
313 "alert_count": snap.alerts.len(),
314 "forecasts": forecasts_json,
315 "forecast_count": snap.forecasts.len(),
316 }))
317 }
318}
319
320pub struct SelfModelGnosisTool {
328 model: Arc<Mutex<SelfModel>>,
329 stats: ToolStats,
330 effects: EffectRow,
331}
332
333impl SelfModelGnosisTool {
334 #[must_use]
335 pub fn new(model: Arc<Mutex<SelfModel>>) -> Self {
336 Self {
337 model,
338 stats: ToolStats::default(),
339 effects: EffectRow::pure(),
340 }
341 }
342}
343
344impl Default for SelfModelGnosisTool {
345 fn default() -> Self {
346 Self::new(Arc::new(Mutex::new(SelfModel::new())))
347 }
348}
349
350#[async_trait]
351impl Tool for SelfModelGnosisTool {
352 fn name(&self) -> &str {
353 "selfmodel.gnosis"
354 }
355 fn gana(&self) -> Gana {
356 Gana::Ghost
357 }
358 fn effects(&self) -> &EffectRow {
359 &self.effects
360 }
361 fn stats(&self) -> &ToolStats {
362 &self.stats
363 }
364 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
365 let model = self
366 .model
367 .lock()
368 .map_err(|e| wm_core::CoreError::Tool(format!("self-model lock: {e}")))?;
369
370 let snap = model.snapshot();
371 let confidence = model.confidence();
372
373 let metrics_json: Vec<Value> = snap
374 .metrics
375 .iter()
376 .map(|m| {
377 json!({
378 "metric": m.kind.as_str(),
379 "samples": m.sample_count,
380 "current": m.current,
381 "min": m.min,
382 "max": m.max,
383 "avg": m.avg,
384 })
385 })
386 .collect();
387
388 let alerts_json: Vec<Value> = snap
389 .alerts
390 .iter()
391 .map(|a| {
392 json!({
393 "metric": a.metric.as_str(),
394 "level": alert_level_as_str(a.level),
395 "message": a.message,
396 })
397 })
398 .collect();
399
400 let mut healthy = 0usize;
402 let mut degraded = 0usize;
403 for m in &snap.metrics {
404 let has_alert = snap.alerts.iter().any(|a| a.metric == m.kind);
405 if has_alert {
406 degraded += 1;
407 } else {
408 healthy += 1;
409 }
410 }
411
412 let overall = if snap.alerts.is_empty() {
413 "healthy"
414 } else {
415 "degraded"
416 };
417
418 Ok(json!({
419 "timestamp": snap.timestamp.to_rfc3339(),
420 "overall_health": overall,
421 "confidence": confidence,
422 "tracked_metrics": healthy + degraded,
423 "healthy_metrics": healthy,
424 "degraded_metrics": degraded,
425 "alert_count": snap.alerts.len(),
426 "metrics": metrics_json,
427 "alerts": alerts_json,
428 }))
429 }
430}
431
432pub fn register_selfmodel(
436 registry: &wm_dispatch::ToolRegistry,
437 model: Arc<Mutex<SelfModel>>,
438) -> wm_dispatch::ToolRegistry {
439 registry
440 .register(Arc::new(SelfModelForecastTool::new(Arc::clone(&model))))
441 .register(Arc::new(SelfModelAlertsTool::new(Arc::clone(&model))))
442 .register(Arc::new(SelfModelSnapshotTool::new(Arc::clone(&model))))
443 .register(Arc::new(SelfModelGnosisTool::new(model)))
444}
445
446#[cfg(test)]
449mod tests {
450 use super::*;
451
452 fn test_model() -> Arc<Mutex<SelfModel>> {
453 let model = SelfModel::new();
454 for v in [0.1, 0.2, 0.3, 0.4, 0.5] {
455 model.record(MetricKind::CpuLoad, v);
456 }
457 model.record(MetricKind::MemoryPressure, 0.2);
458 model.record(MetricKind::MemoryPressure, 0.3);
459 Arc::new(Mutex::new(model))
460 }
461
462 #[tokio::test]
463 async fn parse_metric_kind_all_variants() {
464 assert_eq!(parse_metric_kind("cpu_load").unwrap(), MetricKind::CpuLoad);
465 assert_eq!(parse_metric_kind("CPU").unwrap(), MetricKind::CpuLoad);
466 assert_eq!(
467 parse_metric_kind("memory_pressure").unwrap(),
468 MetricKind::MemoryPressure
469 );
470 assert_eq!(
471 parse_metric_kind("mem").unwrap(),
472 MetricKind::MemoryPressure
473 );
474 assert_eq!(parse_metric_kind("latency").unwrap(), MetricKind::Latency);
475 assert_eq!(
476 parse_metric_kind("coherence").unwrap(),
477 MetricKind::Coherence
478 );
479 assert_eq!(
480 parse_metric_kind("error_rate").unwrap(),
481 MetricKind::ErrorRate
482 );
483 assert_eq!(parse_metric_kind("disk_io").unwrap(), MetricKind::DiskIo);
484 assert_eq!(parse_metric_kind("swap").unwrap(), MetricKind::SwapUsage);
485 }
486
487 #[tokio::test]
488 async fn parse_metric_kind_invalid() {
489 assert!(parse_metric_kind("nonexistent").is_err());
490 }
491
492 #[tokio::test]
493 async fn forecast_single_metric() {
494 let model = test_model();
495 let tool = SelfModelForecastTool::new(Arc::clone(&model));
496 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
497 let result = tool
498 .call(&mut ctx, json!({"metric": "cpu_load", "horizon": 5}))
499 .await
500 .unwrap();
501 assert_eq!(result["metric"], "cpu_load");
502 assert!(result["forecast"]["predicted_value"].is_number());
503 assert!(result["forecast"]["confidence"].is_number());
504 }
505
506 #[tokio::test]
507 async fn forecast_all_metrics() {
508 let model = test_model();
509 let tool = SelfModelForecastTool::new(Arc::clone(&model));
510 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
511 let result = tool.call(&mut ctx, json!({"horizon": 3})).await.unwrap();
512 let forecasts = result["forecasts"].as_array().unwrap();
513 assert_eq!(forecasts.len(), 2);
514 assert_eq!(result["count"], 2);
515 }
516
517 #[tokio::test]
518 async fn forecast_empty_model() {
519 let model = Arc::new(Mutex::new(SelfModel::new()));
520 let tool = SelfModelForecastTool::new(Arc::clone(&model));
521 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
522 let result = tool.call(&mut ctx, json!({})).await.unwrap();
523 assert_eq!(result["forecasts"].as_array().unwrap().len(), 0);
524 }
525
526 #[tokio::test]
527 async fn forecast_insufficient_data() {
528 let model = Arc::new(Mutex::new(SelfModel::new()));
529 {
530 let m = model.lock().unwrap();
531 m.record(MetricKind::CpuLoad, 0.3);
532 }
533 let tool = SelfModelForecastTool::new(Arc::clone(&model));
534 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
535 let result = tool
536 .call(&mut ctx, json!({"metric": "cpu_load"}))
537 .await
538 .unwrap();
539 assert!(result["forecast"].is_null());
540 assert!(result["message"].as_str().unwrap().contains("insufficient"));
541 }
542
543 #[tokio::test]
544 async fn forecast_invalid_metric() {
545 let model = test_model();
546 let tool = SelfModelForecastTool::new(Arc::clone(&model));
547 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
548 let err = tool
549 .call(&mut ctx, json!({"metric": "nonexistent"}))
550 .await
551 .unwrap_err();
552 assert!(err.to_string().contains("unknown metric"));
553 }
554
555 #[tokio::test]
556 async fn alerts_clear() {
557 let model = Arc::new(Mutex::new(SelfModel::new()));
558 {
559 let m = model.lock().unwrap();
560 for v in [0.1, 0.12, 0.11, 0.13] {
561 m.record(MetricKind::CpuLoad, v);
562 }
563 }
564 let tool = SelfModelAlertsTool::new(Arc::clone(&model));
565 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
566 let result = tool.call(&mut ctx, json!({})).await.unwrap();
567 assert_eq!(result["total"], 0);
568 assert_eq!(result["critical_count"], 0);
569 }
570
571 #[tokio::test]
572 async fn alerts_triggered() {
573 let model = Arc::new(Mutex::new(SelfModel::new()));
574 {
575 let m = model.lock().unwrap();
576 for v in [0.5, 0.6, 0.7, 0.8, 0.9, 0.95] {
577 m.record(MetricKind::CpuLoad, v);
578 }
579 }
580 let tool = SelfModelAlertsTool::new(Arc::clone(&model));
581 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
582 let result = tool.call(&mut ctx, json!({})).await.unwrap();
583 assert!(result["total"].as_u64().unwrap() > 0);
584 let alerts = result["alerts"].as_array().unwrap();
585 assert!(alerts.iter().any(|a| a["metric"] == "cpu_load"));
586 }
587
588 #[tokio::test]
589 async fn snapshot_with_data() {
590 let model = test_model();
591 let tool = SelfModelSnapshotTool::new(Arc::clone(&model));
592 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
593 let result = tool.call(&mut ctx, json!({})).await.unwrap();
594 assert!(result["confidence"].is_number());
595 assert!(result["metric_count"].as_u64().unwrap() > 0);
596 assert!(result["forecast_count"].as_u64().unwrap() > 0);
597 assert!(result["timestamp"].is_string());
598 assert!(result["conservative_mode"].is_boolean());
599 }
600
601 #[tokio::test]
602 async fn snapshot_empty_model() {
603 let model = Arc::new(Mutex::new(SelfModel::new()));
604 let tool = SelfModelSnapshotTool::new(Arc::clone(&model));
605 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
606 let result = tool.call(&mut ctx, json!({})).await.unwrap();
607 assert_eq!(result["metric_count"], 0);
608 assert_eq!(result["alert_count"], 0);
609 assert_eq!(result["forecast_count"], 0);
610 }
611
612 #[tokio::test]
613 async fn register_selfmodel_registers_three_tools() {
614 let model = test_model();
615 let registry = wm_dispatch::ToolRegistry::new();
616 let registry = register_selfmodel(®istry, model);
617 assert!(registry.get("selfmodel.forecast").is_some());
618 assert!(registry.get("selfmodel.alerts").is_some());
619 assert!(registry.get("selfmodel.snapshot").is_some());
620 }
621
622 #[tokio::test]
623 async fn forecast_default_horizon() {
624 let model = test_model();
625 let tool = SelfModelForecastTool::new(Arc::clone(&model));
626 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
627 let result = tool
628 .call(&mut ctx, json!({"metric": "cpu_load"}))
629 .await
630 .unwrap();
631 assert_eq!(result["forecast"]["horizon"], 5);
632 }
633
634 #[tokio::test]
635 async fn snapshot_conservative_mode_flag() {
636 let model = Arc::new(Mutex::new(SelfModel::new()));
637 let tool = SelfModelSnapshotTool::new(Arc::clone(&model));
639 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
640 let result = tool.call(&mut ctx, json!({})).await.unwrap();
641 assert_eq!(result["conservative_mode"], false);
642 }
643
644 #[tokio::test]
645 async fn gnosis_reports_health_and_metrics() {
646 let model = test_model();
647 let tool = SelfModelGnosisTool::new(Arc::clone(&model));
648 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
649 let result = tool.call(&mut ctx, json!({})).await.unwrap();
650
651 assert_eq!(result["overall_health"], "degraded");
654 assert_eq!(result["tracked_metrics"], 2);
655 assert_eq!(result["healthy_metrics"], 1);
656 assert_eq!(result["degraded_metrics"], 1);
657 assert_eq!(result["alert_count"], 2);
658 assert_eq!(result["metrics"].as_array().unwrap().len(), 2);
659 assert!(result["confidence"].as_f64().unwrap() > 0.0);
660 }
661}