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