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 + 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    pub const fn record_developer_iteration(&mut self) {
74        self.actual_developer_runs += 1;
75    }
76
77    /// Record a completed reviewer pass.
78    pub const fn record_reviewer_pass(&mut self) {
79        self.actual_reviewer_runs += 1;
80    }
81}
82
83impl Default for RunContext {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::checkpoint::state::{
93        AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, PipelineCheckpoint, PipelinePhase,
94        RebaseState,
95    };
96
97    #[test]
98    fn test_run_context_new() {
99        let ctx = RunContext::new();
100        assert!(!ctx.run_id.is_empty());
101        assert!(ctx.parent_run_id.is_none());
102        assert_eq!(ctx.resume_count, 0);
103        assert_eq!(ctx.actual_developer_runs, 0);
104        assert_eq!(ctx.actual_reviewer_runs, 0);
105    }
106
107    #[test]
108    fn test_run_context_from_checkpoint() {
109        // Create a mock checkpoint
110        let cli_args = CliArgsSnapshot::new(5, 2, None, true, 2, false, None);
111        let dev_config =
112            AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
113        let rev_config =
114            AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
115
116        let original_run_id = uuid::Uuid::new_v4().to_string();
117        let checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
118            phase: PipelinePhase::Development,
119            iteration: 2,
120            total_iterations: 5,
121            reviewer_pass: 0,
122            total_reviewer_passes: 2,
123            developer_agent: "claude",
124            reviewer_agent: "codex",
125            cli_args,
126            developer_agent_config: dev_config,
127            reviewer_agent_config: rev_config,
128            rebase_state: RebaseState::default(),
129            git_user_name: None,
130            git_user_email: None,
131            run_id: &original_run_id,
132            parent_run_id: None,
133            resume_count: 1,
134            actual_developer_runs: 2,
135            actual_reviewer_runs: 0,
136            working_dir: "/test/repo".to_string(),
137            prompt_md_checksum: None,
138            config_path: None,
139            config_checksum: None,
140        });
141
142        let run_ctx = RunContext::from_checkpoint(&checkpoint);
143
144        assert_ne!(
145            run_ctx.run_id, original_run_id,
146            "new run_id should be generated"
147        );
148        assert_eq!(run_ctx.parent_run_id, Some(original_run_id));
149        assert_eq!(run_ctx.resume_count, 2, "resume_count should increment");
150        assert_eq!(run_ctx.actual_developer_runs, 2);
151        assert_eq!(run_ctx.actual_reviewer_runs, 0);
152    }
153
154    #[test]
155    fn test_run_context_with_developer_runs() {
156        let ctx = RunContext::new().with_developer_runs(5);
157        assert_eq!(ctx.actual_developer_runs, 5);
158    }
159
160    #[test]
161    fn test_run_context_with_reviewer_runs() {
162        let ctx = RunContext::new().with_reviewer_runs(3);
163        assert_eq!(ctx.actual_reviewer_runs, 3);
164    }
165}