Skip to main content

ralph_workflow/checkpoint/
restore.rs

1//! State restoration from checkpoints.
2//!
3//! This module provides functionality to restore pipeline state from a checkpoint,
4//! including CLI arguments and configuration overrides.
5
6use crate::checkpoint::execution_history::ExecutionHistory;
7use crate::checkpoint::state::{PipelineCheckpoint, PipelinePhase, RebaseState};
8use crate::config::Config;
9
10/// Rich context about a resumed session for use in agent prompts.
11///
12/// This struct contains information that helps AI agents understand where
13/// they are in the pipeline when resuming from a checkpoint, enabling them
14/// to provide more contextual and appropriate responses.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ResumeContext {
17    /// The phase being resumed from
18    pub phase: PipelinePhase,
19    /// Current iteration number (for development)
20    pub iteration: u32,
21    /// Total iterations
22    pub total_iterations: u32,
23    /// Current reviewer pass
24    pub reviewer_pass: u32,
25    /// Total reviewer passes
26    pub total_reviewer_passes: u32,
27    /// Number of times this session has been resumed
28    pub resume_count: u32,
29    /// Rebase state if applicable
30    pub rebase_state: RebaseState,
31    /// Run ID for tracing
32    pub run_id: String,
33    /// Captured prompts from the original run for deterministic replay
34    pub prompt_history:
35        Option<std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>>,
36    /// Execution history from the checkpoint (if available)
37    pub execution_history: Option<ExecutionHistory>,
38}
39
40impl ResumeContext {
41    /// Display name for the current phase.
42    #[must_use]
43    pub fn phase_name(&self) -> String {
44        match self.phase {
45            PipelinePhase::Rebase => "Rebase".to_string(),
46            PipelinePhase::Planning => "Planning".to_string(),
47            PipelinePhase::Development => format!(
48                "Development iteration {}/{}",
49                self.iteration, self.total_iterations
50            ),
51            PipelinePhase::Review => format!(
52                "Review (pass {}/{})",
53                self.reviewer_pass, self.total_reviewer_passes
54            ),
55            PipelinePhase::CommitMessage => "Commit Message Generation".to_string(),
56            PipelinePhase::FinalValidation => "Final Validation".to_string(),
57            PipelinePhase::Complete => "Complete".to_string(),
58            PipelinePhase::PreRebase => "Pre-Rebase".to_string(),
59            PipelinePhase::PreRebaseConflict => "Pre-Rebase Conflict".to_string(),
60            PipelinePhase::PostRebase => "Post-Rebase".to_string(),
61            PipelinePhase::PostRebaseConflict => "Post-Rebase Conflict".to_string(),
62            PipelinePhase::AwaitingDevFix => "Awaiting Dev Fix".to_string(),
63            PipelinePhase::Interrupted => "Interrupted".to_string(),
64        }
65    }
66}
67
68impl PipelineCheckpoint {
69    /// Extract rich resume context from this checkpoint.
70    ///
71    /// This method creates a `ResumeContext` containing all the information
72    /// needed to generate informative prompts for agents when resuming.
73    #[must_use]
74    pub fn resume_context(&self) -> ResumeContext {
75        ResumeContext {
76            phase: self.phase,
77            iteration: self.iteration,
78            total_iterations: self.total_iterations,
79            reviewer_pass: self.reviewer_pass,
80            total_reviewer_passes: self.total_reviewer_passes,
81            resume_count: self.resume_count,
82            rebase_state: self.rebase_state.clone(),
83            run_id: self.run_id.clone(),
84            prompt_history: self.prompt_history.clone(),
85            execution_history: self.execution_history.clone(),
86        }
87    }
88}
89
90/// Apply checkpoint values to the current config for deterministic resume.
91pub fn apply_checkpoint_to_config(config: &mut Config, checkpoint: &PipelineCheckpoint) {
92    let cli_args = &checkpoint.cli_args;
93
94    // Always restore developer_iters and reviewer_reviews from checkpoint
95    // to ensure exact state restoration, even if zero
96    config.developer_iters = cli_args.developer_iters;
97    config.reviewer_reviews = cli_args.reviewer_reviews;
98
99    // Note: review_depth is stored as a string in the checkpoint
100    // but as an enum in Config. For now, we don't override it.
101    // This could be enhanced to parse the string back to an enum.
102
103    // Apply model overrides if they exist in the checkpoint
104    if let Some(ref model) = checkpoint.developer_agent_config.model_override {
105        config.developer_model = Some(model.clone());
106    }
107    if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
108        config.reviewer_model = Some(model.clone());
109    }
110
111    // Apply provider overrides if they exist in the checkpoint
112    if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
113        config.developer_provider = Some(provider.clone());
114    }
115    if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
116        config.reviewer_provider = Some(provider.clone());
117    }
118
119    // Apply context levels if they exist in the checkpoint
120    config.developer_context = checkpoint.developer_agent_config.context_level;
121    config.reviewer_context = checkpoint.reviewer_agent_config.context_level;
122
123    // Apply git identity if it exists in the checkpoint
124    if let Some(ref name) = checkpoint.git_user_name {
125        config.git_user_name = Some(name.clone());
126    }
127    if let Some(ref email) = checkpoint.git_user_email {
128        config.git_user_email = Some(email.clone());
129    }
130
131    // Always restore isolation_mode from checkpoint for exact state restoration
132    config.isolation_mode = cli_args.isolation_mode;
133
134    // Apply verbosity level from checkpoint
135    config.verbosity = crate::config::types::Verbosity::from(cli_args.verbosity);
136
137    // Apply show_streaming_metrics from checkpoint
138    config.show_streaming_metrics = cli_args.show_streaming_metrics;
139
140    // Apply reviewer_json_parser from checkpoint if it exists
141    if let Some(ref parser) = cli_args.reviewer_json_parser {
142        config.reviewer_json_parser = Some(parser.clone());
143    }
144}
145
146/// Restore environment variables from a checkpoint.
147///
148/// This function is delegated to the runtime boundary module.
149pub use crate::checkpoint::environment::restore_environment_from_checkpoint;
150
151/// Inner implementation for restoring environment variables.
152/// This function is delegated to the runtime boundary module.
153pub use crate::checkpoint::environment::restore_environment_impl;
154
155/// Calculate the starting iteration for development phase resume.
156#[must_use]
157pub fn calculate_start_iteration(checkpoint: &PipelineCheckpoint, max_iterations: u32) -> u32 {
158    match checkpoint.phase {
159        PipelinePhase::Planning | PipelinePhase::Development => {
160            checkpoint.iteration.clamp(1, max_iterations)
161        }
162        // For later phases, development is already complete
163        _ => max_iterations,
164    }
165}
166
167/// Calculate the starting reviewer pass for review phase resume.
168///
169/// When resuming from a checkpoint in the review phase, this determines
170/// which pass to start from based on the checkpoint state.
171///
172/// # Arguments
173///
174/// * `checkpoint` - The checkpoint to calculate from
175/// * `max_passes` - Maximum review passes configured
176///
177/// # Returns
178///
179/// The pass number to start from (1-indexed).
180#[must_use]
181pub fn calculate_start_reviewer_pass(checkpoint: &PipelineCheckpoint, max_passes: u32) -> u32 {
182    match checkpoint.phase {
183        PipelinePhase::Review => checkpoint.reviewer_pass.clamp(1, max_passes.max(1)),
184        // For earlier phases, start from the beginning
185        PipelinePhase::Planning
186        | PipelinePhase::Development
187        | PipelinePhase::PreRebase
188        | PipelinePhase::PreRebaseConflict => 1,
189        // For later phases, review is already complete
190        _ => max_passes,
191    }
192}
193
194/// Determine if a phase should be skipped based on checkpoint.
195///
196/// Returns true if the checkpoint indicates this phase has already been completed.
197#[must_use]
198pub const fn should_skip_phase(phase: PipelinePhase, checkpoint: &PipelineCheckpoint) -> bool {
199    phase_rank(phase) < phase_rank(checkpoint.phase)
200}
201
202/// Get the rank (position) of a phase in the pipeline.
203///
204/// Lower values indicate earlier phases in the pipeline.
205const fn phase_rank(phase: PipelinePhase) -> u32 {
206    match phase {
207        PipelinePhase::Planning => 0,
208        PipelinePhase::Development => 1,
209        PipelinePhase::CommitMessage => 3,
210        PipelinePhase::FinalValidation => 4,
211        PipelinePhase::Complete => 5,
212        PipelinePhase::AwaitingDevFix => 6,
213        PipelinePhase::Interrupted => 7,
214        // Review and rebase phases all map to rank 2
215        PipelinePhase::Review
216        | PipelinePhase::PreRebase
217        | PipelinePhase::PreRebaseConflict
218        | PipelinePhase::Rebase
219        | PipelinePhase::PostRebase
220        | PipelinePhase::PostRebaseConflict => 2,
221    }
222}
223
224#[cfg(test)]
225#[derive(Debug, Clone)]
226pub struct RestoredContext {
227    pub phase: PipelinePhase,
228    pub resume_iteration: u32,
229    pub total_iterations: u32,
230    pub resume_reviewer_pass: u32,
231    pub total_reviewer_passes: u32,
232    pub developer_agent: String,
233    pub reviewer_agent: String,
234    pub cli_args: Option<crate::checkpoint::state::CliArgsSnapshot>,
235}
236
237#[cfg(test)]
238impl RestoredContext {
239    /// Create a restored context from a checkpoint.
240    #[must_use]
241    pub fn from_checkpoint(checkpoint: &PipelineCheckpoint) -> Self {
242        // Determine if CLI args are meaningful (non-default values)
243        let cli_args = if checkpoint.cli_args.developer_iters > 0
244            || checkpoint.cli_args.reviewer_reviews > 0
245        {
246            Some(checkpoint.cli_args.clone())
247        } else {
248            None
249        };
250
251        Self {
252            phase: checkpoint.phase,
253            resume_iteration: checkpoint.iteration,
254            total_iterations: checkpoint.total_iterations,
255            resume_reviewer_pass: checkpoint.reviewer_pass,
256            total_reviewer_passes: checkpoint.total_reviewer_passes,
257            developer_agent: checkpoint.developer_agent.clone(),
258            reviewer_agent: checkpoint.reviewer_agent.clone(),
259            cli_args,
260        }
261    }
262
263    /// Check if we should use checkpoint values for iteration counts.
264    ///
265    /// Returns true if the checkpoint has meaningful CLI args that should
266    /// override the current configuration.
267    #[must_use]
268    pub fn should_use_checkpoint_iterations(&self) -> bool {
269        self.cli_args
270            .as_ref()
271            .is_some_and(|args| args.developer_iters > 0)
272    }
273
274    /// Check if we should use checkpoint values for reviewer counts.
275    #[must_use]
276    pub fn should_use_checkpoint_reviewer_passes(&self) -> bool {
277        self.cli_args
278            .as_ref()
279            .is_some_and(|args| args.reviewer_reviews > 0)
280    }
281}
282
283#[cfg(test)]
284mod tests;