1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::time::Duration;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ExecutionRequest {
20 pub id: Uuid,
22
23 pub command: Command,
25
26 #[serde(default)]
28 pub env: HashMap<String, String>,
29
30 pub working_dir: Option<PathBuf>,
32
33 pub timeout_ms: Option<u64>,
35
36 pub output_log_path: Option<PathBuf>,
38
39 #[serde(default)]
41 pub metadata: ExecutionMetadata,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ExecutionPlan {
47 pub id: Uuid,
49
50 pub description: String,
52
53 pub strategy: ExecutionStrategy,
55
56 pub commands: Vec<ExecutionRequest>,
58
59 #[serde(default)]
61 pub metadata: ExecutionMetadata,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "type", rename_all = "snake_case")]
67pub enum Command {
68 Script {
70 path: PathBuf,
71 interpreter: Option<String>,
72 },
73
74 Exec { program: String, args: Vec<String> },
76
77 Shell {
79 command: String,
80 #[serde(default = "default_shell")]
81 shell: String,
82 },
83
84 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#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(tag = "type", rename_all = "snake_case")]
106pub enum ExecutionStrategy {
107 Serial {
109 #[serde(default = "default_stop_on_error")]
110 stop_on_error: bool,
111 },
112
113 Parallel { max_concurrency: Option<usize> },
115
116 DependencyGraph {
118 dependencies: HashMap<usize, Vec<usize>>,
119 },
120}
121
122fn default_stop_on_error() -> bool {
123 true
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ExecutionResult {
133 pub id: Uuid,
135
136 pub status: ExecutionStatus,
138
139 pub success: bool,
141
142 pub exit_code: i32,
144
145 pub stdout: String,
147
148 pub stderr: String,
150
151 pub duration: Duration,
153
154 pub started_at: DateTime<Utc>,
156
157 pub completed_at: Option<DateTime<Utc>>,
159
160 pub error: Option<String>,
162
163 pub stdout_overflow_file: Option<PathBuf>,
165
166 pub stderr_overflow_file: Option<PathBuf>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct PlanExecutionResult {
173 pub plan_id: Uuid,
175
176 pub status: ExecutionStatus,
178
179 pub results: Vec<ExecutionResult>,
181
182 pub total_duration: Duration,
184
185 pub stats: ExecutionStats,
187}
188
189#[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 #[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#[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 #[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 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
256pub struct ExecutionMetadata {
257 pub source: Option<String>,
259
260 pub conversation_id: Option<Uuid>,
262
263 #[serde(default)]
265 pub tags: HashMap<String, String>,
266}
267
268#[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#[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 #[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 #[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#[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}