swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! Termination reasons and verdicts
//!
//! Defines the possible outcomes of task termination.

use serde::{Deserialize, Serialize};

/// Final termination verdict
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TerminationVerdict {
    /// Task completed successfully
    Success { reason: SuccessReason },
    /// Task failed
    Failure { reason: FailureReason },
    /// Task timed out (max_ticks reached)
    Timeout { partial_success: bool },
    /// External termination request
    ExternalStop { reason: String },
}

impl TerminationVerdict {
    /// Check if this verdict represents success
    pub fn is_success(&self) -> bool {
        matches!(
            self,
            TerminationVerdict::Success { .. }
                | TerminationVerdict::Timeout {
                    partial_success: true
                }
        )
    }

    /// Check if this verdict represents failure
    pub fn is_failure(&self) -> bool {
        !self.is_success()
    }

    /// Get a human-readable description
    pub fn description(&self) -> String {
        match self {
            TerminationVerdict::Success { reason } => format!("Success: {}", reason.description()),
            TerminationVerdict::Failure { reason } => format!("Failure: {}", reason.description()),
            TerminationVerdict::Timeout { partial_success } => {
                if *partial_success {
                    "Timeout (partial success)".to_string()
                } else {
                    "Timeout (failure)".to_string()
                }
            }
            TerminationVerdict::ExternalStop { reason } => format!("External stop: {}", reason),
        }
    }
}

/// Reasons for successful termination
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SuccessReason {
    /// Worker returned Done with success=true
    WorkerDone {
        worker_id: usize,
        message: Option<String>,
    },
    /// All success conditions were met
    ConditionsMet,
    /// Exploration completed naturally (all paths explored)
    ExplorationComplete,
    /// Goal action was executed successfully
    GoalActionCompleted { action: String },
}

impl SuccessReason {
    pub fn description(&self) -> String {
        match self {
            SuccessReason::WorkerDone { worker_id, message } => {
                let msg = message.as_deref().unwrap_or("(no message)");
                format!("Worker {} completed: {}", worker_id, msg)
            }
            SuccessReason::ConditionsMet => "All success conditions met".to_string(),
            SuccessReason::ExplorationComplete => "Exploration completed".to_string(),
            SuccessReason::GoalActionCompleted { action } => {
                format!("Goal action '{}' completed", action)
            }
        }
    }
}

/// Reasons for failed termination
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FailureReason {
    /// Worker returned Done with success=false
    WorkerFailed {
        worker_id: usize,
        message: Option<String>,
    },
    /// A failure condition was triggered
    ConditionFailed { condition_name: String },
    /// Maximum errors exceeded
    MaxErrorsExceeded { count: u64, limit: u64 },
    /// Exploration exhausted without reaching goal
    ExplorationExhausted,
    /// No valid actions available
    NoValidActions,
    /// Internal error
    InternalError { message: String },
}

impl FailureReason {
    pub fn description(&self) -> String {
        match self {
            FailureReason::WorkerFailed { worker_id, message } => {
                let msg = message.as_deref().unwrap_or("(no message)");
                format!("Worker {} failed: {}", worker_id, msg)
            }
            FailureReason::ConditionFailed { condition_name } => {
                format!("Failure condition '{}' triggered", condition_name)
            }
            FailureReason::MaxErrorsExceeded { count, limit } => {
                format!("Max errors exceeded ({}/{})", count, limit)
            }
            FailureReason::ExplorationExhausted => {
                "Exploration exhausted without reaching goal".to_string()
            }
            FailureReason::NoValidActions => "No valid actions available".to_string(),
            FailureReason::InternalError { message } => format!("Internal error: {}", message),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_verdict_is_success() {
        assert!(TerminationVerdict::Success {
            reason: SuccessReason::ConditionsMet
        }
        .is_success());

        assert!(TerminationVerdict::Timeout {
            partial_success: true
        }
        .is_success());

        assert!(!TerminationVerdict::Timeout {
            partial_success: false
        }
        .is_success());

        assert!(!TerminationVerdict::Failure {
            reason: FailureReason::ExplorationExhausted
        }
        .is_success());
    }
}