1use crate::agents::AgentRegistry;
7use crate::checkpoint::execution_history::ExecutionHistory;
8use crate::checkpoint::file_state::FileSystemState;
9use crate::checkpoint::state::{
10 AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, PipelineCheckpoint, PipelinePhase,
11 RebaseState,
12};
13use crate::checkpoint::RunContext;
14use crate::config::{Config, ReviewDepth};
15use crate::logger::Logger;
16
17pub struct CheckpointBuilder {
32 phase: Option<PipelinePhase>,
33 iteration: u32,
34 total_iterations: u32,
35 reviewer_pass: u32,
36 total_reviewer_passes: u32,
37 developer_agent: Option<String>,
38 reviewer_agent: Option<String>,
39 cli_args: Option<CliArgsSnapshot>,
40 developer_agent_config: Option<AgentConfigSnapshot>,
41 reviewer_agent_config: Option<AgentConfigSnapshot>,
42 rebase_state: RebaseState,
43 config_path: Option<std::path::PathBuf>,
44 git_user_name: Option<String>,
45 git_user_email: Option<String>,
46 run_context: Option<RunContext>,
48 execution_history: Option<ExecutionHistory>,
50 prompt_history: Option<std::collections::HashMap<String, String>>,
51 skip_rebase: Option<bool>,
53}
54
55impl Default for CheckpointBuilder {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl CheckpointBuilder {
62 pub fn new() -> Self {
64 Self {
65 phase: None,
66 iteration: 1,
67 total_iterations: 1,
68 reviewer_pass: 0,
69 total_reviewer_passes: 0,
70 developer_agent: None,
71 reviewer_agent: None,
72 cli_args: None,
73 developer_agent_config: None,
74 reviewer_agent_config: None,
75 rebase_state: RebaseState::default(),
76 config_path: None,
77 git_user_name: None,
78 git_user_email: None,
79 run_context: None,
80 execution_history: None,
81 prompt_history: None,
82 skip_rebase: None,
83 }
84 }
85
86 pub fn phase(mut self, phase: PipelinePhase, iteration: u32, total_iterations: u32) -> Self {
88 self.phase = Some(phase);
89 self.iteration = iteration;
90 self.total_iterations = total_iterations;
91 self
92 }
93
94 pub fn reviewer_pass(mut self, pass: u32, total: u32) -> Self {
96 self.reviewer_pass = pass;
97 self.total_reviewer_passes = total;
98 self
99 }
100
101 #[cfg(test)]
103 pub fn agents(mut self, developer: &str, reviewer: &str) -> Self {
104 self.developer_agent = Some(developer.to_string());
105 self.reviewer_agent = Some(reviewer.to_string());
106 self
107 }
108
109 #[cfg(test)]
111 pub fn cli_args(mut self, args: CliArgsSnapshot) -> Self {
112 self.cli_args = Some(args);
113 self
114 }
115
116 #[cfg(test)]
118 pub fn developer_config(mut self, config: AgentConfigSnapshot) -> Self {
119 self.developer_agent_config = Some(config);
120 self
121 }
122
123 #[cfg(test)]
125 pub fn reviewer_config(mut self, config: AgentConfigSnapshot) -> Self {
126 self.reviewer_agent_config = Some(config);
127 self
128 }
129
130 #[cfg(test)]
132 pub fn rebase_state(mut self, state: RebaseState) -> Self {
133 self.rebase_state = state;
134 self
135 }
136
137 #[cfg(test)]
139 pub fn config_path(mut self, path: Option<std::path::PathBuf>) -> Self {
140 self.config_path = path;
141 self
142 }
143
144 #[cfg(test)]
146 pub fn git_identity(mut self, name: Option<&str>, email: Option<&str>) -> Self {
147 self.git_user_name = name.map(String::from);
148 self.git_user_email = email.map(String::from);
149 self
150 }
151
152 pub fn skip_rebase(mut self, value: bool) -> Self {
154 self.skip_rebase = Some(value);
155 self
156 }
157
158 pub fn capture_cli_args(mut self, config: &Config) -> Self {
160 let review_depth_str = review_depth_to_string(config.review_depth);
161 let skip_rebase = self.skip_rebase.unwrap_or(false);
162
163 let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
164 config.developer_iters,
165 config.reviewer_reviews,
166 config.commit_msg.clone(),
167 review_depth_str,
168 skip_rebase,
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_execution_history(mut self, history: ExecutionHistory) -> Self {
252 self.execution_history = Some(history);
253 self
254 }
255
256 pub fn with_prompt_history(
264 mut self,
265 history: std::collections::HashMap<String, String>,
266 ) -> Self {
267 self.prompt_history = if history.is_empty() {
268 None
269 } else {
270 Some(history)
271 };
272 self
273 }
274
275 pub fn build(self) -> Option<PipelineCheckpoint> {
280 let phase = self.phase?;
281 let developer_agent = self.developer_agent?;
282 let reviewer_agent = self.reviewer_agent?;
283 let cli_args = self.cli_args?;
284 let developer_config = self.developer_agent_config?;
285 let reviewer_config = self.reviewer_agent_config?;
286
287 let git_user_name = self.git_user_name.as_deref();
288 let git_user_email = self.git_user_email.as_deref();
289
290 let run_context = self.run_context.unwrap_or_default();
292
293 let mut checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
294 phase,
295 iteration: self.iteration,
296 total_iterations: self.total_iterations,
297 reviewer_pass: self.reviewer_pass,
298 total_reviewer_passes: self.total_reviewer_passes,
299 developer_agent: &developer_agent,
300 reviewer_agent: &reviewer_agent,
301 cli_args,
302 developer_agent_config: developer_config,
303 reviewer_agent_config: reviewer_config,
304 rebase_state: self.rebase_state,
305 git_user_name,
306 git_user_email,
307 run_id: &run_context.run_id,
308 parent_run_id: run_context.parent_run_id.as_deref(),
309 resume_count: run_context.resume_count,
310 actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
311 actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
312 });
313
314 if let Some(path) = self.config_path {
315 checkpoint = checkpoint.with_config(Some(path));
316 }
317
318 checkpoint.execution_history = self.execution_history;
320
321 checkpoint.prompt_history = self.prompt_history;
323
324 checkpoint.file_system_state = Some(FileSystemState::capture_current());
326
327 checkpoint.env_snapshot =
329 Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current());
330
331 Some(checkpoint)
332 }
333}
334
335fn review_depth_to_string(depth: ReviewDepth) -> Option<String> {
337 match depth {
338 ReviewDepth::Standard => Some("standard".to_string()),
339 ReviewDepth::Comprehensive => Some("comprehensive".to_string()),
340 ReviewDepth::Security => Some("security".to_string()),
341 ReviewDepth::Incremental => Some("incremental".to_string()),
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_builder_basic() {
351 let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
352 let dev_config =
353 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
354 let rev_config =
355 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
356
357 let checkpoint = CheckpointBuilder::new()
358 .phase(PipelinePhase::Development, 2, 5)
359 .reviewer_pass(1, 2)
360 .agents("dev", "rev")
361 .cli_args(cli_args)
362 .developer_config(dev_config)
363 .reviewer_config(rev_config)
364 .build()
365 .unwrap();
366
367 assert_eq!(checkpoint.phase, PipelinePhase::Development);
368 assert_eq!(checkpoint.iteration, 2);
369 assert_eq!(checkpoint.total_iterations, 5);
370 assert_eq!(checkpoint.reviewer_pass, 1);
371 assert_eq!(checkpoint.total_reviewer_passes, 2);
372 }
373
374 #[test]
375 fn test_builder_missing_required_field() {
376 let result = CheckpointBuilder::new().build();
378 assert!(result.is_none());
379 }
380
381 #[test]
382 fn test_review_depth_to_string() {
383 assert_eq!(
384 review_depth_to_string(ReviewDepth::Standard),
385 Some("standard".to_string())
386 );
387 assert_eq!(
388 review_depth_to_string(ReviewDepth::Comprehensive),
389 Some("comprehensive".to_string())
390 );
391 assert_eq!(
392 review_depth_to_string(ReviewDepth::Security),
393 Some("security".to_string())
394 );
395 assert_eq!(
396 review_depth_to_string(ReviewDepth::Incremental),
397 Some("incremental".to_string())
398 );
399 }
400
401 #[test]
402 fn test_builder_with_prompt_history() {
403 let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
404 let dev_config =
405 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
406 let rev_config =
407 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
408
409 let mut prompts = std::collections::HashMap::new();
410 prompts.insert(
411 "development_1".to_string(),
412 "Implement feature X".to_string(),
413 );
414
415 let checkpoint = CheckpointBuilder::new()
416 .phase(PipelinePhase::Development, 2, 5)
417 .reviewer_pass(1, 2)
418 .agents("dev", "rev")
419 .cli_args(cli_args)
420 .developer_config(dev_config)
421 .reviewer_config(rev_config)
422 .with_prompt_history(prompts)
423 .build()
424 .unwrap();
425
426 assert_eq!(checkpoint.phase, PipelinePhase::Development);
427 assert!(checkpoint.prompt_history.is_some());
428 let history = checkpoint.prompt_history.as_ref().unwrap();
429 assert_eq!(history.len(), 1);
430 assert_eq!(
431 history.get("development_1"),
432 Some(&"Implement feature X".to_string())
433 );
434 }
435
436 #[test]
437 fn test_builder_with_prompt_history_multiple() {
438 let cli_args = CliArgsSnapshot::new(5, 2, "test".into(), None, false, true, 2, false, None);
439 let dev_config =
440 AgentConfigSnapshot::new("dev".into(), "cmd".into(), "-o".into(), None, true);
441 let rev_config =
442 AgentConfigSnapshot::new("rev".into(), "cmd".into(), "-o".into(), None, true);
443
444 let mut prompts = std::collections::HashMap::new();
445 prompts.insert(
446 "development_1".to_string(),
447 "Implement feature X".to_string(),
448 );
449 prompts.insert("review_1".to_string(), "Review the changes".to_string());
450
451 let checkpoint = CheckpointBuilder::new()
452 .phase(PipelinePhase::Development, 2, 5)
453 .reviewer_pass(1, 2)
454 .agents("dev", "rev")
455 .cli_args(cli_args)
456 .developer_config(dev_config)
457 .reviewer_config(rev_config)
458 .with_prompt_history(prompts)
459 .build()
460 .unwrap();
461
462 assert!(checkpoint.prompt_history.is_some());
463 let history = checkpoint.prompt_history.as_ref().unwrap();
464 assert_eq!(history.len(), 2);
465 assert_eq!(
466 history.get("development_1"),
467 Some(&"Implement feature X".to_string())
468 );
469 assert_eq!(
470 history.get("review_1"),
471 Some(&"Review the changes".to_string())
472 );
473 }
474}