ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! DecisionContext: Information available for making decisions

use super::action::ActionResult;
use super::state::AgentState;
use serde::{Deserialize, Serialize};

/// Context for making a decision
///
/// Contains all the information a Decider needs to choose the next action.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DecisionContext {
    /// The agent's ID
    pub agent_id: u32,

    /// The original query/goal
    pub query: String,

    /// Current phase of execution
    pub phase: String,

    /// Current tick number
    pub tick: u64,

    /// Recent action results (for learning from past)
    pub recent_results: Vec<ActionResult>,

    /// Current agent state
    pub state: AgentState,

    /// Files currently being worked on
    pub active_files: Vec<String>,

    /// Remaining work items
    pub remaining_work: Vec<String>,

    /// Custom metadata
    #[serde(default)]
    pub metadata: std::collections::HashMap<String, String>,
}

impl DecisionContext {
    /// Create a new context
    pub fn new(agent_id: u32, query: impl Into<String>) -> Self {
        Self {
            agent_id,
            query: query.into(),
            ..Default::default()
        }
    }

    /// Set the current phase
    pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
        self.phase = phase.into();
        self
    }

    /// Set the current tick
    pub fn with_tick(mut self, tick: u64) -> Self {
        self.tick = tick;
        self
    }

    /// Add a recent result
    pub fn add_result(&mut self, result: ActionResult) {
        self.recent_results.push(result);
        // Keep only last N results
        if self.recent_results.len() > 10 {
            self.recent_results.remove(0);
        }
    }

    /// Set active files
    pub fn with_active_files(mut self, files: Vec<String>) -> Self {
        self.active_files = files;
        self
    }

    /// Set remaining work
    pub fn with_remaining_work(mut self, work: Vec<String>) -> Self {
        self.remaining_work = work;
        self
    }

    /// Add metadata
    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }

    /// Get the last result
    pub fn last_result(&self) -> Option<&ActionResult> {
        self.recent_results.last()
    }

    /// Check if the last action succeeded
    pub fn last_succeeded(&self) -> bool {
        self.last_result().map(|r| r.success).unwrap_or(true)
    }

    /// Check if the last action failed
    pub fn last_failed(&self) -> bool {
        self.last_result().map(|r| !r.success).unwrap_or(false)
    }

    /// Count recent failures
    pub fn recent_failure_count(&self) -> usize {
        self.recent_results.iter().filter(|r| !r.success).count()
    }

    /// Calculate recent success rate
    pub fn recent_success_rate(&self) -> f64 {
        if self.recent_results.is_empty() {
            return 1.0;
        }
        let successes = self.recent_results.iter().filter(|r| r.success).count();
        successes as f64 / self.recent_results.len() as f64
    }

    /// Check if there's remaining work
    pub fn has_remaining_work(&self) -> bool {
        !self.remaining_work.is_empty()
    }

    /// Get the next work item
    pub fn next_work_item(&self) -> Option<&String> {
        self.remaining_work.first()
    }

    /// Check if query contains a keyword (case-insensitive)
    pub fn query_contains(&self, keyword: &str) -> bool {
        self.query.to_lowercase().contains(&keyword.to_lowercase())
    }

    /// Check if query contains any of the keywords
    pub fn query_contains_any(&self, keywords: &[&str]) -> bool {
        let query_lower = self.query.to_lowercase();
        keywords
            .iter()
            .any(|k| query_lower.contains(&k.to_lowercase()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::decider::action::Action;

    #[test]
    fn test_context_creation() {
        let ctx = DecisionContext::new(0, "rename foo to bar")
            .with_phase("executing")
            .with_tick(5);

        assert_eq!(ctx.agent_id, 0);
        assert_eq!(ctx.query, "rename foo to bar");
        assert_eq!(ctx.phase, "executing");
        assert_eq!(ctx.tick, 5);
    }

    #[test]
    fn test_context_results() {
        let mut ctx = DecisionContext::new(0, "test");

        let success = super::super::action::ActionResult::success(Action::read("a.rs"));
        let failure = super::super::action::ActionResult::failure(Action::read("b.rs"), "error");

        ctx.add_result(success);
        ctx.add_result(failure);

        assert!(!ctx.last_succeeded());
        assert!(ctx.last_failed());
        assert_eq!(ctx.recent_failure_count(), 1);
        assert_eq!(ctx.recent_success_rate(), 0.5);
    }

    #[test]
    fn test_query_keywords() {
        let ctx = DecisionContext::new(0, "Rename function foo to bar");

        assert!(ctx.query_contains("rename"));
        assert!(ctx.query_contains("RENAME")); // case insensitive
        assert!(ctx.query_contains_any(&["rename", "delete", "add"]));
        assert!(!ctx.query_contains_any(&["compile", "test"]));
    }
}