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 #[serde(default)]
38 pub metadata: ExecutionMetadata,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ExecutionPlan {
44 pub id: Uuid,
46
47 pub description: String,
49
50 pub strategy: ExecutionStrategy,
52
53 pub commands: Vec<ExecutionRequest>,
55
56 #[serde(default)]
58 pub metadata: ExecutionMetadata,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type", rename_all = "snake_case")]
64pub enum Command {
65 Script {
67 path: PathBuf,
68 interpreter: Option<String>,
69 },
70
71 Exec { program: String, args: Vec<String> },
73
74 Shell {
76 command: String,
77 #[serde(default = "default_shell")]
78 shell: String,
79 },
80
81 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#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(tag = "type", rename_all = "snake_case")]
103pub enum ExecutionStrategy {
104 Serial {
106 #[serde(default = "default_stop_on_error")]
107 stop_on_error: bool,
108 },
109
110 Parallel { max_concurrency: Option<usize> },
112
113 DependencyGraph {
115 dependencies: HashMap<usize, Vec<usize>>,
116 },
117}
118
119fn default_stop_on_error() -> bool {
120 true
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ExecutionResult {
130 pub id: Uuid,
132
133 pub status: ExecutionStatus,
135
136 pub success: bool,
138
139 pub exit_code: i32,
141
142 pub stdout: String,
144
145 pub stderr: String,
147
148 pub duration: Duration,
150
151 pub started_at: DateTime<Utc>,
153
154 pub completed_at: Option<DateTime<Utc>>,
156
157 pub error: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct PlanExecutionResult {
164 pub plan_id: Uuid,
166
167 pub status: ExecutionStatus,
169
170 pub results: Vec<ExecutionResult>,
172
173 pub total_duration: Duration,
175
176 pub stats: ExecutionStats,
178}
179
180#[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 #[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#[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 #[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 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
247pub struct ExecutionMetadata {
248 pub source: Option<String>,
250
251 pub conversation_id: Option<Uuid>,
253
254 #[serde(default)]
256 pub tags: HashMap<String, String>,
257}
258
259#[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#[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 #[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 #[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#[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}