llm_optimizer_processor/decision/
coordinator.rs

1//! Decision Engine Coordinator
2//!
3//! Orchestrates multiple optimization strategies and coordinates decision-making.
4
5use async_trait::async_trait;
6use chrono::Utc;
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Instant;
10use tokio::sync::RwLock;
11use tracing::{debug, info, warn};
12use uuid::Uuid;
13
14use super::config::DecisionEngineConfig;
15use super::error::{DecisionError, DecisionResult, DecisionState};
16use super::traits::{DecisionEngine, OptimizationStrategy};
17use super::types::{Decision, DecisionCriteria, DecisionInput, DecisionOutcome, DecisionStats};
18
19/// Decision Engine Coordinator
20///
21/// Manages multiple optimization strategies and coordinates decision-making
22pub struct DecisionCoordinator {
23    config: DecisionEngineConfig,
24    state: DecisionState,
25    strategies: Vec<Arc<dyn OptimizationStrategy>>,
26    stats: Arc<RwLock<DecisionStats>>,
27    active_decisions: Arc<RwLock<HashMap<String, Decision>>>,
28    decision_history: Arc<RwLock<Vec<Decision>>>,
29    outcome_history: Arc<RwLock<Vec<DecisionOutcome>>>,
30    start_time: Instant,
31}
32
33impl DecisionCoordinator {
34    /// Create a new Decision Coordinator
35    pub fn new(
36        config: DecisionEngineConfig,
37        strategies: Vec<Arc<dyn OptimizationStrategy>>,
38    ) -> Self {
39        Self {
40            config,
41            state: DecisionState::Initialized,
42            strategies,
43            stats: Arc::new(RwLock::new(DecisionStats::default())),
44            active_decisions: Arc::new(RwLock::new(HashMap::new())),
45            decision_history: Arc::new(RwLock::new(Vec::new())),
46            outcome_history: Arc::new(RwLock::new(Vec::new())),
47            start_time: Instant::now(),
48        }
49    }
50
51    /// Validate a decision against criteria
52    async fn validate_decision(
53        &self,
54        decision: &Decision,
55        criteria: &DecisionCriteria,
56    ) -> DecisionResult<()> {
57        // Check confidence threshold
58        if decision.confidence < criteria.min_confidence {
59            return Err(DecisionError::ValidationFailed(format!(
60                "Confidence {:.2} below minimum {:.2}",
61                decision.confidence, criteria.min_confidence
62            )));
63        }
64
65        // Check cost constraints
66        if let Some(max_cost) = criteria.max_cost_increase_usd {
67            if let Some(cost_change) = decision.expected_impact.cost_change_usd {
68                if cost_change > max_cost {
69                    return Err(DecisionError::ConstraintViolation(format!(
70                        "Cost increase ${:.2} exceeds maximum ${:.2}",
71                        cost_change, max_cost
72                    )));
73                }
74            }
75        }
76
77        // Check latency constraints
78        if let Some(max_latency) = criteria.max_latency_increase_ms {
79            if let Some(latency_change) = decision.expected_impact.latency_change_ms {
80                if latency_change > max_latency {
81                    return Err(DecisionError::ConstraintViolation(format!(
82                        "Latency increase {:.2}ms exceeds maximum {:.2}ms",
83                        latency_change, max_latency
84                    )));
85                }
86            }
87        }
88
89        // Check quality constraints
90        if let Some(min_quality) = criteria.min_quality_score {
91            if let Some(quality_change) = decision.expected_impact.quality_change {
92                // If quality is expected to drop below minimum, reject
93                if quality_change < 0.0 {
94                    let estimated_quality = min_quality + quality_change;
95                    if estimated_quality < min_quality {
96                        return Err(DecisionError::ConstraintViolation(format!(
97                            "Quality drop would result in score {:.2}, below minimum {:.2}",
98                            estimated_quality, min_quality
99                        )));
100                    }
101                }
102            }
103        }
104
105        // Verify all required safety checks passed
106        for required_check in &criteria.required_safety_checks {
107            let check_passed = decision
108                .safety_checks
109                .iter()
110                .any(|check| check.name == *required_check && check.passed);
111
112            if !check_passed {
113                return Err(DecisionError::ValidationFailed(format!(
114                    "Required safety check '{}' did not pass",
115                    required_check
116                )));
117            }
118        }
119
120        Ok(())
121    }
122
123    /// Merge and prioritize decisions from multiple strategies
124    async fn merge_decisions(
125        &self,
126        all_decisions: Vec<Vec<Decision>>,
127        criteria: &DecisionCriteria,
128    ) -> Vec<Decision> {
129        let mut merged: Vec<Decision> = all_decisions.into_iter().flatten().collect();
130
131        // Sort by priority (descending)
132        merged.sort_by(|a, b| b.priority.cmp(&a.priority));
133
134        // Apply max concurrent decisions limit
135        merged.truncate(criteria.max_concurrent_decisions);
136
137        // Check for conflicts
138        let mut final_decisions = Vec::new();
139        for decision in merged {
140            // Check if this decision conflicts with already selected decisions
141            let conflicts = final_decisions.iter().any(|d: &Decision| {
142                self.decisions_conflict(&decision, d)
143            });
144
145            if !conflicts {
146                final_decisions.push(decision);
147            } else {
148                debug!(
149                    "Skipping decision {} due to conflict",
150                    decision.id
151                );
152            }
153        }
154
155        final_decisions
156    }
157
158    /// Check if two decisions conflict
159    fn decisions_conflict(&self, d1: &Decision, d2: &Decision) -> bool {
160        // Same decision type conflicts
161        if d1.decision_type == d2.decision_type {
162            return true;
163        }
164
165        // Model switch conflicts with prompt optimization (need stable model first)
166        use super::types::DecisionType::*;
167        matches!(
168            (&d1.decision_type, &d2.decision_type),
169            (ModelSwitch, OptimizePrompt) | (OptimizePrompt, ModelSwitch)
170        )
171    }
172
173    /// Update statistics
174    async fn update_stats(&self, decisions: &[Decision]) {
175        let mut stats = self.stats.write().await;
176
177        stats.total_decisions += decisions.len() as u64;
178
179        for decision in decisions {
180            let decision_type = format!("{:?}", decision.decision_type);
181            *stats.decisions_by_type.entry(decision_type).or_insert(0) += 1;
182
183            *stats
184                .decisions_by_strategy
185                .entry(decision.strategy.clone())
186                .or_insert(0) += 1;
187
188            // Update average confidence
189            let total = stats.total_decisions as f64;
190            stats.avg_confidence = ((stats.avg_confidence * (total - 1.0)) + decision.confidence) / total;
191        }
192
193        stats.last_decision_time = Some(Utc::now());
194    }
195}
196
197#[async_trait]
198impl DecisionEngine for DecisionCoordinator {
199    fn name(&self) -> &str {
200        &self.config.id
201    }
202
203    fn state(&self) -> DecisionState {
204        self.state
205    }
206
207    async fn start(&mut self) -> DecisionResult<()> {
208        if !self.state.can_transition_to(DecisionState::Starting) {
209            return Err(DecisionError::InvalidStateTransition {
210                from: self.state,
211                to: DecisionState::Starting,
212            });
213        }
214
215        info!("Starting Decision Engine Coordinator");
216        self.state = DecisionState::Starting;
217
218        // Initialize strategies
219        info!("Initialized {} strategies", self.strategies.len());
220
221        self.state = DecisionState::Running;
222        info!("Decision Engine Coordinator started successfully");
223
224        Ok(())
225    }
226
227    async fn stop(&mut self) -> DecisionResult<()> {
228        if !self.state.can_transition_to(DecisionState::Draining) {
229            return Err(DecisionError::InvalidStateTransition {
230                from: self.state,
231                to: DecisionState::Draining,
232            });
233        }
234
235        info!("Stopping Decision Engine Coordinator");
236        self.state = DecisionState::Draining;
237
238        // Wait for active decisions to complete
239        let active_count = self.active_decisions.read().await.len();
240        if active_count > 0 {
241            info!("Waiting for {} active decisions to complete", active_count);
242        }
243
244        self.state = DecisionState::Stopped;
245        info!("Decision Engine Coordinator stopped");
246
247        Ok(())
248    }
249
250    async fn make_decisions(
251        &mut self,
252        input: DecisionInput,
253        criteria: &DecisionCriteria,
254    ) -> DecisionResult<Vec<Decision>> {
255        if !self.state.can_process() {
256            return Err(DecisionError::NotRunning);
257        }
258
259        let start = Instant::now();
260
261        debug!(
262            "Making decisions with {} insights and {} recommendations",
263            input.insights.len(),
264            input.recommendations.len()
265        );
266
267        // Run each applicable strategy
268        let mut all_decisions = Vec::new();
269
270        for strategy in &self.strategies {
271            // Check if strategy is enabled
272            if !criteria.enabled_strategies.contains(&strategy.name().to_string()) {
273                continue;
274            }
275
276            // Check if strategy is applicable
277            if !strategy.is_applicable(&input) {
278                debug!("Strategy {} not applicable", strategy.name());
279                continue;
280            }
281
282            // Evaluate strategy
283            match strategy.evaluate(&input, criteria).await {
284                Ok(decisions) => {
285                    debug!(
286                        "Strategy {} generated {} decisions",
287                        strategy.name(),
288                        decisions.len()
289                    );
290                    all_decisions.push(decisions);
291                }
292                Err(e) => {
293                    warn!("Strategy {} evaluation failed: {}", strategy.name(), e);
294                }
295            }
296        }
297
298        // Merge and prioritize decisions
299        let mut merged_decisions = self.merge_decisions(all_decisions, criteria).await;
300
301        // Validate each decision
302        let mut validated_decisions = Vec::new();
303        for decision in merged_decisions {
304            match self.validate_decision(&decision, criteria).await {
305                Ok(()) => {
306                    validated_decisions.push(decision);
307                }
308                Err(e) => {
309                    warn!("Decision {} validation failed: {}", decision.id, e);
310                }
311            }
312        }
313
314        // Update statistics
315        self.update_stats(&validated_decisions).await;
316
317        // Store decisions
318        let mut history = self.decision_history.write().await;
319        history.extend(validated_decisions.clone());
320
321        // Keep only recent history
322        if history.len() > self.config.monitoring.decision_history_size {
323            let len = history.len();
324            history.drain(0..len - self.config.monitoring.decision_history_size);
325        }
326
327        // Add to active decisions
328        let mut active = self.active_decisions.write().await;
329        for decision in &validated_decisions {
330            active.insert(decision.id.clone(), decision.clone());
331        }
332
333        let elapsed = start.elapsed();
334        info!(
335            "Generated {} validated decisions in {:?}",
336            validated_decisions.len(),
337            elapsed
338        );
339
340        Ok(validated_decisions)
341    }
342
343    async fn record_outcome(&mut self, outcome: DecisionOutcome) -> DecisionResult<()> {
344        debug!("Recording outcome for decision {}", outcome.decision_id);
345
346        // Remove from active decisions
347        let mut active = self.active_decisions.write().await;
348        let decision = active.remove(&outcome.decision_id);
349
350        // Update statistics
351        let mut stats = self.stats.write().await;
352
353        if outcome.success {
354            stats.successful_decisions += 1;
355
356            // Update cost savings if available
357            if let Some(actual_impact) = &outcome.actual_impact {
358                if actual_impact.cost_change_usd < 0.0 {
359                    stats.total_cost_savings_usd += actual_impact.cost_change_usd.abs();
360                }
361                if actual_impact.latency_change_ms < 0.0 {
362                    stats.total_latency_improvement_ms += actual_impact.latency_change_ms.abs();
363                }
364            }
365        } else {
366            stats.failed_decisions += 1;
367        }
368
369        if outcome.rolled_back {
370            stats.rolled_back_decisions += 1;
371        }
372
373        drop(stats);
374
375        // Note: Strategy learning is disabled in this coordinator because strategies
376        // are wrapped in Arc<dyn> which doesn't allow mutable access.
377        // To enable learning, strategies would need to use interior mutability (e.g., Mutex).
378        if decision.is_some() {
379            debug!("Strategy learning not implemented for Arc-wrapped strategies");
380        }
381
382        // Store outcome in history
383        let mut history = self.outcome_history.write().await;
384        history.push(outcome);
385
386        // Keep only recent history
387        if history.len() > self.config.monitoring.outcome_history_size {
388            let len = history.len();
389            history.drain(0..len - self.config.monitoring.outcome_history_size);
390        }
391
392        Ok(())
393    }
394
395    fn get_stats(&self) -> DecisionStats {
396        // Synchronous method, can't await. Return default or use try_read
397        // In production, we'd use try_read() and handle the lock failure
398        DecisionStats::default()
399    }
400
401    async fn health_check(&self) -> DecisionResult<()> {
402        if !self.state.can_process() {
403            return Err(DecisionError::NotRunning);
404        }
405
406        // Check if strategies are responsive
407        let strategy_count = self.strategies.len();
408        if strategy_count == 0 {
409            return Err(DecisionError::Internal(
410                "No strategies configured".to_string(),
411            ));
412        }
413
414        Ok(())
415    }
416
417    async fn reset(&mut self) -> DecisionResult<()> {
418        info!("Resetting Decision Engine Coordinator");
419
420        // Clear active decisions
421        self.active_decisions.write().await.clear();
422
423        // Clear histories
424        self.decision_history.write().await.clear();
425        self.outcome_history.write().await.clear();
426
427        // Reset statistics
428        *self.stats.write().await = DecisionStats::default();
429
430        Ok(())
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use crate::analyzer::{AnalysisReport, Insight, InsightCategory, Severity, Confidence};
438    use crate::decision::types::*;
439
440    #[tokio::test]
441    async fn test_coordinator_lifecycle() {
442        let config = DecisionEngineConfig::default();
443        let mut coordinator = DecisionCoordinator::new(config, vec![]);
444
445        assert_eq!(coordinator.state(), DecisionState::Initialized);
446
447        coordinator.start().await.unwrap();
448        assert_eq!(coordinator.state(), DecisionState::Running);
449
450        coordinator.stop().await.unwrap();
451        assert_eq!(coordinator.state(), DecisionState::Stopped);
452    }
453
454    #[tokio::test]
455    async fn test_decision_validation() {
456        let config = DecisionEngineConfig::default();
457        let coordinator = DecisionCoordinator::new(config, vec![]);
458
459        let decision = Decision {
460            id: Uuid::new_v4().to_string(),
461            timestamp: Utc::now(),
462            strategy: "test".to_string(),
463            decision_type: DecisionType::NoAction,
464            confidence: 0.9,
465            expected_impact: ExpectedImpact {
466                cost_change_usd: Some(10.0),
467                latency_change_ms: Some(50.0),
468                quality_change: None,
469                throughput_change_rps: None,
470                success_rate_change_pct: None,
471                time_to_impact: std::time::Duration::from_secs(60),
472                confidence: 0.9,
473            },
474            config_changes: vec![],
475            justification: "Test decision".to_string(),
476            related_insights: vec![],
477            related_recommendations: vec![],
478            priority: 50,
479            requires_approval: false,
480            safety_checks: vec![
481                SafetyCheck {
482                    name: "cost_check".to_string(),
483                    passed: true,
484                    details: "OK".to_string(),
485                    timestamp: Utc::now(),
486                },
487                SafetyCheck {
488                    name: "latency_check".to_string(),
489                    passed: true,
490                    details: "OK".to_string(),
491                    timestamp: Utc::now(),
492                },
493                SafetyCheck {
494                    name: "quality_check".to_string(),
495                    passed: true,
496                    details: "OK".to_string(),
497                    timestamp: Utc::now(),
498                },
499            ],
500            rollback_plan: None,
501            metadata: HashMap::new(),
502        };
503
504        let criteria = DecisionCriteria::default();
505
506        // Should pass validation
507        assert!(coordinator.validate_decision(&decision, &criteria).await.is_ok());
508    }
509
510    #[tokio::test]
511    async fn test_validation_failures() {
512        let config = DecisionEngineConfig::default();
513        let coordinator = DecisionCoordinator::new(config, vec![]);
514
515        let mut decision = Decision {
516            id: Uuid::new_v4().to_string(),
517            timestamp: Utc::now(),
518            strategy: "test".to_string(),
519            decision_type: DecisionType::NoAction,
520            confidence: 0.5, // Below default minimum of 0.7
521            expected_impact: ExpectedImpact {
522                cost_change_usd: None,
523                latency_change_ms: None,
524                quality_change: None,
525                throughput_change_rps: None,
526                success_rate_change_pct: None,
527                time_to_impact: std::time::Duration::from_secs(60),
528                confidence: 0.5,
529            },
530            config_changes: vec![],
531            justification: "Test".to_string(),
532            related_insights: vec![],
533            related_recommendations: vec![],
534            priority: 50,
535            requires_approval: false,
536            safety_checks: vec![],
537            rollback_plan: None,
538            metadata: HashMap::new(),
539        };
540
541        let criteria = DecisionCriteria::default();
542
543        // Should fail due to low confidence
544        assert!(coordinator.validate_decision(&decision, &criteria).await.is_err());
545    }
546
547    #[tokio::test]
548    async fn test_health_check() {
549        let config = DecisionEngineConfig::default();
550        let mut coordinator = DecisionCoordinator::new(config, vec![]);
551
552        // Should fail when not running
553        assert!(coordinator.health_check().await.is_err());
554
555        // Should succeed when running
556        coordinator.start().await.unwrap();
557        assert!(coordinator.health_check().await.is_ok());
558    }
559}