bzzz-core 0.1.0

Bzzz core library - Declarative orchestration engine for AI Agents
Documentation
//! Conditional pattern executor
//!
//! Evaluates condition expression, executes then or else branch.
//! Failure semantics: branch execution failure causes overall failure.
//!
//! ## CR2: CapabilityOutput
//!
//! After branch execution completes, applies expose resolution or output behavior
//! to produce the final capability output.

use async_trait::async_trait;

use crate::template::{ExpressionResolver, HandlebarsResolver};
use crate::{ExecutionMetrics, ExecutionResult, FlowPattern, RunError, RunId, RunStatus};

use super::{build_capability_output, execute_worker, PatternContext, PatternExecutor};

/// Conditional pattern executor
pub struct ConditionalExecutor;

impl Default for ConditionalExecutor {
    fn default() -> Self {
        Self::new()
    }
}

impl ConditionalExecutor {
    /// Create a new conditional executor
    pub fn new() -> Self {
        ConditionalExecutor
    }

    /// Evaluate condition expression
    ///
    /// Uses ExpressionResolver to resolve template expressions like:
    /// - `{{input.x}}` - Input parameters
    /// - `{{steps.worker.output.y}}` - Step outputs
    /// - Literal `true` / `false`
    ///
    /// After resolution, evaluates the result as a boolean.
    fn evaluate_condition(&self, condition: &str, ctx: &PatternContext) -> Result<bool, RunError> {
        let resolver = HandlebarsResolver::new();

        // Resolve the condition expression
        let resolved = resolver
            .resolve(condition, &ctx.scope)
            .map_err(|e| e.to_run_error())?;

        // Evaluate the resolved value as boolean
        let trimmed = resolved.trim().to_lowercase();
        match trimmed.as_str() {
            "true" => Ok(true),
            "false" => Ok(false),
            // Try to parse as JSON boolean
            other => {
                // If it's a number, treat 0 as false, non-zero as true
                if let Ok(n) = other.parse::<i64>() {
                    Ok(n != 0)
                } else if let Ok(n) = other.parse::<f64>() {
                    Ok(n != 0.0)
                } else {
                    // Non-"false" string values default to true
                    // This allows flexible condition syntax
                    Ok(other != "false" && !other.is_empty())
                }
            }
        }
    }
}

#[async_trait]
impl PatternExecutor for ConditionalExecutor {
    fn name(&self) -> &'static str {
        "conditional"
    }

    async fn execute(
        &self,
        ctx: &PatternContext,
        runtime: &dyn crate::RuntimeAdapter,
        cancel: &crate::CancellationToken,
    ) -> Result<ExecutionResult, RunError> {
        let (condition, then_branch, else_branch) = match &ctx.swarm.flow {
            FlowPattern::Conditional {
                condition,
                then,
                else_,
            } => (condition.clone(), then.clone(), else_.clone()),
            _ => {
                return Err(RunError::PatternError {
                    pattern: "conditional".into(),
                    step: "flow".into(),
                    message: "ConditionalExecutor requires Conditional pattern in flow".into(),
                })
            }
        };

        // Check cancellation
        if cancel.is_cancelled().await {
            return Ok(ExecutionResult {
                run_id: RunId::new(),
                status: RunStatus::Cancelled,
                artifacts: vec![],
                error: Some(RunError::Cancelled {
                    reason: "Execution cancelled".into(),
                }),
                metrics: ExecutionMetrics::default(),
                output: None,
            });
        }

        // Evaluate condition
        let condition_result = self.evaluate_condition(&condition, ctx)?;

        // Select branch
        let branch_name = if condition_result {
            then_branch
        } else {
            else_branch.ok_or_else(|| RunError::PatternError {
                pattern: "conditional".into(),
                step: "else".into(),
                message: "No else branch defined for false condition".into(),
            })?
        };

        // Get worker for branch
        let worker = ctx
            .get_worker(&branch_name)
            .ok_or_else(|| RunError::PatternError {
                pattern: "conditional".into(),
                step: branch_name.clone(),
                message: format!("Worker '{}' not found in swarm for branch", branch_name),
            })?;

        // Execute branch with scope for parameter resolution
        let result = execute_worker(worker, runtime, &ctx.runtime_ctx, &ctx.scope, cancel).await?;

        // CR2: Build scope with branch output for expose resolution
        let mut final_scope = ctx.scope.clone();
        if let Some(output) = &result.output {
            final_scope.add_step_output(branch_name.clone(), output.clone());
        }

        let exec_result = ExecutionResult {
            run_id: RunId::new(),
            status: result.status,
            artifacts: result.artifacts,
            error: result.error,
            metrics: result.metrics,
            output: None,
        };

        // CR2: Apply expose resolution or output behavior
        Ok(build_capability_output(
            exec_result,
            &ctx.swarm,
            &final_scope,
        ))
    }

    async fn on_failure(
        &self,
        _ctx: &mut PatternContext,
        _runtime: &dyn crate::RuntimeAdapter,
        _failed_worker: &str,
        _error: &RunError,
    ) -> Result<bool, RunError> {
        // Conditional: branch failure stops execution
        Ok(false)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{CancellationToken, ExecutionContext, FlowPattern, RuntimeKind, SwarmFile};

    #[test]
    fn test_conditional_executor_name() {
        let executor = ConditionalExecutor::new();
        assert_eq!(executor.name(), "conditional");
    }

    #[test]
    fn test_condition_evaluation() {
        let executor = ConditionalExecutor::new();
        let swarm = SwarmFile::new(
            "test",
            FlowPattern::Conditional {
                condition: "true".into(),
                then: "a".into(),
                else_: None,
            },
        );
        let ctx = PatternContext::new(swarm, ExecutionContext::new("ctx", RuntimeKind::Local));

        assert!(executor.evaluate_condition("true", &ctx).unwrap());
        assert!(!executor.evaluate_condition("false", &ctx).unwrap());
        // Non-"false" values default to true (after resolution)
        assert!(executor.evaluate_condition("x > 0", &ctx).unwrap());
    }

    #[tokio::test]
    async fn test_conditional_executor_wrong_pattern() {
        let executor = ConditionalExecutor::new();
        let swarm = SwarmFile::new("test", FlowPattern::Sequence { steps: vec![] });
        let ctx = PatternContext::new(swarm, ExecutionContext::new("ctx", RuntimeKind::Local));
        let cancel = CancellationToken::new();

        let result = executor
            .execute(&ctx, &crate::LocalRuntime::new(), &cancel)
            .await;
        assert!(result.is_err());
    }
}