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 executor: Option<Arc<dyn ProcessExecutor>>,
59 log_run_id: Option<String>,
61}
62
63impl Default for CheckpointBuilder {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl CheckpointBuilder {
70 pub fn new() -> Self {
72 Self {
73 phase: None,
74 iteration: 1,
75 total_iterations: 1,
76 reviewer_pass: 0,
77 total_reviewer_passes: 0,
78 developer_agent: None,
79 reviewer_agent: None,
80 cli_args: None,
81 developer_agent_config: None,
82 reviewer_agent_config: None,
83 rebase_state: RebaseState::default(),
84 config_path: None,
85 git_user_name: None,
86 git_user_email: None,
87 run_context: None,
88 execution_history: None,
89 prompt_history: None,
90 prompt_inputs: None,
91 prompt_permissions: PromptPermissionsState::default(),
92 executor: None,
93 log_run_id: None,
94 }
95 }
96
97 pub fn phase(mut self, phase: PipelinePhase, iteration: u32, total_iterations: u32) -> Self {
99 self.phase = Some(phase);
100 self.iteration = iteration;
101 self.total_iterations = total_iterations;
102 self
103 }
104
105 pub fn reviewer_pass(mut self, pass: u32, total: u32) -> Self {
107 self.reviewer_pass = pass;
108 self.total_reviewer_passes = total;
109 self
110 }
111
112 pub fn agents(mut self, developer: &str, reviewer: &str) -> Self {
114 self.developer_agent = Some(developer.to_string());
115 self.reviewer_agent = Some(reviewer.to_string());
116 self
117 }
118
119 pub fn cli_args(mut self, args: CliArgsSnapshot) -> Self {
121 self.cli_args = Some(args);
122 self
123 }
124
125 pub fn developer_config(mut self, config: AgentConfigSnapshot) -> Self {
127 self.developer_agent_config = Some(config);
128 self
129 }
130
131 pub fn reviewer_config(mut self, config: AgentConfigSnapshot) -> Self {
133 self.reviewer_agent_config = Some(config);
134 self
135 }
136
137 pub fn rebase_state(mut self, state: RebaseState) -> Self {
139 self.rebase_state = state;
140 self
141 }
142
143 pub fn config_path(mut self, path: Option<std::path::PathBuf>) -> Self {
145 self.config_path = path;
146 self
147 }
148
149 pub fn git_identity(mut self, name: Option<&str>, email: Option<&str>) -> Self {
151 self.git_user_name = name.map(String::from);
152 self.git_user_email = email.map(String::from);
153 self
154 }
155
156 pub fn with_executor(mut self, executor: Arc<dyn ProcessExecutor>) -> Self {
158 self.executor = Some(executor);
159 self
160 }
161
162 pub fn capture_cli_args(mut self, config: &Config) -> Self {
164 let review_depth_str = review_depth_to_string(config.review_depth);
165 let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
166 config.developer_iters,
167 config.reviewer_reviews,
168 review_depth_str,
169 config.isolation_mode,
170 )
171 .verbosity(config.verbosity as u8)
172 .show_streaming_metrics(config.show_streaming_metrics)
173 .reviewer_json_parser(config.reviewer_json_parser.clone())
174 .build();
175 self.cli_args = Some(snapshot);
176 self
177 }
178
179 pub fn capture_from_context(
184 mut self,
185 config: &Config,
186 registry: &AgentRegistry,
187 developer_name: &str,
188 reviewer_name: &str,
189 logger: &Logger,
190 run_context: &RunContext,
191 ) -> Self {
192 self.run_context = Some(run_context.clone());
194
195 self = self.capture_cli_args(config);
197
198 if let Some(agent_config) = registry.resolve_config(developer_name) {
200 let snapshot = AgentConfigSnapshot::new(
201 developer_name.to_string(),
202 agent_config.cmd.clone(),
203 agent_config.output_flag.clone(),
204 Some(agent_config.yolo_flag.clone()),
205 agent_config.can_commit,
206 )
207 .with_model_override(config.developer_model.clone())
208 .with_provider_override(config.developer_provider.clone())
209 .with_context_level(config.developer_context);
210 self.developer_agent_config = Some(snapshot);
211 self.developer_agent = Some(developer_name.to_string());
212 } else {
213 logger.warn(&format!(
214 "Developer agent '{}' not found in registry",
215 developer_name
216 ));
217 }
218
219 if let Some(agent_config) = registry.resolve_config(reviewer_name) {
221 let snapshot = AgentConfigSnapshot::new(
222 reviewer_name.to_string(),
223 agent_config.cmd.clone(),
224 agent_config.output_flag.clone(),
225 Some(agent_config.yolo_flag.clone()),
226 agent_config.can_commit,
227 )
228 .with_model_override(config.reviewer_model.clone())
229 .with_provider_override(config.reviewer_provider.clone())
230 .with_context_level(config.reviewer_context);
231 self.reviewer_agent_config = Some(snapshot);
232 self.reviewer_agent = Some(reviewer_name.to_string());
233 } else {
234 logger.warn(&format!(
235 "Reviewer agent '{}' not found in registry",
236 reviewer_name
237 ));
238 }
239
240 self.git_user_name = config.git_user_name.clone();
242 self.git_user_email = config.git_user_email.clone();
243
244 self
245 }
246
247 pub fn with_executor_from_context(mut self, executor_arc: Arc<dyn ProcessExecutor>) -> Self {
252 self.executor = Some(executor_arc);
253 self
254 }
255
256 pub fn with_execution_history(mut self, history: ExecutionHistory) -> Self {
261 self.execution_history = Some(history);
262 self
263 }
264
265 pub fn with_prompt_history(
273 mut self,
274 history: std::collections::HashMap<String, String>,
275 ) -> Self {
276 self.prompt_history = if history.is_empty() {
277 None
278 } else {
279 Some(history)
280 };
281 self
282 }
283
284 pub fn with_prompt_inputs(mut self, prompt_inputs: PromptInputsState) -> Self {
290 let is_empty = prompt_inputs.planning.is_none()
291 && prompt_inputs.development.is_none()
292 && prompt_inputs.review.is_none()
293 && prompt_inputs.commit.is_none()
294 && prompt_inputs.xsd_retry_last_output.is_none();
295 self.prompt_inputs = if is_empty { None } else { Some(prompt_inputs) };
296 self
297 }
298
299 pub fn with_prompt_permissions(mut self, prompt_permissions: PromptPermissionsState) -> Self {
301 self.prompt_permissions = prompt_permissions;
302 self
303 }
304
305 pub fn with_log_run_id(mut self, log_run_id: String) -> Self {
310 self.log_run_id = Some(log_run_id);
311 self
312 }
313
314 pub fn build(self) -> Option<PipelineCheckpoint> {
322 self.build_internal(None)
323 }
324
325 pub fn build_with_workspace(self, workspace: &dyn Workspace) -> Option<PipelineCheckpoint> {
335 self.build_internal(Some(workspace))
336 }
337
338 fn build_internal(self, workspace: Option<&dyn Workspace>) -> Option<PipelineCheckpoint> {
340 let phase = self.phase?;
341 let developer_agent = self.developer_agent?;
342 let reviewer_agent = self.reviewer_agent?;
343 let cli_args = self.cli_args?;
344 let developer_config = self.developer_agent_config?;
345 let reviewer_config = self.reviewer_agent_config?;
346
347 let git_user_name = self.git_user_name.as_deref();
348 let git_user_email = self.git_user_email.as_deref();
349
350 let run_context = self.run_context.unwrap_or_default();
352
353 let working_dir = workspace
354 .map(|ws| ws.root().to_string_lossy().to_string())
355 .or_else(|| {
356 std::env::current_dir()
357 .ok()
358 .map(|p| p.to_string_lossy().to_string())
359 })
360 .unwrap_or_default();
361
362 let prompt_md_checksum = workspace.and_then(|ws| {
363 calculate_file_checksum_with_workspace(ws, std::path::Path::new("PROMPT.md"))
364 });
365
366 let (config_path, config_checksum) = if let Some(path) = self.config_path {
367 let path_string = path.to_string_lossy().to_string();
368 let checksum = workspace.and_then(|ws| {
369 let relative = path.strip_prefix(ws.root()).ok().unwrap_or(&path);
370 calculate_file_checksum_with_workspace(ws, relative)
371 });
372 (Some(path_string), checksum)
373 } else {
374 (None, None)
375 };
376
377 let mut checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
378 phase,
379 iteration: self.iteration,
380 total_iterations: self.total_iterations,
381 reviewer_pass: self.reviewer_pass,
382 total_reviewer_passes: self.total_reviewer_passes,
383 developer_agent: &developer_agent,
384 reviewer_agent: &reviewer_agent,
385 cli_args,
386 developer_agent_config: developer_config,
387 reviewer_agent_config: reviewer_config,
388 rebase_state: self.rebase_state,
389 git_user_name,
390 git_user_email,
391 run_id: &run_context.run_id,
392 parent_run_id: run_context.parent_run_id.as_deref(),
393 resume_count: run_context.resume_count,
394 actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
395 actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
396 working_dir,
397 prompt_md_checksum,
398 config_path,
399 config_checksum,
400 });
401
402 checkpoint.execution_history = self.execution_history;
404
405 checkpoint.prompt_history = self.prompt_history;
407
408 checkpoint.prompt_inputs = self.prompt_inputs;
410
411 checkpoint.prompt_permissions = self.prompt_permissions;
413
414 checkpoint.log_run_id = self.log_run_id;
416
417 let executor_ref = self.executor.as_ref().map(|e| e.as_ref());
421 checkpoint.file_system_state = if let Some(ws) = workspace {
422 executor_ref.map(|executor| FileSystemState::capture_with_workspace(ws, executor))
423 } else {
424 Some(FileSystemState::capture_with_optional_executor_impl(
425 executor_ref,
426 ))
427 };
428
429 checkpoint.env_snapshot =
431 Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current());
432
433 Some(checkpoint)
434 }
435}
436
437fn review_depth_to_string(depth: ReviewDepth) -> Option<String> {
439 match depth {
440 ReviewDepth::Standard => Some("standard".to_string()),
441 ReviewDepth::Comprehensive => Some("comprehensive".to_string()),
442 ReviewDepth::Security => Some("security".to_string()),
443 ReviewDepth::Incremental => Some("incremental".to_string()),
444 }
445}
446
447#[cfg(test)]
448mod tests;