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, PromptPermissionsState};
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 prompt_permissions: PromptPermissionsState,
57 last_substitution_log: Option<crate::prompts::SubstitutionLog>,
58 executor: Option<Arc<dyn ProcessExecutor>>,
60 log_run_id: Option<String>,
62}
63
64impl Default for CheckpointBuilder {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl CheckpointBuilder {
71 pub fn new() -> Self {
73 Self {
74 phase: None,
75 iteration: 1,
76 total_iterations: 1,
77 reviewer_pass: 0,
78 total_reviewer_passes: 0,
79 developer_agent: None,
80 reviewer_agent: None,
81 cli_args: None,
82 developer_agent_config: None,
83 reviewer_agent_config: None,
84 rebase_state: RebaseState::default(),
85 config_path: None,
86 git_user_name: None,
87 git_user_email: None,
88 run_context: None,
89 execution_history: None,
90 prompt_history: None,
91 prompt_inputs: None,
92 prompt_permissions: PromptPermissionsState::default(),
93 last_substitution_log: None,
94 executor: None,
95 log_run_id: None,
96 }
97 }
98
99 pub fn phase(mut self, phase: PipelinePhase, iteration: u32, total_iterations: u32) -> Self {
101 self.phase = Some(phase);
102 self.iteration = iteration;
103 self.total_iterations = total_iterations;
104 self
105 }
106
107 pub fn reviewer_pass(mut self, pass: u32, total: u32) -> Self {
109 self.reviewer_pass = pass;
110 self.total_reviewer_passes = total;
111 self
112 }
113
114 pub fn agents(mut self, developer: &str, reviewer: &str) -> Self {
116 self.developer_agent = Some(developer.to_string());
117 self.reviewer_agent = Some(reviewer.to_string());
118 self
119 }
120
121 pub fn cli_args(mut self, args: CliArgsSnapshot) -> Self {
123 self.cli_args = Some(args);
124 self
125 }
126
127 pub fn with_last_substitution_log(
129 mut self,
130 log: Option<crate::prompts::SubstitutionLog>,
131 ) -> Self {
132 self.last_substitution_log = log;
133 self
134 }
135
136 pub fn developer_config(mut self, config: AgentConfigSnapshot) -> Self {
138 self.developer_agent_config = Some(config);
139 self
140 }
141
142 pub fn reviewer_config(mut self, config: AgentConfigSnapshot) -> Self {
144 self.reviewer_agent_config = Some(config);
145 self
146 }
147
148 pub fn rebase_state(mut self, state: RebaseState) -> Self {
150 self.rebase_state = state;
151 self
152 }
153
154 pub fn config_path(mut self, path: Option<std::path::PathBuf>) -> Self {
156 self.config_path = path;
157 self
158 }
159
160 pub fn git_identity(mut self, name: Option<&str>, email: Option<&str>) -> Self {
162 self.git_user_name = name.map(String::from);
163 self.git_user_email = email.map(String::from);
164 self
165 }
166
167 pub fn with_executor(mut self, executor: Arc<dyn ProcessExecutor>) -> Self {
169 self.executor = Some(executor);
170 self
171 }
172
173 pub fn capture_cli_args(mut self, config: &Config) -> Self {
175 let review_depth_str = review_depth_to_string(config.review_depth);
176 let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
177 config.developer_iters,
178 config.reviewer_reviews,
179 review_depth_str,
180 config.isolation_mode,
181 )
182 .verbosity(config.verbosity as u8)
183 .show_streaming_metrics(config.show_streaming_metrics)
184 .reviewer_json_parser(config.reviewer_json_parser.clone())
185 .build();
186 self.cli_args = Some(snapshot);
187 self
188 }
189
190 pub fn capture_from_context(
195 mut self,
196 config: &Config,
197 registry: &AgentRegistry,
198 developer_name: &str,
199 reviewer_name: &str,
200 logger: &Logger,
201 run_context: &RunContext,
202 ) -> Self {
203 self.run_context = Some(run_context.clone());
205
206 self = self.capture_cli_args(config);
208
209 if let Some(agent_config) = registry.resolve_config(developer_name) {
211 let snapshot = AgentConfigSnapshot::new(
212 developer_name.to_string(),
213 agent_config.cmd.clone(),
214 agent_config.output_flag.clone(),
215 Some(agent_config.yolo_flag.clone()),
216 agent_config.can_commit,
217 )
218 .with_model_override(config.developer_model.clone())
219 .with_provider_override(config.developer_provider.clone())
220 .with_context_level(config.developer_context);
221 self.developer_agent_config = Some(snapshot);
222 self.developer_agent = Some(developer_name.to_string());
223 } else {
224 logger.warn(&format!(
225 "Developer agent '{}' not found in registry",
226 developer_name
227 ));
228 }
229
230 if let Some(agent_config) = registry.resolve_config(reviewer_name) {
232 let snapshot = AgentConfigSnapshot::new(
233 reviewer_name.to_string(),
234 agent_config.cmd.clone(),
235 agent_config.output_flag.clone(),
236 Some(agent_config.yolo_flag.clone()),
237 agent_config.can_commit,
238 )
239 .with_model_override(config.reviewer_model.clone())
240 .with_provider_override(config.reviewer_provider.clone())
241 .with_context_level(config.reviewer_context);
242 self.reviewer_agent_config = Some(snapshot);
243 self.reviewer_agent = Some(reviewer_name.to_string());
244 } else {
245 logger.warn(&format!(
246 "Reviewer agent '{}' not found in registry",
247 reviewer_name
248 ));
249 }
250
251 self.git_user_name = config.git_user_name.clone();
253 self.git_user_email = config.git_user_email.clone();
254
255 self
256 }
257
258 pub fn with_executor_from_context(mut self, executor_arc: Arc<dyn ProcessExecutor>) -> Self {
263 self.executor = Some(executor_arc);
264 self
265 }
266
267 pub fn with_execution_history(mut self, history: ExecutionHistory) -> Self {
272 self.execution_history = Some(history);
273 self
274 }
275
276 pub fn with_prompt_history(
284 mut self,
285 history: std::collections::HashMap<String, String>,
286 ) -> Self {
287 self.prompt_history = if history.is_empty() {
288 None
289 } else {
290 Some(history)
291 };
292 self
293 }
294
295 pub fn with_prompt_inputs(mut self, prompt_inputs: PromptInputsState) -> Self {
301 let is_empty = prompt_inputs.planning.is_none()
302 && prompt_inputs.development.is_none()
303 && prompt_inputs.review.is_none()
304 && prompt_inputs.commit.is_none()
305 && prompt_inputs.xsd_retry_last_output.is_none();
306 self.prompt_inputs = if is_empty { None } else { Some(prompt_inputs) };
307 self
308 }
309
310 pub fn with_prompt_permissions(mut self, prompt_permissions: PromptPermissionsState) -> Self {
312 self.prompt_permissions = prompt_permissions;
313 self
314 }
315
316 pub fn with_log_run_id(mut self, log_run_id: String) -> Self {
321 self.log_run_id = Some(log_run_id);
322 self
323 }
324
325 pub fn build(self) -> Option<PipelineCheckpoint> {
333 self.build_internal(None)
334 }
335
336 pub fn build_with_workspace(self, workspace: &dyn Workspace) -> Option<PipelineCheckpoint> {
346 self.build_internal(Some(workspace))
347 }
348
349 fn build_internal(self, workspace: Option<&dyn Workspace>) -> Option<PipelineCheckpoint> {
351 let phase = self.phase?;
352 let developer_agent = self.developer_agent?;
353 let reviewer_agent = self.reviewer_agent?;
354 let cli_args = self.cli_args?;
355 let developer_config = self.developer_agent_config?;
356 let reviewer_config = self.reviewer_agent_config?;
357
358 let git_user_name = self.git_user_name.as_deref();
359 let git_user_email = self.git_user_email.as_deref();
360
361 let run_context = self.run_context.unwrap_or_default();
363
364 let working_dir = workspace
365 .map(|ws| ws.root().to_string_lossy().to_string())
366 .or_else(|| {
367 std::env::current_dir()
368 .ok()
369 .map(|p| p.to_string_lossy().to_string())
370 })
371 .unwrap_or_default();
372
373 let prompt_md_checksum = workspace.and_then(|ws| {
374 calculate_file_checksum_with_workspace(ws, std::path::Path::new("PROMPT.md"))
375 });
376
377 let (config_path, config_checksum) = if let Some(path) = self.config_path {
378 let path_string = path.to_string_lossy().to_string();
379 let checksum = workspace.and_then(|ws| {
380 let relative = path.strip_prefix(ws.root()).ok().unwrap_or(&path);
381 calculate_file_checksum_with_workspace(ws, relative)
382 });
383 (Some(path_string), checksum)
384 } else {
385 (None, None)
386 };
387
388 let mut checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
389 phase,
390 iteration: self.iteration,
391 total_iterations: self.total_iterations,
392 reviewer_pass: self.reviewer_pass,
393 total_reviewer_passes: self.total_reviewer_passes,
394 developer_agent: &developer_agent,
395 reviewer_agent: &reviewer_agent,
396 cli_args,
397 developer_agent_config: developer_config,
398 reviewer_agent_config: reviewer_config,
399 rebase_state: self.rebase_state,
400 git_user_name,
401 git_user_email,
402 run_id: &run_context.run_id,
403 parent_run_id: run_context.parent_run_id.as_deref(),
404 resume_count: run_context.resume_count,
405 actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
406 actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
407 working_dir,
408 prompt_md_checksum,
409 config_path,
410 config_checksum,
411 });
412
413 checkpoint.execution_history = self.execution_history;
415
416 checkpoint.prompt_history = self.prompt_history;
418
419 checkpoint.prompt_inputs = self.prompt_inputs;
421
422 checkpoint.prompt_permissions = self.prompt_permissions;
424
425 checkpoint.last_substitution_log = self.last_substitution_log;
427
428 checkpoint.log_run_id = self.log_run_id;
430
431 let executor_ref = self.executor.as_ref().map(|e| e.as_ref());
435 checkpoint.file_system_state = if let Some(ws) = workspace {
436 executor_ref.map(|executor| FileSystemState::capture_with_workspace(ws, executor))
437 } else {
438 Some(FileSystemState::capture_with_optional_executor_impl(
439 executor_ref,
440 ))
441 };
442
443 checkpoint.env_snapshot =
445 Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current());
446
447 Some(checkpoint)
448 }
449}
450
451fn review_depth_to_string(depth: ReviewDepth) -> Option<String> {
453 match depth {
454 ReviewDepth::Standard => Some("standard".to_string()),
455 ReviewDepth::Comprehensive => Some("comprehensive".to_string()),
456 ReviewDepth::Security => Some("security".to_string()),
457 ReviewDepth::Incremental => Some("incremental".to_string()),
458 }
459}
460
461#[cfg(test)]
462mod tests;