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}