Skip to main content

ralph_workflow/pipeline/
mod.rs

1//! Pipeline Execution Module
2//!
3//! This module contains the core pipeline execution infrastructure for running
4//! AI agents with real-time output streaming.
5//!
6//! # Key Types
7//!
8//! - [`AgentPhaseGuard`] - RAII guard for phase cleanup on success/failure
9//! - [`Timer`] - Execution duration tracking with phase support
10//! - [`PipelineRuntime`] - Runtime context for agent execution
11//!
12//! # Features
13//!
14//! - **Single-attempt execution** - One agent invocation per effect
15//! - **Real-time streaming** - Live output from agents during execution
16//! - **Log management** - Structured logging to `.agent/logs/`
17//!
18//! # Module Structure
19//!
20//! - `types` - Pipeline statistics tracking and RAII guards
21//! - [`logfile`] - Unified log file path creation, parsing, and discovery
22//! - [`idle_timeout`] - Timeout handling for stuck agents
23
24#![deny(unsafe_code)]
25
26mod clipboard;
27pub mod idle_timeout;
28pub mod logfile;
29mod prompt;
30mod types;
31
32pub use prompt::{
33    extract_error_message_from_logfile, run_with_prompt, PipelineRuntime, PromptCommand,
34};
35pub use types::AgentPhaseGuard;
36
37// ===== Timer Utilities =====
38
39use std::time::{Duration, Instant};
40
41/// Timer for tracking execution duration
42#[derive(Clone)]
43pub struct Timer {
44    start_time: Instant,
45    phase_start: Instant,
46}
47
48impl Timer {
49    /// Create a new timer, starting now
50    pub fn new() -> Self {
51        let now = Instant::now();
52        Self {
53            start_time: now,
54            phase_start: now,
55        }
56    }
57
58    /// Start a new phase timer
59    pub fn start_phase(&mut self) {
60        self.phase_start = Instant::now();
61    }
62
63    /// Get elapsed time since timer start
64    pub fn elapsed(&self) -> Duration {
65        self.start_time.elapsed()
66    }
67
68    /// Get elapsed time since phase start
69    pub fn phase_elapsed(&self) -> Duration {
70        self.phase_start.elapsed()
71    }
72
73    /// Format a duration as "Xm YYs"
74    pub fn format_duration(duration: Duration) -> String {
75        let total_secs = duration.as_secs();
76        let mins = total_secs / 60;
77        let secs = total_secs % 60;
78        format!("{mins}m {secs:02}s")
79    }
80
81    /// Get formatted elapsed time since start
82    pub fn elapsed_formatted(&self) -> String {
83        Self::format_duration(self.elapsed())
84    }
85
86    /// Get formatted elapsed time since phase start
87    pub fn phase_elapsed_formatted(&self) -> String {
88        Self::format_duration(self.phase_elapsed())
89    }
90}
91
92impl Default for Timer {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98#[cfg(test)]
99mod timer_tests {
100    use super::*;
101    use std::thread;
102
103    #[test]
104    fn test_format_duration_zero() {
105        let d = Duration::from_secs(0);
106        assert_eq!(Timer::format_duration(d), "0m 00s");
107    }
108
109    #[test]
110    fn test_format_duration_seconds() {
111        let d = Duration::from_secs(30);
112        assert_eq!(Timer::format_duration(d), "0m 30s");
113    }
114
115    #[test]
116    fn test_format_duration_minutes() {
117        let d = Duration::from_secs(65);
118        assert_eq!(Timer::format_duration(d), "1m 05s");
119    }
120
121    #[test]
122    fn test_format_duration_large() {
123        let d = Duration::from_secs(3661);
124        assert_eq!(Timer::format_duration(d), "61m 01s");
125    }
126
127    #[test]
128    fn test_timer_elapsed() {
129        let timer = Timer::new();
130        thread::sleep(Duration::from_millis(10));
131        assert!(timer.elapsed() >= Duration::from_millis(10));
132    }
133
134    #[test]
135    fn test_timer_phase() {
136        let mut timer = Timer::new();
137        thread::sleep(Duration::from_millis(10));
138        timer.start_phase();
139        thread::sleep(Duration::from_millis(10));
140        // Phase elapsed should be less than total elapsed
141        assert!(timer.phase_elapsed() < timer.elapsed());
142    }
143}