1use crate::agents::AgentRole;
7use crate::checkpoint::execution_history::ExecutionStep;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11use super::event::PipelinePhase;
12
13#[derive(Clone, Serialize, Deserialize, Debug)]
18pub struct PipelineState {
19 pub phase: PipelinePhase,
20 pub previous_phase: Option<PipelinePhase>,
21 pub iteration: u32,
22 pub total_iterations: u32,
23 pub reviewer_pass: u32,
24 pub total_reviewer_passes: u32,
25 pub review_issues_found: bool,
26 pub context_cleaned: bool,
27 pub agent_chain: AgentChainState,
28 pub rebase: RebaseState,
29 pub commit: CommitState,
30 pub execution_history: Vec<ExecutionStep>,
31}
32
33impl PipelineState {
34 pub fn initial(developer_iters: u32, reviewer_reviews: u32) -> Self {
35 let initial_phase = if developer_iters == 0 {
37 if reviewer_reviews == 0 {
39 PipelinePhase::CommitMessage
41 } else {
42 PipelinePhase::Review
43 }
44 } else {
45 PipelinePhase::Planning
46 };
47
48 Self {
49 phase: initial_phase,
50 previous_phase: None,
51 iteration: 0,
52 total_iterations: developer_iters,
53 reviewer_pass: 0,
54 total_reviewer_passes: reviewer_reviews,
55 review_issues_found: false,
56 context_cleaned: false,
57 agent_chain: AgentChainState::initial(),
58 rebase: RebaseState::NotStarted,
59 commit: CommitState::NotStarted,
60 execution_history: Vec::new(),
61 }
62 }
63
64 pub fn is_complete(&self) -> bool {
65 matches!(
66 self.phase,
67 PipelinePhase::Complete | PipelinePhase::Interrupted
68 )
69 }
70
71 pub fn current_head(&self) -> String {
72 self.rebase
73 .current_head()
74 .unwrap_or_else(|| "HEAD".to_string())
75 }
76}
77
78#[derive(Clone, Serialize, Deserialize, Debug)]
85pub struct AgentChainState {
86 pub agents: Vec<String>,
87 pub current_agent_index: usize,
88 pub models_per_agent: Vec<Vec<String>>,
89 pub current_model_index: usize,
90 pub retry_cycle: u32,
91 pub max_cycles: u32,
92 pub current_role: AgentRole,
93}
94
95impl AgentChainState {
96 pub fn initial() -> Self {
97 Self {
98 agents: Vec::new(),
99 current_agent_index: 0,
100 models_per_agent: Vec::new(),
101 current_model_index: 0,
102 retry_cycle: 0,
103 max_cycles: 3,
104 current_role: AgentRole::Developer,
105 }
106 }
107
108 pub fn with_agents(
109 mut self,
110 agents: Vec<String>,
111 models_per_agent: Vec<Vec<String>>,
112 role: AgentRole,
113 ) -> Self {
114 self.agents = agents;
115 self.models_per_agent = models_per_agent;
116 self.current_role = role;
117 self
118 }
119
120 pub fn with_max_cycles(mut self, max_cycles: u32) -> Self {
125 self.max_cycles = max_cycles;
126 self
127 }
128
129 pub fn current_agent(&self) -> Option<&String> {
130 self.agents.get(self.current_agent_index)
131 }
132
133 pub fn current_model(&self) -> Option<&String> {
140 self.models_per_agent
141 .get(self.current_agent_index)
142 .and_then(|models| models.get(self.current_model_index))
143 }
144
145 pub fn is_exhausted(&self) -> bool {
146 self.retry_cycle >= self.max_cycles
147 && self.current_agent_index == 0
148 && self.current_model_index == 0
149 }
150
151 pub fn advance_to_next_model(&self) -> Self {
152 let mut new = self.clone();
153 if let Some(models) = new.models_per_agent.get(new.current_agent_index) {
154 if new.current_model_index + 1 < models.len() {
155 new.current_model_index += 1;
156 } else {
157 new.current_model_index = 0;
158 }
159 }
160 new
161 }
162
163 pub fn switch_to_next_agent(&self) -> Self {
164 let mut new = self.clone();
165 if new.current_agent_index + 1 < new.agents.len() {
166 new.current_agent_index += 1;
167 new.current_model_index = 0;
168 } else {
169 new.current_agent_index = 0;
170 new.current_model_index = 0;
171 new.retry_cycle += 1;
172 }
173 new
174 }
175
176 pub fn reset_for_role(&self, role: AgentRole) -> Self {
177 let mut new = self.clone();
178 new.current_role = role;
179 new.current_agent_index = 0;
180 new.current_model_index = 0;
181 new.retry_cycle = 0;
182 new
183 }
184
185 pub fn reset(&self) -> Self {
186 let mut new = self.clone();
187 new.current_agent_index = 0;
188 new.current_model_index = 0;
189 new
190 }
191
192 pub fn start_retry_cycle(&self) -> Self {
193 let mut new = self.clone();
194 new.current_agent_index = 0;
195 new.current_model_index = 0;
196 new.retry_cycle += 1;
197 new
198 }
199}
200
201#[derive(Clone, Serialize, Deserialize, Debug)]
206pub enum RebaseState {
207 NotStarted,
208 InProgress {
209 original_head: String,
210 target_branch: String,
211 },
212 Conflicted {
213 original_head: String,
214 target_branch: String,
215 files: Vec<PathBuf>,
216 resolution_attempts: u32,
217 },
218 Completed {
219 new_head: String,
220 },
221 Skipped,
222}
223
224impl RebaseState {
225 #[doc(hidden)]
226 #[cfg(any(test, feature = "test-utils"))]
227 pub fn is_terminal(&self) -> bool {
228 matches!(self, RebaseState::Completed { .. } | RebaseState::Skipped)
229 }
230
231 pub fn current_head(&self) -> Option<String> {
232 match self {
233 RebaseState::NotStarted | RebaseState::Skipped => None,
234 RebaseState::InProgress { original_head, .. } => Some(original_head.clone()),
235 RebaseState::Conflicted { .. } => None,
236 RebaseState::Completed { new_head } => Some(new_head.clone()),
237 }
238 }
239
240 #[doc(hidden)]
241 #[cfg(any(test, feature = "test-utils"))]
242 pub fn is_in_progress(&self) -> bool {
243 matches!(
244 self,
245 RebaseState::InProgress { .. } | RebaseState::Conflicted { .. }
246 )
247 }
248}
249
250pub const MAX_VALIDATION_RETRY_ATTEMPTS: u32 = 100;
261
262pub const MAX_DEV_VALIDATION_RETRY_ATTEMPTS: u32 = 2;
268
269#[derive(Clone, Serialize, Deserialize, Debug)]
274pub enum CommitState {
275 NotStarted,
276 Generating { attempt: u32, max_attempts: u32 },
277 Generated { message: String },
278 Committed { hash: String },
279 Skipped,
280}
281
282impl CommitState {
283 #[doc(hidden)]
284 #[cfg(any(test, feature = "test-utils"))]
285 pub fn is_terminal(&self) -> bool {
286 matches!(self, CommitState::Committed { .. } | CommitState::Skipped)
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_pipeline_state_initial() {
296 let state = PipelineState::initial(5, 2);
297 assert_eq!(state.phase, PipelinePhase::Planning);
298 assert_eq!(state.total_iterations, 5);
299 assert_eq!(state.total_reviewer_passes, 2);
300 assert!(!state.is_complete());
301 }
302
303 #[test]
304 fn test_agent_chain_initial() {
305 let chain = AgentChainState::initial();
306 assert!(chain.agents.is_empty());
307 assert_eq!(chain.current_agent_index, 0);
308 assert_eq!(chain.retry_cycle, 0);
309 }
310
311 #[test]
312 fn test_agent_chain_with_agents() {
313 let chain = AgentChainState::initial()
314 .with_agents(
315 vec!["claude".to_string(), "codex".to_string()],
316 vec![vec![], vec![]],
317 AgentRole::Developer,
318 )
319 .with_max_cycles(3);
320
321 assert_eq!(chain.agents.len(), 2);
322 assert_eq!(chain.current_agent(), Some(&"claude".to_string()));
323 assert_eq!(chain.max_cycles, 3);
324 }
325
326 #[test]
327 fn test_agent_chain_advance_to_next_model() {
328 let chain = AgentChainState::initial().with_agents(
329 vec!["claude".to_string()],
330 vec![vec!["model1".to_string(), "model2".to_string()]],
331 AgentRole::Developer,
332 );
333
334 let new_chain = chain.advance_to_next_model();
335 assert_eq!(new_chain.current_model_index, 1);
336 assert_eq!(new_chain.current_model(), Some(&"model2".to_string()));
337 }
338
339 #[test]
340 fn test_agent_chain_switch_to_next_agent() {
341 let chain = AgentChainState::initial().with_agents(
342 vec!["claude".to_string(), "codex".to_string()],
343 vec![vec![], vec![]],
344 AgentRole::Developer,
345 );
346
347 let new_chain = chain.switch_to_next_agent();
348 assert_eq!(new_chain.current_agent_index, 1);
349 assert_eq!(new_chain.current_agent(), Some(&"codex".to_string()));
350 assert_eq!(new_chain.retry_cycle, 0);
351 }
352
353 #[test]
354 fn test_agent_chain_exhausted() {
355 let chain = AgentChainState::initial()
356 .with_agents(
357 vec!["claude".to_string()],
358 vec![vec![]],
359 AgentRole::Developer,
360 )
361 .with_max_cycles(3);
362
363 let chain = chain.start_retry_cycle();
364 let chain = chain.start_retry_cycle();
365 let chain = chain.start_retry_cycle();
366
367 assert!(chain.is_exhausted());
368 }
369
370 #[test]
371 fn test_rebase_state_not_started() {
372 let state = RebaseState::NotStarted;
373 assert!(!state.is_terminal());
374 assert!(state.current_head().is_none());
375 assert!(!state.is_in_progress());
376 }
377
378 #[test]
379 fn test_rebase_state_in_progress() {
380 let state = RebaseState::InProgress {
381 original_head: "abc123".to_string(),
382 target_branch: "main".to_string(),
383 };
384 assert!(!state.is_terminal());
385 assert_eq!(state.current_head(), Some("abc123".to_string()));
386 assert!(state.is_in_progress());
387 }
388
389 #[test]
390 fn test_rebase_state_completed() {
391 let state = RebaseState::Completed {
392 new_head: "def456".to_string(),
393 };
394 assert!(state.is_terminal());
395 assert_eq!(state.current_head(), Some("def456".to_string()));
396 assert!(!state.is_in_progress());
397 }
398
399 #[test]
400 fn test_commit_state_not_started() {
401 let state = CommitState::NotStarted;
402 assert!(!state.is_terminal());
403 }
404
405 #[test]
406 fn test_commit_state_generating() {
407 let state = CommitState::Generating {
408 attempt: 1,
409 max_attempts: 3,
410 };
411 assert!(!state.is_terminal());
412 }
413
414 #[test]
415 fn test_commit_state_committed() {
416 let state = CommitState::Committed {
417 hash: "abc123".to_string(),
418 };
419 assert!(state.is_terminal());
420 }
421
422 #[test]
423 fn test_is_complete_during_finalizing() {
424 let state = PipelineState {
427 phase: PipelinePhase::Finalizing,
428 ..PipelineState::initial(5, 2)
429 };
430 assert!(
431 !state.is_complete(),
432 "Finalizing phase should not be complete - event loop must continue"
433 );
434 }
435
436 #[test]
437 fn test_is_complete_after_finalization() {
438 let state = PipelineState {
440 phase: PipelinePhase::Complete,
441 ..PipelineState::initial(5, 2)
442 };
443 assert!(state.is_complete(), "Complete phase should be complete");
444 }
445}