Skip to main content

execution_engine/
types.rs

1//! Execution Engine Types
2//!
3//! Core data types for execution requests and results.
4//! See docs/types.md for complete reference.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::time::Duration;
11use uuid::Uuid;
12
13// ============================================================================
14// Input Types
15// ============================================================================
16
17/// Single command execution request
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ExecutionRequest {
20    /// Unique identifier for this request
21    pub id: Uuid,
22
23    /// Command to execute
24    pub command: Command,
25
26    /// Environment variables
27    #[serde(default)]
28    pub env: HashMap<String, String>,
29
30    /// Working directory (optional)
31    pub working_dir: Option<PathBuf>,
32
33    /// Timeout in milliseconds (optional)
34    pub timeout_ms: Option<u64>,
35
36    /// Optional path to save stdout/stderr output to a file
37    pub output_log_path: Option<PathBuf>,
38
39    /// Metadata for tracking
40    #[serde(default)]
41    pub metadata: ExecutionMetadata,
42}
43
44/// Plan for executing multiple commands
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ExecutionPlan {
47    /// Unique identifier for this plan
48    pub id: Uuid,
49
50    /// Human-readable description
51    pub description: String,
52
53    /// Execution strategy
54    pub strategy: ExecutionStrategy,
55
56    /// Commands to execute
57    pub commands: Vec<ExecutionRequest>,
58
59    /// Metadata
60    #[serde(default)]
61    pub metadata: ExecutionMetadata,
62}
63
64/// Standardized command types
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "type", rename_all = "snake_case")]
67pub enum Command {
68    /// Execute a script file
69    Script {
70        path: PathBuf,
71        interpreter: Option<String>,
72    },
73
74    /// Execute command with arguments
75    Exec { program: String, args: Vec<String> },
76
77    /// Execute shell command string
78    Shell {
79        command: String,
80        #[serde(default = "default_shell")]
81        shell: String,
82    },
83
84    /// AWS CLI command (convenience)
85    AwsCli {
86        service: String,
87        operation: String,
88        #[serde(default)]
89        args: Vec<String>,
90        profile: Option<String>,
91        region: Option<String>,
92    },
93}
94
95fn default_shell() -> String {
96    if cfg!(target_os = "windows") {
97        "powershell".to_string()
98    } else {
99        "bash".to_string()
100    }
101}
102
103/// Strategy for executing multiple commands
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(tag = "type", rename_all = "snake_case")]
106pub enum ExecutionStrategy {
107    /// Execute sequentially, stop on error
108    Serial {
109        #[serde(default = "default_stop_on_error")]
110        stop_on_error: bool,
111    },
112
113    /// Execute concurrently
114    Parallel { max_concurrency: Option<usize> },
115
116    /// Execute based on dependency graph
117    DependencyGraph {
118        dependencies: HashMap<usize, Vec<usize>>,
119    },
120}
121
122fn default_stop_on_error() -> bool {
123    true
124}
125
126// ============================================================================
127// Output Types
128// ============================================================================
129
130/// Result of a single command execution
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ExecutionResult {
133    /// Execution ID
134    pub id: Uuid,
135
136    /// Final status
137    pub status: ExecutionStatus,
138
139    /// Success flag (exit_code == 0)
140    pub success: bool,
141
142    /// Process exit code
143    pub exit_code: i32,
144
145    /// Standard output
146    pub stdout: String,
147
148    /// Standard error
149    pub stderr: String,
150
151    /// Execution duration
152    pub duration: Duration,
153
154    /// Start timestamp
155    pub started_at: DateTime<Utc>,
156
157    /// Completion timestamp
158    pub completed_at: Option<DateTime<Utc>>,
159
160    /// Error message (if failed)
161    pub error: Option<String>,
162
163    /// Path to overflow stdout file (when using StreamToFile strategy)
164    pub stdout_overflow_file: Option<PathBuf>,
165
166    /// Path to overflow stderr file (when using StreamToFile strategy)
167    pub stderr_overflow_file: Option<PathBuf>,
168}
169
170/// Result of executing a plan
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct PlanExecutionResult {
173    /// Plan ID
174    pub plan_id: Uuid,
175
176    /// Overall status
177    pub status: ExecutionStatus,
178
179    /// Results for each command
180    pub results: Vec<ExecutionResult>,
181
182    /// Total duration
183    pub total_duration: Duration,
184
185    /// Statistics
186    pub stats: ExecutionStats,
187}
188
189/// Status enum for executions
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum ExecutionStatus {
193    Pending,
194    Running,
195    Completed,
196    Failed,
197    Cancelled,
198    Timeout,
199}
200
201impl ExecutionStatus {
202    /// Check if status is terminal (execution is finished)
203    #[must_use]
204    pub fn is_terminal(&self) -> bool {
205        matches!(
206            self,
207            ExecutionStatus::Completed
208                | ExecutionStatus::Failed
209                | ExecutionStatus::Cancelled
210                | ExecutionStatus::Timeout
211        )
212    }
213}
214
215/// Statistics for plan execution
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ExecutionStats {
218    pub total: usize,
219    pub completed: usize,
220    pub failed: usize,
221    pub cancelled: usize,
222    pub timeout: usize,
223}
224
225impl ExecutionStats {
226    /// Create new empty stats
227    #[must_use]
228    pub fn new(total: usize) -> Self {
229        Self {
230            total,
231            completed: 0,
232            failed: 0,
233            cancelled: 0,
234            timeout: 0,
235        }
236    }
237
238    /// Update stats based on status
239    pub fn update(&mut self, status: ExecutionStatus) {
240        match status {
241            ExecutionStatus::Completed => self.completed += 1,
242            ExecutionStatus::Failed => self.failed += 1,
243            ExecutionStatus::Cancelled => self.cancelled += 1,
244            ExecutionStatus::Timeout => self.timeout += 1,
245            _ => {}
246        }
247    }
248}
249
250// ============================================================================
251// Metadata Types
252// ============================================================================
253
254/// Metadata for tracking executions
255#[derive(Debug, Clone, Serialize, Deserialize, Default)]
256pub struct ExecutionMetadata {
257    /// Source of request
258    pub source: Option<String>,
259
260    /// Related conversation ID
261    pub conversation_id: Option<Uuid>,
262
263    /// Custom tags
264    #[serde(default)]
265    pub tags: HashMap<String, String>,
266}
267
268/// Summary information for listing executions
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct ExecutionSummary {
271    pub id: Uuid,
272    pub status: ExecutionStatus,
273    pub started_at: DateTime<Utc>,
274    pub duration: Option<Duration>,
275}
276
277// ============================================================================
278// Internal Types (not part of public API)
279// ============================================================================
280
281/// Internal state tracking
282#[derive(Debug, Clone)]
283pub struct ExecutionState {
284    pub id: Uuid,
285    pub request: ExecutionRequest,
286    pub status: ExecutionStatus,
287    pub started_at: DateTime<Utc>,
288    pub completed_at: Option<DateTime<Utc>>,
289    pub stdout: String,
290    pub stderr: String,
291    pub exit_code: Option<i32>,
292    pub error: Option<String>,
293    pub stdout_overflow_file: Option<PathBuf>,
294    pub stderr_overflow_file: Option<PathBuf>,
295}
296
297impl ExecutionState {
298    /// Create new execution state
299    #[must_use]
300    pub fn new(request: ExecutionRequest) -> Self {
301        Self {
302            id: request.id,
303            request,
304            status: ExecutionStatus::Pending,
305            started_at: Utc::now(),
306            completed_at: None,
307            stdout: String::new(),
308            stderr: String::new(),
309            exit_code: None,
310            error: None,
311            stdout_overflow_file: None,
312            stderr_overflow_file: None,
313        }
314    }
315
316    /// Convert to ExecutionResult
317    #[must_use]
318    pub fn to_result(&self) -> ExecutionResult {
319        let duration = if let Some(completed) = self.completed_at {
320            (completed - self.started_at)
321                .to_std()
322                .unwrap_or(Duration::from_secs(0))
323        } else {
324            Duration::from_secs(0)
325        };
326
327        ExecutionResult {
328            id: self.id,
329            status: self.status,
330            success: self.exit_code == Some(0),
331            exit_code: self.exit_code.unwrap_or(-1),
332            stdout: self.stdout.clone(),
333            stderr: self.stderr.clone(),
334            duration,
335            started_at: self.started_at,
336            completed_at: self.completed_at,
337            error: self.error.clone(),
338            stdout_overflow_file: self.stdout_overflow_file.clone(),
339            stderr_overflow_file: self.stderr_overflow_file.clone(),
340        }
341    }
342}
343
344// ============================================================================
345// Tests
346// ============================================================================
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_execution_status_terminal() {
354        assert!(!ExecutionStatus::Pending.is_terminal());
355        assert!(!ExecutionStatus::Running.is_terminal());
356        assert!(ExecutionStatus::Completed.is_terminal());
357        assert!(ExecutionStatus::Failed.is_terminal());
358        assert!(ExecutionStatus::Cancelled.is_terminal());
359        assert!(ExecutionStatus::Timeout.is_terminal());
360    }
361
362    #[test]
363    fn test_execution_stats_update() {
364        let mut stats = ExecutionStats::new(5);
365        assert_eq!(stats.total, 5);
366        assert_eq!(stats.completed, 0);
367
368        stats.update(ExecutionStatus::Completed);
369        assert_eq!(stats.completed, 1);
370
371        stats.update(ExecutionStatus::Failed);
372        assert_eq!(stats.failed, 1);
373
374        stats.update(ExecutionStatus::Timeout);
375        assert_eq!(stats.timeout, 1);
376    }
377
378    #[test]
379    fn test_command_serialization() {
380        let cmd = Command::Shell {
381            command: "echo hello".to_string(),
382            shell: "bash".to_string(),
383        };
384
385        let json = serde_json::to_string(&cmd).unwrap();
386        assert!(json.contains("shell"));
387        assert!(json.contains("echo hello"));
388
389        let deserialized: Command = serde_json::from_str(&json).unwrap();
390        match deserialized {
391            Command::Shell { command, shell } => {
392                assert_eq!(command, "echo hello");
393                assert_eq!(shell, "bash");
394            }
395            _ => panic!("Wrong variant"),
396        }
397    }
398
399    #[test]
400    fn test_execution_request_default_fields() {
401        let request = ExecutionRequest {
402            id: Uuid::new_v4(),
403            command: Command::Shell {
404                command: "ls".to_string(),
405                shell: "bash".to_string(),
406            },
407            env: HashMap::new(),
408            working_dir: None,
409            timeout_ms: None,
410            output_log_path: None,
411            metadata: ExecutionMetadata::default(),
412        };
413
414        assert!(request.env.is_empty());
415        assert!(request.working_dir.is_none());
416        assert!(request.timeout_ms.is_none());
417        assert!(request.metadata.source.is_none());
418    }
419
420    #[test]
421    fn test_execution_state_to_result() {
422        let request = ExecutionRequest {
423            id: Uuid::new_v4(),
424            command: Command::Shell {
425                command: "echo test".to_string(),
426                shell: "bash".to_string(),
427            },
428            env: HashMap::new(),
429            working_dir: None,
430            timeout_ms: None,
431            output_log_path: None,
432            metadata: ExecutionMetadata::default(),
433        };
434
435        let mut state = ExecutionState::new(request);
436        state.status = ExecutionStatus::Completed;
437        state.exit_code = Some(0);
438        state.stdout = "test output".to_string();
439        state.completed_at = Some(Utc::now());
440
441        let result = state.to_result();
442        assert_eq!(result.status, ExecutionStatus::Completed);
443        assert!(result.success);
444        assert_eq!(result.exit_code, 0);
445        assert_eq!(result.stdout, "test output");
446    }
447
448    #[test]
449    fn test_default_shell() {
450        let shell = default_shell();
451        if cfg!(target_os = "windows") {
452            assert_eq!(shell, "powershell");
453        } else {
454            assert_eq!(shell, "bash");
455        }
456    }
457
458    #[test]
459    fn test_execution_metadata_default() {
460        let metadata = ExecutionMetadata::default();
461        assert!(metadata.source.is_none());
462        assert!(metadata.conversation_id.is_none());
463        assert!(metadata.tags.is_empty());
464    }
465
466    #[test]
467    fn test_execution_strategy_serialization() {
468        let strategy = ExecutionStrategy::Serial {
469            stop_on_error: true,
470        };
471
472        let json = serde_json::to_string(&strategy).unwrap();
473        assert!(json.contains("serial"));
474        assert!(json.contains("stop_on_error"));
475    }
476}