ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! Action: What an agent decides to do
//!
//! Actions are the output of a Decider's decision-making process.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

/// An action that an agent can take
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
    /// The kind of action
    pub kind: ActionKind,

    /// Target of the action (file path, symbol name, etc.)
    pub target: Option<String>,

    /// Additional arguments
    pub args: HashMap<String, String>,

    /// Reason for this action (for logging/debugging)
    pub reason: Option<String>,
}

impl Action {
    /// Create a new action
    pub fn new(kind: ActionKind) -> Self {
        Self {
            kind,
            target: None,
            args: HashMap::new(),
            reason: None,
        }
    }

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

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

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

    /// Create a read action
    pub fn read(path: impl Into<String>) -> Self {
        Self::new(ActionKind::Read).with_target(path)
    }

    /// Create a grep action
    pub fn grep(pattern: impl Into<String>) -> Self {
        Self::new(ActionKind::Grep).with_arg("pattern", pattern)
    }

    /// Create a glob action
    pub fn glob(pattern: impl Into<String>) -> Self {
        Self::new(ActionKind::Glob).with_arg("pattern", pattern)
    }

    /// Create a mutation action
    pub fn mutate(mutation_type: impl Into<String>, target: impl Into<String>) -> Self {
        Self::new(ActionKind::Mutate)
            .with_arg("mutation_type", mutation_type)
            .with_target(target)
    }

    /// Create a rest action (agent has nothing to do)
    pub fn rest(reason: impl Into<String>) -> Self {
        Self::new(ActionKind::Rest).with_reason(reason)
    }

    /// Create a done action (agent completed its task)
    pub fn done() -> Self {
        Self::new(ActionKind::Done)
    }

    /// Check if this is a terminal action
    pub fn is_terminal(&self) -> bool {
        matches!(self.kind, ActionKind::Done | ActionKind::Rest)
    }
}

/// The kind of action
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ActionKind {
    // === Investigation ===
    /// Read a file
    Read,
    /// Search with grep pattern
    Grep,
    /// Search with glob pattern
    Glob,
    /// List directory
    List,

    // === Mutation ===
    /// Apply a mutation
    Mutate,
    /// Apply a batch of mutations
    MutateBatch,

    // === Control ===
    /// Rest (nothing to do right now)
    Rest,
    /// Done (completed task)
    Done,
    /// Escalate to higher-level decision maker
    Escalate,
}

impl ActionKind {
    /// Get the name of this action kind
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Read => "read",
            Self::Grep => "grep",
            Self::Glob => "glob",
            Self::List => "list",
            Self::Mutate => "mutate",
            Self::MutateBatch => "mutate_batch",
            Self::Rest => "rest",
            Self::Done => "done",
            Self::Escalate => "escalate",
        }
    }

    /// Check if this is an investigation action
    pub fn is_investigation(&self) -> bool {
        matches!(self, Self::Read | Self::Grep | Self::Glob | Self::List)
    }

    /// Check if this is a mutation action
    pub fn is_mutation(&self) -> bool {
        matches!(self, Self::Mutate | Self::MutateBatch)
    }
}

/// Result of executing an action
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionResult {
    /// The action that was executed
    pub action: Action,

    /// Whether the action succeeded
    pub success: bool,

    /// Output/result of the action
    pub output: Option<String>,

    /// Error message if failed
    pub error: Option<String>,

    /// Duration in microseconds
    pub duration_us: u64,

    /// Files affected
    pub affected_files: Vec<PathBuf>,

    /// Changes made (for mutations)
    pub changes: usize,
}

impl ActionResult {
    /// Create a successful result
    pub fn success(action: Action) -> Self {
        Self {
            action,
            success: true,
            output: None,
            error: None,
            duration_us: 0,
            affected_files: Vec::new(),
            changes: 0,
        }
    }

    /// Create a failed result
    pub fn failure(action: Action, error: impl Into<String>) -> Self {
        Self {
            action,
            success: false,
            output: None,
            error: Some(error.into()),
            duration_us: 0,
            affected_files: Vec::new(),
            changes: 0,
        }
    }

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

    /// Set the duration
    pub fn with_duration(mut self, duration_us: u64) -> Self {
        self.duration_us = duration_us;
        self
    }

    /// Set affected files
    pub fn with_affected_files(mut self, files: Vec<PathBuf>) -> Self {
        self.affected_files = files;
        self
    }

    /// Set changes count
    pub fn with_changes(mut self, changes: usize) -> Self {
        self.changes = changes;
        self
    }
}

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

    #[test]
    fn test_action_creation() {
        let action = Action::read("src/lib.rs");
        assert_eq!(action.kind, ActionKind::Read);
        assert_eq!(action.target, Some("src/lib.rs".to_string()));

        let action = Action::grep("TODO").with_target("src/");
        assert_eq!(action.kind, ActionKind::Grep);
        assert_eq!(action.args.get("pattern"), Some(&"TODO".to_string()));

        let action = Action::mutate("Rename", "old_fn");
        assert_eq!(action.kind, ActionKind::Mutate);
        assert_eq!(action.target, Some("old_fn".to_string()));
    }

    #[test]
    fn test_action_result() {
        let action = Action::read("test.rs");
        let result = ActionResult::success(action.clone())
            .with_output("file contents")
            .with_duration(100);

        assert!(result.success);
        assert_eq!(result.output, Some("file contents".to_string()));
        assert_eq!(result.duration_us, 100);

        let result = ActionResult::failure(action, "File not found");
        assert!(!result.success);
        assert_eq!(result.error, Some("File not found".to_string()));
    }
}