Skip to main content

ralph_workflow/executor/
executor_trait.rs

1//! ProcessExecutor trait definition.
2//!
3//! This module defines the trait abstraction for process execution,
4//! enabling dependency injection for testing.
5
6use super::{AgentChildHandle, AgentSpawnConfig, ProcessOutput, RealAgentChild};
7use std::io;
8use std::path::Path;
9
10/// Trait for executing external processes.
11///
12/// This trait abstracts process execution to allow dependency injection.
13/// Production code uses `RealProcessExecutor` which calls actual commands.
14/// Test code can use `MockProcessExecutor` to control process behavior.
15///
16/// Only external process execution is abstracted. Internal code logic is never mocked.
17pub trait ProcessExecutor: Send + Sync + std::fmt::Debug {
18    /// Execute a command with given arguments and return its output.
19    ///
20    /// # Arguments
21    ///
22    /// * `command` - The program to execute
23    /// * `args` - Command-line arguments to pass to the program
24    /// * `env` - Environment variables to set for the process (optional)
25    /// * `workdir` - Working directory for the process (optional)
26    ///
27    /// # Returns
28    ///
29    /// Returns a `ProcessOutput` containing exit status, stdout, and stderr.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if command cannot be spawned or if output capture fails.
34    fn execute(
35        &self,
36        command: &str,
37        args: &[&str],
38        env: &[(String, String)],
39        workdir: Option<&Path>,
40    ) -> io::Result<ProcessOutput>;
41
42    /// Spawn a process with stdin input and return the child handle.
43    ///
44    /// This method is used when you need to write to the process's stdin
45    /// or stream its output in real-time. Unlike `execute()`, this returns
46    /// a `Child` handle for direct interaction.
47    ///
48    /// # Arguments
49    ///
50    /// * `command` - The program to execute
51    /// * `args` - Command-line arguments to pass to the program
52    /// * `env` - Environment variables to set for the process (optional)
53    /// * `workdir` - Working directory for the process (optional)
54    ///
55    /// # Returns
56    ///
57    /// Returns a `Child` handle that can be used to interact with the process.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if command cannot be spawned.
62    fn spawn(
63        &self,
64        command: &str,
65        args: &[&str],
66        env: &[(String, String)],
67        workdir: Option<&Path>,
68    ) -> io::Result<std::process::Child> {
69        let mut cmd = std::process::Command::new(command);
70        cmd.args(args);
71
72        for (key, value) in env {
73            cmd.env(key, value);
74        }
75
76        if let Some(dir) = workdir {
77            cmd.current_dir(dir);
78        }
79
80        cmd.stdin(std::process::Stdio::piped())
81            .stdout(std::process::Stdio::piped())
82            .stderr(std::process::Stdio::piped())
83            .spawn()
84    }
85
86    /// Spawn an agent process with streaming output support.
87    ///
88    /// This method is specifically designed for spawning AI agent subprocesses
89    /// that need to output streaming JSON in real-time. Unlike `spawn()`, this
90    /// returns a handle with boxed stdout for trait object compatibility.
91    ///
92    /// # Arguments
93    ///
94    /// * `config` - Agent spawn configuration including command, args, env, prompt, etc.
95    ///
96    /// # Returns
97    ///
98    /// Returns an `AgentChildHandle` with stdout, stderr, and the child process.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the agent cannot be spawned.
103    ///
104    /// # Default Implementation
105    ///
106    /// The default implementation uses the `spawn()` method with additional
107    /// configuration for agent-specific needs. Mock implementations should
108    /// override this to return mock results without spawning real processes.
109    fn spawn_agent(&self, config: &AgentSpawnConfig) -> io::Result<AgentChildHandle> {
110        let mut cmd = std::process::Command::new(&config.command);
111        cmd.args(&config.args);
112
113        // Set environment variables
114        for (key, value) in &config.env {
115            cmd.env(key, value);
116        }
117
118        // Add the prompt as the final argument
119        cmd.arg(&config.prompt);
120
121        // Set buffering variables for real-time streaming
122        cmd.env("PYTHONUNBUFFERED", "1");
123        cmd.env("NODE_ENV", "production");
124
125        // Spawn the process with piped stdout/stderr
126        let mut child = cmd
127            .stdin(std::process::Stdio::null())
128            .stdout(std::process::Stdio::piped())
129            .stderr(std::process::Stdio::piped())
130            .spawn()?;
131
132        let stdout = child
133            .stdout
134            .take()
135            .ok_or_else(|| io::Error::other("Failed to capture stdout"))?;
136        let stderr = child
137            .stderr
138            .take()
139            .ok_or_else(|| io::Error::other("Failed to capture stderr"))?;
140
141        Ok(AgentChildHandle {
142            stdout: Box::new(stdout),
143            stderr: Box::new(stderr),
144            inner: Box::new(RealAgentChild(child)),
145        })
146    }
147
148    /// Check if a command exists and can be executed.
149    ///
150    /// This is a convenience method that executes a command with a
151    /// `--version` or similar flag to check if it's available.
152    ///
153    /// # Arguments
154    ///
155    /// * `command` - The program to check
156    ///
157    /// # Returns
158    ///
159    /// Returns `true` if command exists, `false` otherwise.
160    fn command_exists(&self, command: &str) -> bool {
161        match self.execute(command, &[], &[], None) {
162            Ok(output) => output.status.success(),
163            Err(_) => false,
164        }
165    }
166}