Skip to main content

ralph_workflow/checkpoint/
run_context.rs

1//! Run context for tracking pipeline execution lineage and state.
2//!
3//! This module provides the `RunContext` struct which tracks information
4//! about a pipeline run including its unique identifier, parent run (if resumed),
5//! and actual execution counts (separate from configured counts).
6
7use serde::{Deserialize, Serialize};
8
9/// Context for tracking pipeline execution lineage and state.
10///
11/// This tracks information about the current pipeline run that is separate
12/// from the configured parameters. It enables:
13/// - Unique identification of each run (`run_id`)
14/// - Tracking resume lineage (`parent_run_id`)
15/// - Counting how many times a session has been resumed (`resume_count`)
16/// - Tracking actual completed iterations vs configured iterations
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct RunContext {
19    /// Unique identifier for this run (UUID v4)
20    pub run_id: String,
21    /// Parent run ID if this is a resumed session
22    pub parent_run_id: Option<String>,
23    /// Number of times this session has been resumed
24    pub resume_count: u32,
25    /// Actual number of developer iterations that have completed
26    pub actual_developer_runs: u32,
27    /// Actual number of reviewer passes that have completed
28    pub actual_reviewer_runs: u32,
29}
30
31impl RunContext {
32    /// Create a new `RunContext` for a fresh run.
33    #[must_use]
34    pub fn new() -> Self {
35        Self {
36            run_id: uuid::Uuid::new_v4().to_string(),
37            parent_run_id: None,
38            resume_count: 0,
39            actual_developer_runs: 0,
40            actual_reviewer_runs: 0,
41        }
42    }
43
44    /// Create a `RunContext` from a checkpoint (for resume scenarios).
45    #[must_use]
46    pub fn from_checkpoint(checkpoint: &super::PipelineCheckpoint) -> Self {
47        Self {
48            run_id: uuid::Uuid::new_v4().to_string(), // New run_id for resumed run
49            parent_run_id: Some(checkpoint.run_id.clone()),
50            resume_count: checkpoint.resume_count.saturating_add(1),
51            actual_developer_runs: checkpoint.actual_developer_runs,
52            actual_reviewer_runs: checkpoint.actual_reviewer_runs,
53        }
54    }
55
56    /// Update the actual developer runs count.
57    #[cfg(test)]
58    #[must_use]
59    pub const fn with_developer_runs(mut self, runs: u32) -> Self {
60        self.actual_developer_runs = runs;
61        self
62    }
63
64    /// Update the actual reviewer runs count.
65    #[cfg(test)]
66    #[must_use]
67    pub const fn with_reviewer_runs(mut self, runs: u32) -> Self {
68        self.actual_reviewer_runs = runs;
69        self
70    }
71
72    /// Record a completed developer iteration.
73    #[must_use]
74    pub const fn record_developer_iteration(mut self) -> Self {
75        self.actual_developer_runs = self.actual_developer_runs.saturating_add(1);
76        self
77    }
78
79    /// Record a completed reviewer pass.
80    #[must_use]
81    pub const fn record_reviewer_pass(mut self) -> Self {
82        self.actual_reviewer_runs = self.actual_reviewer_runs.saturating_add(1);
83        self
84    }
85}
86
87impl Default for RunContext {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::checkpoint::state::{
97        AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, PipelineCheckpoint, PipelinePhase,
98        RebaseState,
99    };
100
101    #[test]
102    fn test_run_context_new() {
103        let ctx = RunContext::new();
104        assert!(!ctx.run_id.is_empty());
105        assert!(ctx.parent_run_id.is_none());
106        assert_eq!(ctx.resume_count, 0);
107        assert_eq!(ctx.actual_developer_runs, 0);
108        assert_eq!(ctx.actual_reviewer_runs, 0);
109    }
110
111    #[test]
112    fn test_run_context_from_checkpoint() {
113        // Create a mock checkpoint
114        let cli_args = CliArgsSnapshot::new(5, 2, None, true, 2, false, None);
115        let dev_config =
116            AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
117        let rev_config =
118            AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
119
120        let original_run_id = uuid::Uuid::new_v4().to_string();
121        let checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
122            phase: PipelinePhase::Development,
123            iteration: 2,
124            total_iterations: 5,
125            reviewer_pass: 0,
126            total_reviewer_passes: 2,
127            developer_agent: "claude",
128            reviewer_agent: "codex",
129            cli_args,
130            developer_agent_config: dev_config,
131            reviewer_agent_config: rev_config,
132            rebase_state: RebaseState::default(),
133            git_user_name: None,
134            git_user_email: None,
135            run_id: &original_run_id,
136            parent_run_id: None,
137            resume_count: 1,
138            actual_developer_runs: 2,
139            actual_reviewer_runs: 0,
140            working_dir: "/test/repo".to_string(),
141            prompt_md_checksum: None,
142            config_path: None,
143            config_checksum: None,
144        });
145
146        let run_ctx = RunContext::from_checkpoint(&checkpoint);
147
148        assert_ne!(
149            run_ctx.run_id, original_run_id,
150            "new run_id should be generated"
151        );
152        assert_eq!(run_ctx.parent_run_id, Some(original_run_id));
153        assert_eq!(run_ctx.resume_count, 2, "resume_count should increment");
154        assert_eq!(run_ctx.actual_developer_runs, 2);
155        assert_eq!(run_ctx.actual_reviewer_runs, 0);
156    }
157
158    #[test]
159    fn test_run_context_with_developer_runs() {
160        let ctx = RunContext::new().with_developer_runs(5);
161        assert_eq!(ctx.actual_developer_runs, 5);
162    }
163
164    #[test]
165    fn test_run_context_with_reviewer_runs() {
166        let ctx = RunContext::new().with_reviewer_runs(3);
167        assert_eq!(ctx.actual_reviewer_runs, 3);
168    }
169}