mecha10-behavior-runtime 0.8.2

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Execution engine for behavior trees
//!
//! This module provides the executor that runs behavior trees with tick-based execution.

use crate::{BoxedBehavior, NodeStatus};
use mecha10_core::Context;
use std::time::{Duration, Instant};
use tracing::{debug, info, warn};

#[derive(Clone)]
pub struct ExecutionContext {
    ctx: Context,

    tick_rate_hz: f32,
}

impl ExecutionContext {
    /// Create a new execution context.
    pub fn new(ctx: Context, tick_rate_hz: f32) -> Self {
        Self { ctx, tick_rate_hz }
    }

    /// Get the underlying context.
    pub fn context(&self) -> &Context {
        &self.ctx
    }

    /// Get the tick rate in Hz.
    pub fn tick_rate_hz(&self) -> f32 {
        self.tick_rate_hz
    }
}

#[derive(Debug, Clone)]
pub struct ExecutionStats {
    pub tick_count: usize,

    pub total_duration: Duration,

    pub avg_tick_duration: Duration,

    pub min_tick_duration: Duration,

    pub max_tick_duration: Duration,

    pub final_status: Option<NodeStatus>,
}

impl ExecutionStats {
    fn new() -> Self {
        Self {
            tick_count: 0,
            total_duration: Duration::ZERO,
            avg_tick_duration: Duration::ZERO,
            min_tick_duration: Duration::MAX,
            max_tick_duration: Duration::ZERO,
            final_status: None,
        }
    }

    fn update(&mut self, tick_duration: Duration) {
        self.tick_count += 1;
        self.total_duration += tick_duration;

        if tick_duration < self.min_tick_duration {
            self.min_tick_duration = tick_duration;
        }
        if tick_duration > self.max_tick_duration {
            self.max_tick_duration = tick_duration;
        }

        self.avg_tick_duration = self.total_duration / self.tick_count as u32;
    }

    fn finalize(&mut self, status: NodeStatus) {
        self.final_status = Some(status);
    }
}

pub struct BehaviorExecutor {
    behavior: BoxedBehavior,
    tick_rate_hz: f32,
    max_ticks: Option<usize>,
}

macro_rules! lifecycle_step {
    ($(#[$meta:meta])* $fn_name:ident, $verb:literal, $behavior_method:ident) => {
        $(#[$meta])*
        pub async fn $fn_name(&mut self, ctx: &Context) -> anyhow::Result<()> {
            info!(concat!($verb, " behavior: {}"), self.behavior.name());
            self.behavior.$behavior_method(ctx).await?;
            Ok(())
        }
    };
}

impl BehaviorExecutor {
    mecha10_core::new_with_defaults!(
        /// Create a new behavior executor for `behavior`, ticking at `tick_rate_hz` (e.g. 30.0).
        pub fn new(behavior: BoxedBehavior, tick_rate_hz: f32) -> Self {
            max_ticks: None
        }
    );

    mecha10_core::builder_setter!(into_some, with_max_ticks, max_ticks, usize);

    lifecycle_step!(init, "Initializing", on_init);

    /// Run the behavior until it completes (Success or Failure), ticking at the configured rate.
    ///
    /// Returns the final status and execution statistics.
    pub async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
        let mut stats = ExecutionStats::new();
        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));

        info!(
            "Starting behavior execution: {} at {} Hz",
            self.behavior.name(),
            self.tick_rate_hz
        );

        loop {
            interval.tick().await;

            if let Some(max_ticks) = self.max_ticks {
                if stats.tick_count >= max_ticks {
                    warn!("Behavior exceeded maximum ticks ({})", max_ticks);
                    return Err(anyhow::anyhow!("Exceeded maximum ticks"));
                }
            }

            let tick_start = Instant::now();
            let status = self.behavior.tick(ctx).await?;
            let tick_duration = tick_start.elapsed();

            stats.update(tick_duration);

            debug!(
                "Tick #{}: status={}, duration={:?}",
                stats.tick_count, status, tick_duration
            );

            if status.is_complete() {
                info!(
                    "Behavior completed with status: {} after {} ticks ({:?})",
                    status, stats.tick_count, stats.total_duration
                );
                stats.finalize(status);
                return Ok((status, stats));
            }
        }
    }

    /// Run the behavior for a fixed duration, ticking at the configured rate regardless of
    /// whether it completes.
    pub async fn run_for_duration(
        &mut self,
        ctx: &Context,
        duration: Duration,
    ) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
        let mut stats = ExecutionStats::new();
        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
        let start_time = Instant::now();

        info!(
            "Starting behavior execution: {} for {:?} at {} Hz",
            self.behavior.name(),
            duration,
            self.tick_rate_hz
        );

        let mut last_status = NodeStatus::Running;

        while start_time.elapsed() < duration {
            interval.tick().await;

            let tick_start = Instant::now();
            let status = self.behavior.tick(ctx).await?;
            let tick_duration = tick_start.elapsed();

            stats.update(tick_duration);
            last_status = status;

            debug!(
                "Tick #{}: status={}, duration={:?}",
                stats.tick_count, status, tick_duration
            );

            if status.is_complete() {
                info!("Behavior completed early with status: {}", status);
                break;
            }
        }

        info!(
            "Behavior execution ended after {} ticks ({:?})",
            stats.tick_count, stats.total_duration
        );
        stats.finalize(last_status);
        Ok((last_status, stats))
    }

    lifecycle_step!(terminate, "Terminating", on_terminate);

    /// Reset the behavior to its initial state.
    pub async fn reset(&mut self) -> anyhow::Result<()> {
        info!("Resetting behavior: {}", self.behavior.name());
        self.behavior.reset().await?;
        Ok(())
    }
}