ralph_workflow/checkpoint/
builder.rs1use crate::agents::AgentRegistry;
7use crate::checkpoint::execution_history::ExecutionHistory;
8use crate::checkpoint::file_state::FileSystemState;
9use crate::checkpoint::state::{
10 calculate_file_checksum_with_workspace, AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot,
11 PipelineCheckpoint, PipelinePhase, RebaseState,
12};
13use crate::checkpoint::RunContext;
14use crate::config::{Config, ReviewDepth};
15use crate::executor::ProcessExecutor;
16use crate::logger::Logger;
17use crate::reducer::state::PromptInputsState;
18use crate::workspace::Workspace;
19use std::sync::Arc;
20
21pub struct CheckpointBuilder {
36 phase: Option<PipelinePhase>,
37 iteration: u32,
38 total_iterations: u32,
39 reviewer_pass: u32,
40 total_reviewer_passes: u32,
41 developer_agent: Option<String>,
42 reviewer_agent: Option<String>,
43 cli_args: Option<CliArgsSnapshot>,
44 developer_agent_config: Option<AgentConfigSnapshot>,
45 reviewer_agent_config: Option<AgentConfigSnapshot>,
46 rebase_state: RebaseState,
47 config_path: Option<std::path::PathBuf>,
48 git_user_name: Option<String>,
49 git_user_email: Option<String>,
50 run_context: Option<RunContext>,
52 execution_history: Option<ExecutionHistory>,
54 prompt_history: Option<std::collections::HashMap<String, String>>,
55 prompt_inputs: Option<PromptInputsState>,
56 executor: Option<Arc<dyn ProcessExecutor>>,
58}
59
60impl Default for CheckpointBuilder {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl CheckpointBuilder {
67 pub fn new() -> Self {
69 Self {
70 phase: None,
71 iteration: 1,
72 total_iterations: 1,
73 reviewer_pass: 0,
74 total_reviewer_passes: 0,
75 developer_agent: None,
76 reviewer_agent: None,
77 cli_args: None,
78 developer_agent_config: None,
79 reviewer_agent_config: None,
80 rebase_state: RebaseState::default(),
81 config_path: None,
82 git_user_name: None,
83 git_user_email: None,
84 run_context: None,
85 execution_history: None,
86 prompt_history: None,
87 prompt_inputs: None,
88 executor: None,
89 }
90 }
91
92 pub fn phase(mut self, phase: PipelinePhase, iteration: u32, total_iterations: u32) -> Self {
94 self.phase = Some(phase);
95 self.iteration = iteration;
96 self.total_iterations = total_iterations;
97 self
98 }
99
100 pub fn reviewer_pass(mut self, pass: u32, total: u32) -> Self {
102 self.reviewer_pass = pass;
103 self.total_reviewer_passes = total;
104 self
105 }
106
107 pub fn agents(mut self, developer: &str, reviewer: &str) -> Self {
109 self.developer_agent = Some(developer.to_string());
110 self.reviewer_agent = Some(reviewer.to_string());
111 self
112 }
113
114 pub fn cli_args(mut self, args: CliArgsSnapshot) -> Self {
116 self.cli_args = Some(args);
117 self
118 }
119
120 pub fn developer_config(mut self, config: AgentConfigSnapshot) -> Self {
122 self.developer_agent_config = Some(config);
123 self
124 }
125
126 pub fn reviewer_config(mut self, config: AgentConfigSnapshot) -> Self {
128 self.reviewer_agent_config = Some(config);
129 self
130 }
131
132 pub fn rebase_state(mut self, state: RebaseState) -> Self {
134 self.rebase_state = state;
135 self
136 }
137
138 pub fn config_path(mut self, path: Option<std::path::PathBuf>) -> Self {
140 self.config_path = path;
141 self
142 }
143
144 pub fn git_identity(mut self, name: Option<&str>, email: Option<&str>) -> Self {
146 self.git_user_name = name.map(String::from);
147 self.git_user_email = email.map(String::from);
148 self
149 }
150
151 pub fn with_executor(mut self, executor: Arc<dyn ProcessExecutor>) -> Self {
153 self.executor = Some(executor);
154 self
155 }
156
157 pub fn capture_cli_args(mut self, config: &Config) -> Self {
159 let review_depth_str = review_depth_to_string(config.review_depth);
160 let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
161 config.developer_iters,
162 config.reviewer_reviews,
163 review_depth_str,
164 config.isolation_mode,
165 )
166 .verbosity(config.verbosity as u8)
167 .show_streaming_metrics(config.show_streaming_metrics)
168 .reviewer_json_parser(config.reviewer_json_parser.clone())
169 .build();
170 self.cli_args = Some(snapshot);
171 self
172 }
173
174 pub fn capture_from_context(
179 mut self,
180 config: &Config,
181 registry: &AgentRegistry,
182 developer_name: &str,
183 reviewer_name: &str,
184 logger: &Logger,
185 run_context: &RunContext,
186 ) -> Self {
187 self.run_context = Some(run_context.clone());
189
190 self = self.capture_cli_args(config);
192
193 if let Some(agent_config) = registry.resolve_config(developer_name) {
195 let snapshot = AgentConfigSnapshot::new(
196 developer_name.to_string(),
197 agent_config.cmd.clone(),
198 agent_config.output_flag.clone(),
199 Some(agent_config.yolo_flag.clone()),
200 agent_config.can_commit,
201 )
202 .with_model_override(config.developer_model.clone())
203 .with_provider_override(config.developer_provider.clone())
204 .with_context_level(config.developer_context);
205 self.developer_agent_config = Some(snapshot);
206 self.developer_agent = Some(developer_name.to_string());
207 } else {
208 logger.warn(&format!(
209 "Developer agent '{}' not found in registry",
210 developer_name
211 ));
212 }
213
214 if let Some(agent_config) = registry.resolve_config(reviewer_name) {
216 let snapshot = AgentConfigSnapshot::new(
217 reviewer_name.to_string(),
218 agent_config.cmd.clone(),
219 agent_config.output_flag.clone(),
220 Some(agent_config.yolo_flag.clone()),
221 agent_config.can_commit,
222 )
223 .with_model_override(config.reviewer_model.clone())
224 .with_provider_override(config.reviewer_provider.clone())
225 .with_context_level(config.reviewer_context);
226 self.reviewer_agent_config = Some(snapshot);
227 self.reviewer_agent = Some(reviewer_name.to_string());
228 } else {
229 logger.warn(&format!(
230 "Reviewer agent '{}' not found in registry",
231 reviewer_name
232 ));
233 }
234
235 self.git_user_name = config.git_user_name.clone();
237 self.git_user_email = config.git_user_email.clone();
238
239 self
240 }
241
242 pub fn with_executor_from_context(mut self, executor_arc: Arc<dyn ProcessExecutor>) -> Self {
247 self.executor = Some(executor_arc);
248 self
249 }
250
251 pub fn with_execution_history(mut self, history: ExecutionHistory) -> Self {
256 self.execution_history = Some(history);
257 self
258 }
259
260 pub fn with_prompt_history(
268 mut self,
269 history: std::collections::HashMap<String, String>,
270 ) -> Self {
271 self.prompt_history = if history.is_empty() {
272 None
273 } else {
274 Some(history)
275 };
276 self
277 }
278
279 pub fn with_prompt_inputs(mut self, prompt_inputs: PromptInputsState) -> Self {
285 let is_empty = prompt_inputs.planning.is_none()
286 && prompt_inputs.development.is_none()
287 && prompt_inputs.review.is_none()
288 && prompt_inputs.commit.is_none()
289 && prompt_inputs.xsd_retry_last_output.is_none();
290 self.prompt_inputs = if is_empty { None } else { Some(prompt_inputs) };
291 self
292 }
293
294 pub fn build(self) -> Option<PipelineCheckpoint> {
302 self.build_internal(None)
303 }
304
305 pub fn build_with_workspace(self, workspace: &dyn Workspace) -> Option<PipelineCheckpoint> {
315 self.build_internal(Some(workspace))
316 }
317
318 fn build_internal(self, workspace: Option<&dyn Workspace>) -> Option<PipelineCheckpoint> {
320 let phase = self.phase?;
321 let developer_agent = self.developer_agent?;
322 let reviewer_agent = self.reviewer_agent?;
323 let cli_args = self.cli_args?;
324 let developer_config = self.developer_agent_config?;
325 let reviewer_config = self.reviewer_agent_config?;
326
327 let git_user_name = self.git_user_name.as_deref();
328 let git_user_email = self.git_user_email.as_deref();
329
330 let run_context = self.run_context.unwrap_or_default();
332
333 let working_dir = workspace
334 .map(|ws| ws.root().to_string_lossy().to_string())
335 .or_else(|| {
336 std::env::current_dir()
337 .ok()
338 .map(|p| p.to_string_lossy().to_string())
339 })
340 .unwrap_or_default();
341
342 let prompt_md_checksum = workspace.and_then(|ws| {
343 calculate_file_checksum_with_workspace(ws, std::path::Path::new("PROMPT.md"))
344 });
345
346 let (config_path, config_checksum) = if let Some(path) = self.config_path {
347 let path_string = path.to_string_lossy().to_string();
348 let checksum = workspace.and_then(|ws| {
349 let relative = path.strip_prefix(ws.root()).ok().unwrap_or(&path);
350 calculate_file_checksum_with_workspace(ws, relative)
351 });
352 (Some(path_string), checksum)
353 } else {
354 (None, None)
355 };
356
357 let mut checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
358 phase,
359 iteration: self.iteration,
360 total_iterations: self.total_iterations,
361 reviewer_pass: self.reviewer_pass,
362 total_reviewer_passes: self.total_reviewer_passes,
363 developer_agent: &developer_agent,
364 reviewer_agent: &reviewer_agent,
365 cli_args,
366 developer_agent_config: developer_config,
367 reviewer_agent_config: reviewer_config,
368 rebase_state: self.rebase_state,
369 git_user_name,
370 git_user_email,
371 run_id: &run_context.run_id,
372 parent_run_id: run_context.parent_run_id.as_deref(),
373 resume_count: run_context.resume_count,
374 actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
375 actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
376 working_dir,
377 prompt_md_checksum,
378 config_path,
379 config_checksum,
380 });
381
382 checkpoint.execution_history = self.execution_history;
384
385 checkpoint.prompt_history = self.prompt_history;
387
388 checkpoint.prompt_inputs = self.prompt_inputs;
390
391 let executor_ref = self.executor.as_ref().map(|e| e.as_ref());
395 checkpoint.file_system_state = if let Some(ws) = workspace {
396 executor_ref.map(|executor| FileSystemState::capture_with_workspace(ws, executor))
397 } else {
398 Some(FileSystemState::capture_with_optional_executor_impl(
399 executor_ref,
400 ))
401 };
402
403 checkpoint.env_snapshot =
405 Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current());
406
407 Some(checkpoint)
408 }
409}
410
411fn review_depth_to_string(depth: ReviewDepth) -> Option<String> {
413 match depth {
414 ReviewDepth::Standard => Some("standard".to_string()),
415 ReviewDepth::Comprehensive => Some("comprehensive".to_string()),
416 ReviewDepth::Security => Some("security".to_string()),
417 ReviewDepth::Incremental => Some("incremental".to_string()),
418 }
419}
420
421#[cfg(test)]
422mod tests;