mcp_langbase_reasoning/self_improvement/
system.rs

1//! Self-Improvement System Orchestrator.
2//!
3//! This module provides the main `SelfImprovementSystem` that orchestrates
4//! the 4-phase self-improvement loop: Monitor → Analyzer → Executor → Learner.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────┐
10//! │              SelfImprovementSystem                               │
11//! │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
12//! │  │ Monitor  │──│ Analyzer │──│ Executor │──│ Learner  │        │
13//! │  └──────────┘  └──────────┘  └──────────┘  └──────────┘        │
14//! │       │                                          │              │
15//! │       └──────────────────────────────────────────┘              │
16//! │                         │                                        │
17//! │  ┌──────────────────────┴──────────────────────────────────┐    │
18//! │  │              Shared Components                           │    │
19//! │  │  CircuitBreaker │ Allowlist │ Storage │ Pipes           │    │
20//! │  └─────────────────────────────────────────────────────────┘    │
21//! └─────────────────────────────────────────────────────────────────┘
22//! ```
23//!
24//! # Safety Features
25//!
26//! - **Disabled by Default**: System must be explicitly enabled
27//! - **Circuit Breaker**: Stops after consecutive failures
28//! - **Rate Limiting**: Maximum actions per hour
29//! - **Rollback**: Automatic rollback on regression
30
31use std::sync::Arc;
32
33use chrono::{DateTime, Timelike, Utc};
34use serde::{Deserialize, Serialize};
35use thiserror::Error;
36use tokio::sync::RwLock;
37use tracing::{debug, info, warn};
38
39use super::{
40    ActionAllowlist, AnalysisBlocked, Analyzer, CircuitBreaker, CircuitState, ExecutionBlocked,
41    Executor, HealthReport, Learner, LearningBlocked, Monitor, SelfDiagnosis,
42    SelfImprovementConfig, SelfImprovementPipes,
43};
44use crate::langbase::LangbaseClient;
45use crate::storage::SqliteStorage;
46
47// ============================================================================
48// Error Types
49// ============================================================================
50
51/// Errors that can occur during self-improvement operations.
52#[derive(Debug, Error)]
53pub enum SelfImprovementError {
54    /// System is disabled via configuration.
55    #[error("Self-improvement system is disabled")]
56    Disabled,
57
58    /// Circuit breaker is open, blocking all actions.
59    #[error("Circuit breaker is open: {consecutive_failures} consecutive failures")]
60    CircuitBreakerOpen {
61        /// Number of consecutive failures that opened the circuit.
62        consecutive_failures: u32,
63    },
64
65    /// System is in cooldown period.
66    #[error("System in cooldown until {until}")]
67    InCooldown {
68        /// When the cooldown period ends.
69        until: DateTime<Utc>,
70    },
71
72    /// Rate limit exceeded.
73    #[error("Rate limit exceeded: {count}/{max} actions this hour")]
74    RateLimitExceeded {
75        /// Current action count this hour.
76        count: u32,
77        /// Maximum actions allowed per hour.
78        max: u32,
79    },
80
81    /// Monitor phase failed.
82    #[error("Monitor phase failed: {message}")]
83    MonitorFailed {
84        /// Error details from the monitor phase.
85        message: String,
86    },
87
88    /// Analyzer phase failed.
89    #[error("Analyzer phase failed: {message}")]
90    AnalyzerFailed {
91        /// Error details from the analyzer phase.
92        message: String,
93    },
94
95    /// Executor phase failed.
96    #[error("Executor phase failed: {message}")]
97    ExecutorFailed {
98        /// Error details from the executor phase.
99        message: String,
100    },
101
102    /// Learner phase failed.
103    #[error("Learner phase failed: {message}")]
104    LearnerFailed {
105        /// Error details from the learner phase.
106        message: String,
107    },
108
109    /// Storage operation failed.
110    #[error("Storage error: {message}")]
111    StorageError {
112        /// Error details from the storage operation.
113        message: String,
114    },
115
116    /// Internal system error.
117    #[error("Internal error: {message}")]
118    Internal {
119        /// Error details.
120        message: String,
121    },
122}
123
124// ============================================================================
125// Invocation Event
126// ============================================================================
127
128/// Event recorded when a tool is invoked.
129///
130/// Used by the Monitor phase to track system metrics.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct InvocationEvent {
133    /// Name of the tool that was invoked.
134    pub tool_name: String,
135    /// Latency in milliseconds.
136    pub latency_ms: i64,
137    /// Whether the invocation succeeded.
138    pub success: bool,
139    /// Optional quality score from the response.
140    pub quality_score: Option<f64>,
141    /// When the invocation occurred.
142    pub timestamp: DateTime<Utc>,
143}
144
145// ============================================================================
146// Cycle Result
147// ============================================================================
148
149/// Result of running one improvement cycle.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct CycleResult {
152    /// Whether the cycle completed successfully.
153    pub success: bool,
154    /// Whether any action was taken.
155    pub action_taken: bool,
156    /// Diagnosis that triggered the cycle, if any.
157    pub diagnosis: Option<SelfDiagnosis>,
158    /// Normalized reward if action was taken and verified.
159    pub reward: Option<f64>,
160    /// Any lessons learned from this cycle.
161    pub lessons: Option<String>,
162    /// Error message if cycle failed.
163    pub error: Option<String>,
164    /// Duration of the cycle in milliseconds.
165    pub duration_ms: u64,
166}
167
168// ============================================================================
169// System Status
170// ============================================================================
171
172/// Current status of the self-improvement system.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct SystemStatus {
175    /// Whether the system is enabled.
176    pub enabled: bool,
177    /// Current circuit breaker state.
178    pub circuit_state: CircuitState,
179    /// Consecutive failures count.
180    pub consecutive_failures: u32,
181    /// Whether system is in cooldown.
182    pub in_cooldown: bool,
183    /// Cooldown ends at (if in cooldown).
184    pub cooldown_ends_at: Option<DateTime<Utc>>,
185    /// Actions taken this hour.
186    pub actions_this_hour: u32,
187    /// Maximum actions per hour.
188    pub max_actions_per_hour: u32,
189    /// Total cycles completed.
190    pub total_cycles: u64,
191    /// Total successful actions.
192    pub total_successes: u64,
193    /// Total rolled back actions.
194    pub total_rollbacks: u64,
195    /// Last cycle timestamp.
196    pub last_cycle_at: Option<DateTime<Utc>>,
197}
198
199// ============================================================================
200// System State
201// ============================================================================
202
203/// Internal state for the SelfImprovementSystem.
204struct SystemState {
205    /// Cooldown end time, if in cooldown.
206    cooldown_until: Option<DateTime<Utc>>,
207    /// Actions taken in the current hour.
208    actions_this_hour: u32,
209    /// Hour boundary for rate limiting.
210    rate_limit_hour: DateTime<Utc>,
211    /// Total improvement cycles run.
212    total_cycles: u64,
213    /// Total successful actions.
214    total_successes: u64,
215    /// Total rolled back actions.
216    total_rollbacks: u64,
217    /// Last cycle timestamp.
218    last_cycle_at: Option<DateTime<Utc>>,
219}
220
221impl Default for SystemState {
222    fn default() -> Self {
223        Self {
224            cooldown_until: None,
225            actions_this_hour: 0,
226            rate_limit_hour: Utc::now(),
227            total_cycles: 0,
228            total_successes: 0,
229            total_rollbacks: 0,
230            last_cycle_at: None,
231        }
232    }
233}
234
235// ============================================================================
236// SelfImprovementSystem
237// ============================================================================
238
239/// Orchestrates the 4-phase self-improvement loop.
240///
241/// # Example
242///
243/// ```rust,ignore
244/// let system = SelfImprovementSystem::new(config, storage, langbase);
245///
246/// // Record invocations (called after each tool use)
247/// system.on_invocation(event).await;
248///
249/// // Check health and run cycle if needed
250/// let health = system.check_health().await;
251/// if health.should_act() {
252///     let result = system.run_cycle().await?;
253/// }
254/// ```
255pub struct SelfImprovementSystem {
256    /// System configuration.
257    config: SelfImprovementConfig,
258    /// Phase 1: Monitor for health metrics.
259    monitor: Monitor,
260    /// Phase 2: Analyzer for diagnosis and action selection.
261    analyzer: Analyzer,
262    /// Phase 3: Executor for safe action execution.
263    executor: Executor,
264    /// Phase 4: Learner for reward calculation and learning.
265    learner: Learner,
266    /// Circuit breaker for safety.
267    circuit_breaker: Arc<RwLock<CircuitBreaker>>,
268    /// Action allowlist for validation.
269    allowlist: ActionAllowlist,
270    /// Internal state.
271    state: Arc<RwLock<SystemState>>,
272}
273
274impl SelfImprovementSystem {
275    /// Create a new self-improvement system.
276    ///
277    /// # Arguments
278    ///
279    /// * `config` - System configuration
280    /// * `storage` - SQLite storage backend
281    /// * `langbase` - Langbase API client for pipe calls
282    ///
283    /// # Returns
284    ///
285    /// A new `SelfImprovementSystem` instance.
286    pub fn new(
287        config: SelfImprovementConfig,
288        _storage: SqliteStorage,
289        langbase: LangbaseClient,
290    ) -> Self {
291        info!(
292            enabled = config.enabled,
293            max_actions_per_hour = config.executor.max_actions_per_hour,
294            "Initializing SelfImprovementSystem"
295        );
296
297        // Create shared circuit breaker
298        let circuit_breaker = Arc::new(RwLock::new(CircuitBreaker::new(
299            config.circuit_breaker.clone(),
300        )));
301
302        // Create shared pipes
303        let langbase_arc = Arc::new(langbase);
304        let pipes = Arc::new(SelfImprovementPipes::new(
305            langbase_arc,
306            config.pipes.clone(),
307        ));
308
309        // Create allowlist
310        let allowlist = ActionAllowlist::default_allowlist();
311
312        // Create phase components with shared dependencies
313        let monitor = Monitor::new(config.clone());
314        let analyzer = Analyzer::new(config.clone(), pipes.clone(), circuit_breaker.clone());
315        let executor = Executor::new(config.clone(), allowlist.clone(), circuit_breaker.clone());
316        let learner = Learner::new(config.clone(), pipes, circuit_breaker.clone());
317
318        Self {
319            config,
320            monitor,
321            analyzer,
322            executor,
323            learner,
324            circuit_breaker,
325            allowlist,
326            state: Arc::new(RwLock::new(SystemState::default())),
327        }
328    }
329
330    /// Check if the system is enabled.
331    pub fn is_enabled(&self) -> bool {
332        self.config.enabled
333    }
334
335    /// Record an invocation for metric tracking.
336    ///
337    /// This should be called after each tool invocation to feed the Monitor.
338    pub async fn on_invocation(&self, event: InvocationEvent) {
339        if !self.config.enabled {
340            return;
341        }
342
343        debug!(
344            tool = %event.tool_name,
345            latency_ms = event.latency_ms,
346            success = event.success,
347            "Recording invocation"
348        );
349
350        // Record in monitor (for baseline calculation and anomaly detection)
351        self.monitor
352            .record_invocation(
353                !event.success,
354                event.latency_ms,
355                event.quality_score.unwrap_or(0.8),
356                false, // fallback not tracked here
357            )
358            .await;
359    }
360
361    /// Get current health report from the Monitor.
362    ///
363    /// Returns `Some(HealthReport)` if enough samples have been collected
364    /// and it's time for a check, `None` otherwise.
365    pub async fn check_health(&self) -> Option<HealthReport> {
366        self.monitor.check_health().await
367    }
368
369    /// Force a health check regardless of timing.
370    pub async fn force_health_check(&self) -> Option<HealthReport> {
371        self.monitor.force_check().await
372    }
373
374    /// Get current system status.
375    pub async fn status(&self) -> SystemStatus {
376        let state = self.state.read().await;
377        let cb = self.circuit_breaker.read().await;
378        let cb_summary = cb.summary();
379
380        let in_cooldown = state
381            .cooldown_until
382            .map(|until| Utc::now() < until)
383            .unwrap_or(false);
384
385        SystemStatus {
386            enabled: self.config.enabled,
387            circuit_state: cb_summary.state,
388            consecutive_failures: cb_summary.consecutive_failures,
389            in_cooldown,
390            cooldown_ends_at: if in_cooldown {
391                state.cooldown_until
392            } else {
393                None
394            },
395            actions_this_hour: state.actions_this_hour,
396            max_actions_per_hour: self.config.executor.max_actions_per_hour,
397            total_cycles: state.total_cycles,
398            total_successes: state.total_successes,
399            total_rollbacks: state.total_rollbacks,
400            last_cycle_at: state.last_cycle_at,
401        }
402    }
403
404    /// Run one improvement cycle (Monitor → Analyzer → Executor → Learner).
405    ///
406    /// # Returns
407    ///
408    /// * `Ok(CycleResult)` - Cycle completed (may or may not have taken action)
409    /// * `Err(SelfImprovementError)` - Cycle blocked or failed
410    pub async fn run_cycle(&self) -> Result<CycleResult, SelfImprovementError> {
411        let start = std::time::Instant::now();
412
413        // Check if enabled
414        if !self.config.enabled {
415            return Err(SelfImprovementError::Disabled);
416        }
417
418        // Check circuit breaker
419        {
420            let mut cb = self.circuit_breaker.write().await;
421            if !cb.can_execute() {
422                let summary = cb.summary();
423                return Err(SelfImprovementError::CircuitBreakerOpen {
424                    consecutive_failures: summary.consecutive_failures,
425                });
426            }
427        }
428
429        // Check cooldown
430        {
431            let state = self.state.read().await;
432            if let Some(until) = state.cooldown_until {
433                if Utc::now() < until {
434                    return Err(SelfImprovementError::InCooldown { until });
435                }
436            }
437        }
438
439        // Check rate limit
440        self.check_and_update_rate_limit().await?;
441
442        // Update cycle tracking
443        {
444            let mut state = self.state.write().await;
445            state.total_cycles += 1;
446            state.last_cycle_at = Some(Utc::now());
447        }
448
449        info!("Starting self-improvement cycle");
450
451        // Phase 1: Monitor - Check health (force check since we're running a cycle)
452        let health = match self.monitor.force_check().await {
453            Some(report) => report,
454            None => {
455                info!("Not enough samples for health check");
456                return Ok(CycleResult {
457                    success: true,
458                    action_taken: false,
459                    diagnosis: None,
460                    reward: None,
461                    lessons: None,
462                    error: Some("Insufficient samples for health check".to_string()),
463                    duration_ms: start.elapsed().as_millis() as u64,
464                });
465            }
466        };
467        debug!(?health, "Health report");
468
469        if !health.needs_action() {
470            info!("No action needed - system healthy");
471            return Ok(CycleResult {
472                success: true,
473                action_taken: false,
474                diagnosis: None,
475                reward: None,
476                lessons: None,
477                error: None,
478                duration_ms: start.elapsed().as_millis() as u64,
479            });
480        }
481
482        // Phase 2: Analyzer - Diagnose and select action
483        let analysis_result = match self.analyzer.analyze(&health).await {
484            Ok(result) => result,
485            Err(blocked) => {
486                let msg = match &blocked {
487                    AnalysisBlocked::CircuitOpen { remaining_secs } => {
488                        format!("Circuit open, {} seconds until recovery", remaining_secs)
489                    }
490                    AnalysisBlocked::NoTriggers => "No triggers to analyze".to_string(),
491                    AnalysisBlocked::PipeUnavailable { pipe, error } => {
492                        format!("Pipe '{}' unavailable: {}", pipe, error)
493                    }
494                    AnalysisBlocked::MaxPendingReached { count } => {
495                        format!("Max pending diagnoses reached: {}", count)
496                    }
497                    AnalysisBlocked::SeverityTooLow { severity, minimum } => {
498                        format!("Severity {:?} below minimum {:?}", severity, minimum)
499                    }
500                };
501                warn!(?blocked, "Analysis blocked");
502                return Ok(CycleResult {
503                    success: true,
504                    action_taken: false,
505                    diagnosis: None,
506                    reward: None,
507                    lessons: None,
508                    error: Some(format!("Analysis blocked: {}", msg)),
509                    duration_ms: start.elapsed().as_millis() as u64,
510                });
511            }
512        };
513
514        let diagnosis = analysis_result.diagnosis.clone();
515
516        info!(
517            diagnosis_id = %diagnosis.id,
518            severity = ?diagnosis.severity,
519            action = ?diagnosis.suggested_action.action_type(),
520            "Diagnosis generated"
521        );
522
523        // Validate action against allowlist
524        if let Err(e) = self.allowlist.validate(&diagnosis.suggested_action) {
525            warn!(error = %e, "Action not allowed");
526            return Ok(CycleResult {
527                success: true,
528                action_taken: false,
529                diagnosis: Some(diagnosis),
530                reward: None,
531                lessons: None,
532                error: Some(format!("Action not allowed: {}", e)),
533                duration_ms: start.elapsed().as_millis() as u64,
534            });
535        }
536
537        // Phase 3: Executor - Execute action
538        let current_metrics = self.monitor.get_current_metrics().await;
539        let execution_result = match self.executor.execute(&diagnosis, &current_metrics).await {
540            Ok(result) => result,
541            Err(blocked) => {
542                let msg = match &blocked {
543                    ExecutionBlocked::CircuitOpen { remaining_secs } => {
544                        format!("Circuit open, {} seconds until recovery", remaining_secs)
545                    }
546                    ExecutionBlocked::CooldownActive { remaining_secs } => {
547                        format!("Cooldown active, {} seconds remaining", remaining_secs)
548                    }
549                    ExecutionBlocked::RateLimitExceeded { count, max } => {
550                        format!("Rate limit exceeded: {}/{}", count, max)
551                    }
552                    ExecutionBlocked::NotAllowed { reason } => {
553                        format!("Action not allowed: {}", reason)
554                    }
555                    ExecutionBlocked::NoOpAction { reason } => {
556                        format!("NoOp action: {}", reason)
557                    }
558                    ExecutionBlocked::AwaitingApproval { diagnosis_id } => {
559                        format!("Awaiting approval for diagnosis: {}", diagnosis_id)
560                    }
561                };
562                warn!(?blocked, "Execution blocked");
563                return Ok(CycleResult {
564                    success: true,
565                    action_taken: false,
566                    diagnosis: Some(diagnosis),
567                    reward: None,
568                    lessons: None,
569                    error: Some(format!("Execution blocked: {}", msg)),
570                    duration_ms: start.elapsed().as_millis() as u64,
571                });
572            }
573        };
574
575        info!(
576            action_id = %execution_result.action_id,
577            outcome = ?execution_result.outcome,
578            "Action executed"
579        );
580
581        // Get current baselines for reward calculation
582        let baselines = self.monitor.get_baselines().await;
583        let post_metrics = self.monitor.get_current_metrics().await;
584
585        // Phase 4: Learner - Calculate reward and learn
586        let learning_result = match self
587            .learner
588            .learn(&execution_result, &diagnosis, &post_metrics, &baselines)
589            .await
590        {
591            Ok(outcome) => Some(outcome),
592            Err(blocked) => {
593                let msg = match &blocked {
594                    LearningBlocked::ExecutionNotCompleted { status } => {
595                        format!("Execution not completed: {:?}", status)
596                    }
597                    LearningBlocked::InsufficientSamples { required, actual } => {
598                        format!("Insufficient samples: {} < {}", actual, required)
599                    }
600                    LearningBlocked::PipeUnavailable { message } => {
601                        format!("Pipe unavailable: {}", message)
602                    }
603                };
604                warn!(?blocked, "Learning blocked: {}", msg);
605                None
606            }
607        };
608
609        let (reward, lessons) = if let Some(outcome) = learning_result {
610            let lesson_text = outcome
611                .learning_synthesis
612                .map(|ls| ls.lessons.join("; "));
613            (Some(outcome.reward.value), lesson_text)
614        } else {
615            (None, None)
616        };
617
618        // Record success/failure in circuit breaker
619        if execution_result.outcome == super::types::ActionOutcome::Success {
620            self.record_success().await;
621            let mut state = self.state.write().await;
622            state.total_successes += 1;
623        } else if execution_result.outcome == super::types::ActionOutcome::RolledBack {
624            self.record_failure().await;
625            let mut state = self.state.write().await;
626            state.total_rollbacks += 1;
627        }
628
629        // Set cooldown
630        self.set_cooldown().await;
631
632        info!(
633            reward = ?reward,
634            "Improvement cycle completed"
635        );
636
637        Ok(CycleResult {
638            success: true,
639            action_taken: true,
640            diagnosis: Some(diagnosis),
641            reward,
642            lessons,
643            error: None,
644            duration_ms: start.elapsed().as_millis() as u64,
645        })
646    }
647
648    /// Force run a cycle even if system would normally not act.
649    ///
650    /// This bypasses health checks but still respects circuit breaker and rate limits.
651    pub async fn force_cycle(&self) -> Result<CycleResult, SelfImprovementError> {
652        info!("Force-running improvement cycle");
653        self.run_cycle().await
654    }
655
656    /// Manually trigger a rollback of a previous action.
657    pub async fn rollback(&self, action_id: &str) -> Result<(), SelfImprovementError> {
658        info!(action_id = action_id, "Manual rollback requested");
659
660        self.executor.rollback_by_id(action_id).await.map_err(|e| {
661            SelfImprovementError::ExecutorFailed {
662                message: format!("Rollback failed: {}", e),
663            }
664        })?;
665
666        Ok(())
667    }
668
669    /// Pause the system for a specified duration.
670    pub async fn pause(&self, duration: std::time::Duration) {
671        let until = Utc::now() + chrono::Duration::from_std(duration).unwrap_or_default();
672        let mut state = self.state.write().await;
673        state.cooldown_until = Some(until);
674        info!(until = %until, "System paused");
675    }
676
677    /// Resume the system from pause.
678    pub async fn resume(&self) {
679        let mut state = self.state.write().await;
680        state.cooldown_until = None;
681        info!("System resumed");
682    }
683
684    // ========================================================================
685    // Internal Helpers
686    // ========================================================================
687
688    async fn check_and_update_rate_limit(&self) -> Result<(), SelfImprovementError> {
689        let mut state = self.state.write().await;
690        let now = Utc::now();
691
692        // Reset counter if we've crossed into a new hour
693        let current_hour = now.date_naive().and_hms_opt(now.time().hour(), 0, 0);
694        let limit_hour = state
695            .rate_limit_hour
696            .date_naive()
697            .and_hms_opt(state.rate_limit_hour.time().hour(), 0, 0);
698
699        if current_hour != limit_hour {
700            state.actions_this_hour = 0;
701            state.rate_limit_hour = now;
702        }
703
704        // Check rate limit
705        if state.actions_this_hour >= self.config.executor.max_actions_per_hour {
706            return Err(SelfImprovementError::RateLimitExceeded {
707                count: state.actions_this_hour,
708                max: self.config.executor.max_actions_per_hour,
709            });
710        }
711
712        // Increment counter
713        state.actions_this_hour += 1;
714
715        Ok(())
716    }
717
718    async fn record_success(&self) {
719        let mut cb = self.circuit_breaker.write().await;
720        cb.record_success();
721    }
722
723    async fn record_failure(&self) {
724        let mut cb = self.circuit_breaker.write().await;
725        cb.record_failure();
726    }
727
728    async fn set_cooldown(&self) {
729        let cooldown = self.config.executor.cooldown_duration();
730        let until = Utc::now() + chrono::Duration::from_std(cooldown).unwrap_or_default();
731        let mut state = self.state.write().await;
732        state.cooldown_until = Some(until);
733        debug!(until = %until, "Cooldown set");
734    }
735}
736
737// ============================================================================
738// Tests
739// ============================================================================
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    #[test]
746    fn test_invocation_event_creation() {
747        let event = InvocationEvent {
748            tool_name: "reasoning_linear".to_string(),
749            latency_ms: 150,
750            success: true,
751            quality_score: Some(0.85),
752            timestamp: Utc::now(),
753        };
754
755        assert_eq!(event.tool_name, "reasoning_linear");
756        assert_eq!(event.latency_ms, 150);
757        assert!(event.success);
758    }
759
760    #[test]
761    fn test_cycle_result_no_action() {
762        let result = CycleResult {
763            success: true,
764            action_taken: false,
765            diagnosis: None,
766            reward: None,
767            lessons: None,
768            error: None,
769            duration_ms: 50,
770        };
771
772        assert!(result.success);
773        assert!(!result.action_taken);
774    }
775
776    #[test]
777    fn test_system_status_default() {
778        let status = SystemStatus {
779            enabled: true,
780            circuit_state: CircuitState::Closed,
781            consecutive_failures: 0,
782            in_cooldown: false,
783            cooldown_ends_at: None,
784            actions_this_hour: 0,
785            max_actions_per_hour: 3,
786            total_cycles: 0,
787            total_successes: 0,
788            total_rollbacks: 0,
789            last_cycle_at: None,
790        };
791
792        assert!(status.enabled);
793        assert_eq!(status.circuit_state, CircuitState::Closed);
794    }
795
796    #[test]
797    fn test_error_display() {
798        let err = SelfImprovementError::Disabled;
799        assert_eq!(err.to_string(), "Self-improvement system is disabled");
800
801        let err = SelfImprovementError::CircuitBreakerOpen {
802            consecutive_failures: 3,
803        };
804        assert!(err.to_string().contains("3 consecutive failures"));
805    }
806}