1use 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::logger::Logger;
16use crate::reducer::state::{PromptInputsState, PromptPermissionsState};
17use crate::workspace::Workspace;
18use crate::ProcessExecutor;
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 fn phase(self, phase: PipelinePhase, iteration: u32, total_iterations: u32) -> Self {
103 Self {
104 phase: Some(phase),
105 iteration,
106 total_iterations,
107 ..self
108 }
109 }
110
111 #[must_use]
113 pub fn reviewer_pass(self, pass: u32, total: u32) -> Self {
114 Self {
115 reviewer_pass: pass,
116 total_reviewer_passes: total,
117 ..self
118 }
119 }
120
121 #[must_use]
123 pub fn agents(self, developer: &str, reviewer: &str) -> Self {
124 Self {
125 developer_agent: Some(developer.to_string()),
126 reviewer_agent: Some(reviewer.to_string()),
127 ..self
128 }
129 }
130
131 #[must_use]
133 pub fn cli_args(self, args: CliArgsSnapshot) -> Self {
134 Self {
135 cli_args: Some(args),
136 ..self
137 }
138 }
139
140 #[must_use]
142 pub fn with_last_substitution_log(self, log: Option<crate::prompts::SubstitutionLog>) -> Self {
143 Self {
144 last_substitution_log: log,
145 ..self
146 }
147 }
148
149 #[must_use]
151 pub fn developer_config(self, config: AgentConfigSnapshot) -> Self {
152 Self {
153 developer_agent_config: Some(config),
154 ..self
155 }
156 }
157
158 #[must_use]
160 pub fn reviewer_config(self, config: AgentConfigSnapshot) -> Self {
161 Self {
162 reviewer_agent_config: Some(config),
163 ..self
164 }
165 }
166
167 #[must_use]
169 pub fn rebase_state(self, state: RebaseState) -> Self {
170 Self {
171 rebase_state: state,
172 ..self
173 }
174 }
175
176 #[must_use]
178 pub fn config_path(self, path: Option<std::path::PathBuf>) -> Self {
179 Self {
180 config_path: path,
181 ..self
182 }
183 }
184
185 #[must_use]
187 pub fn git_identity(self, name: Option<&str>, email: Option<&str>) -> Self {
188 Self {
189 git_user_name: name.map(String::from),
190 git_user_email: email.map(String::from),
191 ..self
192 }
193 }
194
195 #[must_use]
197 pub fn with_executor(self, executor: Arc<dyn ProcessExecutor>) -> Self {
198 Self {
199 executor: Some(executor),
200 ..self
201 }
202 }
203
204 #[must_use]
206 pub fn capture_cli_args(self, config: &Config) -> Self {
207 let review_depth_str = Some(review_depth_to_string(config.review_depth).to_string());
208 let snapshot = crate::checkpoint::state::CliArgsSnapshotBuilder::new(
209 config.developer_iters,
210 config.reviewer_reviews,
211 review_depth_str,
212 config.isolation_mode,
213 )
214 .verbosity(config.verbosity as u8)
215 .show_streaming_metrics(config.show_streaming_metrics)
216 .reviewer_json_parser(config.reviewer_json_parser.clone())
217 .build();
218 Self {
219 cli_args: Some(snapshot),
220 ..self
221 }
222 }
223
224 #[must_use]
229 pub fn capture_from_context(
230 mut self,
231 config: &Config,
232 registry: &AgentRegistry,
233 developer_name: &str,
234 reviewer_name: &str,
235 logger: &Logger,
236 run_context: &RunContext,
237 ) -> Self {
238 self.run_context = Some(run_context.clone());
240
241 self = self.capture_cli_args(config);
243
244 if let Some(agent_config) = registry.resolve_config(developer_name) {
246 let snapshot = AgentConfigSnapshot::new(
247 developer_name.to_string(),
248 agent_config.cmd.clone(),
249 agent_config.output_flag.clone(),
250 Some(agent_config.yolo_flag.clone()),
251 agent_config.can_commit,
252 )
253 .with_model_override(config.developer_model.clone())
254 .with_provider_override(config.developer_provider.clone())
255 .with_context_level(config.developer_context);
256 self.developer_agent_config = Some(snapshot);
257 self.developer_agent = Some(developer_name.to_string());
258 } else {
259 logger.warn(&format!(
260 "Developer agent '{developer_name}' not found in registry"
261 ));
262 }
263
264 if let Some(agent_config) = registry.resolve_config(reviewer_name) {
266 let snapshot = AgentConfigSnapshot::new(
267 reviewer_name.to_string(),
268 agent_config.cmd.clone(),
269 agent_config.output_flag.clone(),
270 Some(agent_config.yolo_flag.clone()),
271 agent_config.can_commit,
272 )
273 .with_model_override(config.reviewer_model.clone())
274 .with_provider_override(config.reviewer_provider.clone())
275 .with_context_level(config.reviewer_context);
276 self.reviewer_agent_config = Some(snapshot);
277 self.reviewer_agent = Some(reviewer_name.to_string());
278 } else {
279 logger.warn(&format!(
280 "Reviewer agent '{reviewer_name}' not found in registry"
281 ));
282 }
283
284 self.git_user_name = config.git_user_name.clone();
286 self.git_user_email = config.git_user_email.clone();
287
288 self
289 }
290
291 #[must_use]
296 pub fn with_executor_from_context(self, executor_arc: Arc<dyn ProcessExecutor>) -> Self {
297 Self {
298 executor: Some(executor_arc),
299 ..self
300 }
301 }
302
303 #[must_use]
308 pub fn with_execution_history(self, history: ExecutionHistory) -> Self {
309 Self {
310 execution_history: Some(history),
311 ..self
312 }
313 }
314
315 #[must_use]
323 pub fn with_prompt_history(
324 self,
325 history: std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>,
326 ) -> Self {
327 let prompt_history = if history.is_empty() {
328 None
329 } else {
330 Some(history)
331 };
332 Self {
333 prompt_history,
334 ..self
335 }
336 }
337
338 #[must_use]
344 pub fn with_prompt_inputs(self, prompt_inputs: PromptInputsState) -> Self {
345 let is_empty = prompt_inputs.planning.is_none()
346 && prompt_inputs.development.is_none()
347 && prompt_inputs.review.is_none()
348 && prompt_inputs.commit.is_none()
349 && prompt_inputs.xsd_retry_last_output.is_none();
350 let prompt_inputs = if is_empty { None } else { Some(prompt_inputs) };
351 Self {
352 prompt_inputs,
353 ..self
354 }
355 }
356
357 #[must_use]
359 pub fn with_prompt_permissions(self, prompt_permissions: PromptPermissionsState) -> Self {
360 Self {
361 prompt_permissions,
362 ..self
363 }
364 }
365
366 #[must_use]
371 pub fn with_log_run_id(self, log_run_id: String) -> Self {
372 Self {
373 log_run_id: Some(log_run_id),
374 ..self
375 }
376 }
377
378 #[must_use]
386 pub fn build(self) -> Option<PipelineCheckpoint> {
387 self.build_internal(None)
388 }
389
390 pub fn build_with_workspace(self, workspace: &dyn Workspace) -> Option<PipelineCheckpoint> {
400 self.build_internal(Some(workspace))
401 }
402
403 fn build_internal(self, workspace: Option<&dyn Workspace>) -> Option<PipelineCheckpoint> {
405 let phase = self.phase?;
406 let developer_agent = self.developer_agent?;
407 let reviewer_agent = self.reviewer_agent?;
408 let cli_args = self.cli_args?;
409 let developer_config = self.developer_agent_config?;
410 let reviewer_config = self.reviewer_agent_config?;
411
412 let git_user_name = self.git_user_name.as_deref();
413 let git_user_email = self.git_user_email.as_deref();
414
415 let run_context = self.run_context.unwrap_or_default();
417
418 let working_dir = workspace
419 .map(|ws| ws.root().to_string_lossy().to_string())
420 .or_else(crate::checkpoint::current_dir::get_current_dir)
421 .unwrap_or_default();
422
423 let prompt_md_checksum = workspace.and_then(|ws| {
424 calculate_file_checksum_with_workspace(ws, std::path::Path::new("PROMPT.md"))
425 });
426
427 let (config_path, config_checksum) = self.config_path.map_or((None, None), |path| {
428 let path_string = path.to_string_lossy().to_string();
429 let checksum = workspace.and_then(|ws| {
430 let relative = path.strip_prefix(ws.root()).ok().unwrap_or(&path);
431 calculate_file_checksum_with_workspace(ws, relative)
432 });
433 (Some(path_string), checksum)
434 });
435
436 let executor_ref = self.executor.as_ref().map(std::convert::AsRef::as_ref);
437
438 let checkpoint = PipelineCheckpoint {
439 execution_history: self.execution_history,
440 prompt_history: self.prompt_history,
441 prompt_inputs: self.prompt_inputs,
442 prompt_permissions: self.prompt_permissions,
443 last_substitution_log: self.last_substitution_log,
444 log_run_id: self.log_run_id,
445 file_system_state: workspace.map_or_else(
446 || {
447 Some(FileSystemState::capture_with_optional_executor_impl(
448 executor_ref,
449 ))
450 },
451 |ws| {
452 executor_ref
453 .map(|executor| FileSystemState::capture_with_workspace(ws, executor))
454 },
455 ),
456 env_snapshot: Some(crate::checkpoint::state::EnvironmentSnapshot::capture_current()),
457 ..PipelineCheckpoint::from_params(CheckpointParams {
458 phase,
459 iteration: self.iteration,
460 total_iterations: self.total_iterations,
461 reviewer_pass: self.reviewer_pass,
462 total_reviewer_passes: self.total_reviewer_passes,
463 developer_agent: &developer_agent,
464 reviewer_agent: &reviewer_agent,
465 cli_args,
466 developer_agent_config: developer_config,
467 reviewer_agent_config: reviewer_config,
468 rebase_state: self.rebase_state,
469 git_user_name,
470 git_user_email,
471 run_id: &run_context.run_id,
472 parent_run_id: run_context.parent_run_id.as_deref(),
473 resume_count: run_context.resume_count,
474 actual_developer_runs: run_context.actual_developer_runs.max(self.iteration),
475 actual_reviewer_runs: run_context.actual_reviewer_runs.max(self.reviewer_pass),
476 working_dir,
477 prompt_md_checksum,
478 config_path,
479 config_checksum,
480 })
481 };
482
483 Some(checkpoint)
484 }
485}
486
487const fn review_depth_to_string(depth: ReviewDepth) -> &'static str {
489 match depth {
490 ReviewDepth::Standard => "standard",
491 ReviewDepth::Comprehensive => "comprehensive",
492 ReviewDepth::Security => "security",
493 ReviewDepth::Incremental => "incremental",
494 }
495}
496
497#[cfg(test)]
498mod tests;