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, crate::prompts::PromptHistoryEntry>>,
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 #[must_use]
73 pub fn new() -> Self {
74 Self {
75 phase: None,
76 iteration: 1,
77 total_iterations: 1,
78 reviewer_pass: 0,
79 total_reviewer_passes: 0,
80 developer_agent: None,
81 reviewer_agent: None,
82 cli_args: None,
83 developer_agent_config: None,
84 reviewer_agent_config: None,
85 rebase_state: RebaseState::default(),
86 config_path: None,
87 git_user_name: None,
88 git_user_email: None,
89 run_context: None,
90 execution_history: None,
91 prompt_history: None,
92 prompt_inputs: None,
93 prompt_permissions: PromptPermissionsState::default(),
94 last_substitution_log: None,
95 executor: None,
96 log_run_id: None,
97 }
98 }
99
100 #[must_use]
102 pub const fn phase(
103 mut self,
104 phase: PipelinePhase,
105 iteration: u32,
106 total_iterations: u32,
107 ) -> Self {
108 self.phase = Some(phase);
109 self.iteration = iteration;
110 self.total_iterations = total_iterations;
111 self
112 }
113
114 #[must_use]
116 pub const fn reviewer_pass(mut self, pass: u32, total: u32) -> Self {
117 self.reviewer_pass = pass;
118 self.total_reviewer_passes = total;
119 self
120 }
121
122 #[must_use]
124 pub fn agents(mut self, developer: &str, reviewer: &str) -> Self {
125 self.developer_agent = Some(developer.to_string());
126 self.reviewer_agent = Some(reviewer.to_string());
127 self
128 }
129
130 #[must_use]
132 pub fn cli_args(mut self, args: CliArgsSnapshot) -> Self {
133 self.cli_args = Some(args);
134 self
135 }
136
137 #[must_use]
139 pub fn with_last_substitution_log(
140 mut self,
141 log: Option<crate::prompts::SubstitutionLog>,
142 ) -> Self {
143 self.last_substitution_log = log;
144 self
145 }
146
147 #[must_use]
149 pub fn developer_config(mut self, config: AgentConfigSnapshot) -> Self {
150 self.developer_agent_config = Some(config);
151 self
152 }
153
154 #[must_use]
156 pub fn reviewer_config(mut self, config: AgentConfigSnapshot) -> Self {
157 self.reviewer_agent_config = Some(config);
158 self
159 }
160
161 #[must_use]
163 pub fn rebase_state(mut self, state: RebaseState) -> Self {
164 self.rebase_state = state;
165 self
166 }
167
168 #[must_use]
170 pub fn config_path(mut self, path: Option<std::path::PathBuf>) -> Self {
171 self.config_path = path;
172 self
173 }
174
175 #[must_use]
177 pub fn git_identity(mut self, name: Option<&str>, email: Option<&str>) -> Self {
178 self.git_user_name = name.map(String::from);
179 self.git_user_email = email.map(String::from);
180 self
181 }
182
183 #[must_use]
185 pub fn with_executor(mut self, executor: Arc<dyn ProcessExecutor>) -> Self {
186 self.executor = Some(executor);
187 self
188 }
189
190 #[must_use]
192 pub fn capture_cli_args(mut self, config: &Config) -> Self {
193 let review_depth_str = Some(review_depth_to_string(config.review_depth).to_string());
194 let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
195 config.developer_iters,
196 config.reviewer_reviews,
197 review_depth_str,
198 config.isolation_mode,
199 )
200 .verbosity(config.verbosity as u8)
201 .show_streaming_metrics(config.show_streaming_metrics)
202 .reviewer_json_parser(config.reviewer_json_parser.clone())
203 .build();
204 self.cli_args = Some(snapshot);
205 self
206 }
207
208 #[must_use]
213 pub fn capture_from_context(
214 mut self,
215 config: &Config,
216 registry: &AgentRegistry,
217 developer_name: &str,
218 reviewer_name: &str,
219 logger: &Logger,
220 run_context: &RunContext,
221 ) -> Self {
222 self.run_context = Some(run_context.clone());
224
225 self = self.capture_cli_args(config);
227
228 if let Some(agent_config) = registry.resolve_config(developer_name) {
230 let snapshot = AgentConfigSnapshot::new(
231 developer_name.to_string(),
232 agent_config.cmd.clone(),
233 agent_config.output_flag.clone(),
234 Some(agent_config.yolo_flag.clone()),
235 agent_config.can_commit,
236 )
237 .with_model_override(config.developer_model.clone())
238 .with_provider_override(config.developer_provider.clone())
239 .with_context_level(config.developer_context);
240 self.developer_agent_config = Some(snapshot);
241 self.developer_agent = Some(developer_name.to_string());
242 } else {
243 logger.warn(&format!(
244 "Developer agent '{developer_name}' not found in registry"
245 ));
246 }
247
248 if let Some(agent_config) = registry.resolve_config(reviewer_name) {
250 let snapshot = AgentConfigSnapshot::new(
251 reviewer_name.to_string(),
252 agent_config.cmd.clone(),
253 agent_config.output_flag.clone(),
254 Some(agent_config.yolo_flag.clone()),
255 agent_config.can_commit,
256 )
257 .with_model_override(config.reviewer_model.clone())
258 .with_provider_override(config.reviewer_provider.clone())
259 .with_context_level(config.reviewer_context);
260 self.reviewer_agent_config = Some(snapshot);
261 self.reviewer_agent = Some(reviewer_name.to_string());
262 } else {
263 logger.warn(&format!(
264 "Reviewer agent '{reviewer_name}' not found in registry"
265 ));
266 }
267
268 self.git_user_name.clone_from(&config.git_user_name);
270 self.git_user_email.clone_from(&config.git_user_email);
271
272 self
273 }
274
275 #[must_use]
280 pub fn with_executor_from_context(mut self, executor_arc: Arc<dyn ProcessExecutor>) -> Self {
281 self.executor = Some(executor_arc);
282 self
283 }
284
285 #[must_use]
290 pub fn with_execution_history(mut self, history: ExecutionHistory) -> Self {
291 self.execution_history = Some(history);
292 self
293 }
294
295 #[must_use]
303 pub fn with_prompt_history(
304 mut self,
305 history: std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>,
306 ) -> Self {
307 self.prompt_history = if history.is_empty() {
308 None
309 } else {
310 Some(history)
311 };
312 self
313 }
314
315 #[must_use]
321 pub fn with_prompt_inputs(mut self, prompt_inputs: PromptInputsState) -> Self {
322 let is_empty = prompt_inputs.planning.is_none()
323 && prompt_inputs.development.is_none()
324 && prompt_inputs.review.is_none()
325 && prompt_inputs.commit.is_none()
326 && prompt_inputs.xsd_retry_last_output.is_none();
327 self.prompt_inputs = if is_empty { None } else { Some(prompt_inputs) };
328 self
329 }
330
331 #[must_use]
333 pub fn with_prompt_permissions(mut self, prompt_permissions: PromptPermissionsState) -> Self {
334 self.prompt_permissions = prompt_permissions;
335 self
336 }
337
338 #[must_use]
343 pub fn with_log_run_id(mut self, log_run_id: String) -> Self {
344 self.log_run_id = Some(log_run_id);
345 self
346 }
347
348 #[must_use]
356 pub fn build(self) -> Option<PipelineCheckpoint> {
357 self.build_internal(None)
358 }
359
360 pub fn build_with_workspace(self, workspace: &dyn Workspace) -> Option<PipelineCheckpoint> {
370 self.build_internal(Some(workspace))
371 }
372
373 fn build_internal(self, workspace: Option<&dyn Workspace>) -> Option<PipelineCheckpoint> {
375 let phase = self.phase?;
376 let developer_agent = self.developer_agent?;
377 let reviewer_agent = self.reviewer_agent?;
378 let cli_args = self.cli_args?;
379 let developer_config = self.developer_agent_config?;
380 let reviewer_config = self.reviewer_agent_config?;
381
382 let git_user_name = self.git_user_name.as_deref();
383 let git_user_email = self.git_user_email.as_deref();
384
385 let run_context = self.run_context.unwrap_or_default();
387
388 let working_dir = workspace
389 .map(|ws| ws.root().to_string_lossy().to_string())
390 .or_else(|| {
391 std::env::current_dir()
392 .ok()
393 .map(|p| p.to_string_lossy().to_string())
394 })
395 .unwrap_or_default();
396
397 let prompt_md_checksum = workspace.and_then(|ws| {
398 calculate_file_checksum_with_workspace(ws, std::path::Path::new("PROMPT.md"))
399 });
400
401 let (config_path, config_checksum) = self.config_path.map_or((None, None), |path| {
402 let path_string = path.to_string_lossy().to_string();
403 let checksum = workspace.and_then(|ws| {
404 let relative = path.strip_prefix(ws.root()).ok().unwrap_or(&path);
405 calculate_file_checksum_with_workspace(ws, relative)
406 });
407 (Some(path_string), checksum)
408 });
409
410 let mut checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
411 phase,
412 iteration: self.iteration,
413 total_iterations: self.total_iterations,
414 reviewer_pass: self.reviewer_pass,
415 total_reviewer_passes: self.total_reviewer_passes,
416 developer_agent: &developer_agent,
417 reviewer_agent: &reviewer_agent,
418 cli_args,
419 developer_agent_config: developer_config,
420 reviewer_agent_config: reviewer_config,
421 rebase_state: self.rebase_state,
422 git_user_name,
423 git_user_email,
424 run_id: &run_context.run_id,
425 parent_run_id: run_context.parent_run_id.as_deref(),
426 resume_count: run_context.resume_count,
427 actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
428 actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
429 working_dir,
430 prompt_md_checksum,
431 config_path,
432 config_checksum,
433 });
434
435 checkpoint.execution_history = self.execution_history;
437
438 checkpoint.prompt_history = self.prompt_history;
440
441 checkpoint.prompt_inputs = self.prompt_inputs;
443
444 checkpoint.prompt_permissions = self.prompt_permissions;
446
447 checkpoint.last_substitution_log = self.last_substitution_log;
449
450 checkpoint.log_run_id = self.log_run_id;
452
453 let executor_ref = self.executor.as_ref().map(std::convert::AsRef::as_ref);
457 checkpoint.file_system_state = workspace.map_or_else(
458 || {
459 Some(FileSystemState::capture_with_optional_executor_impl(
460 executor_ref,
461 ))
462 },
463 |ws| executor_ref.map(|executor| FileSystemState::capture_with_workspace(ws, executor)),
464 );
465
466 checkpoint.env_snapshot =
468 Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current());
469
470 Some(checkpoint)
471 }
472}
473
474const fn review_depth_to_string(depth: ReviewDepth) -> &'static str {
476 match depth {
477 ReviewDepth::Standard => "standard",
478 ReviewDepth::Comprehensive => "comprehensive",
479 ReviewDepth::Security => "security",
480 ReviewDepth::Incremental => "incremental",
481 }
482}
483
484#[cfg(test)]
485mod tests;