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, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub enum DevelopmentStatus {
19 Completed,
21 Partial,
23 Failed,
25}
26
27impl std::fmt::Display for DevelopmentStatus {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 Self::Completed => write!(f, "completed"),
31 Self::Partial => write!(f, "partial"),
32 Self::Failed => write!(f, "failed"),
33 }
34 }
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
57pub struct ContinuationState {
58 pub previous_status: Option<DevelopmentStatus>,
60 pub previous_summary: Option<String>,
62 pub previous_files_changed: Option<Vec<String>>,
64 pub previous_next_steps: Option<String>,
66 pub continuation_attempt: u32,
68}
69
70impl ContinuationState {
71 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn is_continuation(&self) -> bool {
78 self.continuation_attempt > 0
79 }
80
81 pub fn reset(&self) -> Self {
83 Self::default()
84 }
85
86 pub fn trigger_continuation(
88 &self,
89 status: DevelopmentStatus,
90 summary: String,
91 files_changed: Option<Vec<String>>,
92 next_steps: Option<String>,
93 ) -> Self {
94 Self {
95 previous_status: Some(status),
96 previous_summary: Some(summary),
97 previous_files_changed: files_changed,
98 previous_next_steps: next_steps,
99 continuation_attempt: self.continuation_attempt + 1,
100 }
101 }
102}
103
104#[derive(Clone, Serialize, Deserialize, Debug)]
109pub struct PipelineState {
110 pub phase: PipelinePhase,
111 pub previous_phase: Option<PipelinePhase>,
112 pub iteration: u32,
113 pub total_iterations: u32,
114 pub reviewer_pass: u32,
115 pub total_reviewer_passes: u32,
116 pub review_issues_found: bool,
117 pub context_cleaned: bool,
118 pub agent_chain: AgentChainState,
119 pub rebase: RebaseState,
120 pub commit: CommitState,
121 pub execution_history: Vec<ExecutionStep>,
122 #[serde(default)]
127 pub continuation: ContinuationState,
128}
129
130impl PipelineState {
131 pub fn initial(developer_iters: u32, reviewer_reviews: u32) -> Self {
132 let initial_phase = if developer_iters == 0 {
134 if reviewer_reviews == 0 {
136 PipelinePhase::CommitMessage
138 } else {
139 PipelinePhase::Review
140 }
141 } else {
142 PipelinePhase::Planning
143 };
144
145 Self {
146 phase: initial_phase,
147 previous_phase: None,
148 iteration: 0,
149 total_iterations: developer_iters,
150 reviewer_pass: 0,
151 total_reviewer_passes: reviewer_reviews,
152 review_issues_found: false,
153 context_cleaned: false,
154 agent_chain: AgentChainState::initial(),
155 rebase: RebaseState::NotStarted,
156 commit: CommitState::NotStarted,
157 execution_history: Vec::new(),
158 continuation: ContinuationState::new(),
159 }
160 }
161
162 pub fn is_complete(&self) -> bool {
163 matches!(
164 self.phase,
165 PipelinePhase::Complete | PipelinePhase::Interrupted
166 )
167 }
168
169 pub fn current_head(&self) -> String {
170 self.rebase
171 .current_head()
172 .unwrap_or_else(|| "HEAD".to_string())
173 }
174}
175
176#[derive(Clone, Serialize, Deserialize, Debug)]
183pub struct AgentChainState {
184 pub agents: Vec<String>,
185 pub current_agent_index: usize,
186 pub models_per_agent: Vec<Vec<String>>,
187 pub current_model_index: usize,
188 pub retry_cycle: u32,
189 pub max_cycles: u32,
190 pub current_role: AgentRole,
191 #[serde(default)]
196 pub rate_limit_continuation_prompt: Option<String>,
197}
198
199impl AgentChainState {
200 pub fn initial() -> Self {
201 Self {
202 agents: Vec::new(),
203 current_agent_index: 0,
204 models_per_agent: Vec::new(),
205 current_model_index: 0,
206 retry_cycle: 0,
207 max_cycles: 3,
208 current_role: AgentRole::Developer,
209 rate_limit_continuation_prompt: None,
210 }
211 }
212
213 pub fn with_agents(
214 mut self,
215 agents: Vec<String>,
216 models_per_agent: Vec<Vec<String>>,
217 role: AgentRole,
218 ) -> Self {
219 self.agents = agents;
220 self.models_per_agent = models_per_agent;
221 self.current_role = role;
222 self
223 }
224
225 pub fn with_max_cycles(mut self, max_cycles: u32) -> Self {
230 self.max_cycles = max_cycles;
231 self
232 }
233
234 pub fn current_agent(&self) -> Option<&String> {
235 self.agents.get(self.current_agent_index)
236 }
237
238 pub fn current_model(&self) -> Option<&String> {
245 self.models_per_agent
246 .get(self.current_agent_index)
247 .and_then(|models| models.get(self.current_model_index))
248 }
249
250 pub fn is_exhausted(&self) -> bool {
251 self.retry_cycle >= self.max_cycles
252 && self.current_agent_index == 0
253 && self.current_model_index == 0
254 }
255
256 pub fn advance_to_next_model(&self) -> Self {
257 let mut new = self.clone();
258 if let Some(models) = new.models_per_agent.get(new.current_agent_index) {
259 if new.current_model_index + 1 < models.len() {
260 new.current_model_index += 1;
261 } else {
262 new.current_model_index = 0;
263 }
264 }
265 new
266 }
267
268 pub fn switch_to_next_agent(&self) -> Self {
269 let mut new = self.clone();
270 if new.current_agent_index + 1 < new.agents.len() {
271 new.current_agent_index += 1;
272 new.current_model_index = 0;
273 } else {
274 new.current_agent_index = 0;
275 new.current_model_index = 0;
276 new.retry_cycle += 1;
277 }
278 new
279 }
280
281 pub fn switch_to_next_agent_with_prompt(&self, prompt: Option<String>) -> Self {
288 if self.agents.len() <= 1 {
295 let mut exhausted = self.clone();
296 exhausted.current_agent_index = 0;
297 exhausted.current_model_index = 0;
298 exhausted.retry_cycle = exhausted.max_cycles;
299 exhausted.rate_limit_continuation_prompt = None;
300 return exhausted;
301 }
302
303 if self.current_agent_index + 1 >= self.agents.len() {
304 let mut exhausted = self.clone();
305 exhausted.current_agent_index = 0;
306 exhausted.current_model_index = 0;
307 exhausted.retry_cycle = exhausted.max_cycles;
308 exhausted.rate_limit_continuation_prompt = None;
309 return exhausted;
310 }
311
312 let mut next = self.switch_to_next_agent();
313 next.rate_limit_continuation_prompt = prompt;
314 next
315 }
316
317 pub fn clear_continuation_prompt(&self) -> Self {
322 let mut new = self.clone();
323 new.rate_limit_continuation_prompt = None;
324 new
325 }
326
327 pub fn reset_for_role(&self, role: AgentRole) -> Self {
328 let mut new = self.clone();
329 new.current_role = role;
330 new.current_agent_index = 0;
331 new.current_model_index = 0;
332 new.retry_cycle = 0;
333 new.rate_limit_continuation_prompt = None;
334 new
335 }
336
337 pub fn reset(&self) -> Self {
338 let mut new = self.clone();
339 new.current_agent_index = 0;
340 new.current_model_index = 0;
341 new.rate_limit_continuation_prompt = None;
342 new
343 }
344
345 pub fn start_retry_cycle(&self) -> Self {
346 let mut new = self.clone();
347 new.current_agent_index = 0;
348 new.current_model_index = 0;
349 new.retry_cycle += 1;
350 new
351 }
352}
353
354#[derive(Clone, Serialize, Deserialize, Debug)]
359pub enum RebaseState {
360 NotStarted,
361 InProgress {
362 original_head: String,
363 target_branch: String,
364 },
365 Conflicted {
366 original_head: String,
367 target_branch: String,
368 files: Vec<PathBuf>,
369 resolution_attempts: u32,
370 },
371 Completed {
372 new_head: String,
373 },
374 Skipped,
375}
376
377impl RebaseState {
378 #[doc(hidden)]
379 #[cfg(any(test, feature = "test-utils"))]
380 pub fn is_terminal(&self) -> bool {
381 matches!(self, RebaseState::Completed { .. } | RebaseState::Skipped)
382 }
383
384 pub fn current_head(&self) -> Option<String> {
385 match self {
386 RebaseState::NotStarted | RebaseState::Skipped => None,
387 RebaseState::InProgress { original_head, .. } => Some(original_head.clone()),
388 RebaseState::Conflicted { .. } => None,
389 RebaseState::Completed { new_head } => Some(new_head.clone()),
390 }
391 }
392
393 #[doc(hidden)]
394 #[cfg(any(test, feature = "test-utils"))]
395 pub fn is_in_progress(&self) -> bool {
396 matches!(
397 self,
398 RebaseState::InProgress { .. } | RebaseState::Conflicted { .. }
399 )
400 }
401}
402
403pub const MAX_VALIDATION_RETRY_ATTEMPTS: u32 = 100;
414
415pub const MAX_DEV_VALIDATION_RETRY_ATTEMPTS: u32 = 10;
423
424#[derive(Clone, Serialize, Deserialize, Debug)]
429pub enum CommitState {
430 NotStarted,
431 Generating { attempt: u32, max_attempts: u32 },
432 Generated { message: String },
433 Committed { hash: String },
434 Skipped,
435}
436
437impl CommitState {
438 #[doc(hidden)]
439 #[cfg(any(test, feature = "test-utils"))]
440 pub fn is_terminal(&self) -> bool {
441 matches!(self, CommitState::Committed { .. } | CommitState::Skipped)
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn test_pipeline_state_initial() {
451 let state = PipelineState::initial(5, 2);
452 assert_eq!(state.phase, PipelinePhase::Planning);
453 assert_eq!(state.total_iterations, 5);
454 assert_eq!(state.total_reviewer_passes, 2);
455 assert!(!state.is_complete());
456 }
457
458 #[test]
459 fn test_agent_chain_initial() {
460 let chain = AgentChainState::initial();
461 assert!(chain.agents.is_empty());
462 assert_eq!(chain.current_agent_index, 0);
463 assert_eq!(chain.retry_cycle, 0);
464 }
465
466 #[test]
467 fn test_agent_chain_with_agents() {
468 let chain = AgentChainState::initial()
469 .with_agents(
470 vec!["claude".to_string(), "codex".to_string()],
471 vec![vec![], vec![]],
472 AgentRole::Developer,
473 )
474 .with_max_cycles(3);
475
476 assert_eq!(chain.agents.len(), 2);
477 assert_eq!(chain.current_agent(), Some(&"claude".to_string()));
478 assert_eq!(chain.max_cycles, 3);
479 }
480
481 #[test]
482 fn test_agent_chain_advance_to_next_model() {
483 let chain = AgentChainState::initial().with_agents(
484 vec!["claude".to_string()],
485 vec![vec!["model1".to_string(), "model2".to_string()]],
486 AgentRole::Developer,
487 );
488
489 let new_chain = chain.advance_to_next_model();
490 assert_eq!(new_chain.current_model_index, 1);
491 assert_eq!(new_chain.current_model(), Some(&"model2".to_string()));
492 }
493
494 #[test]
495 fn test_agent_chain_switch_to_next_agent() {
496 let chain = AgentChainState::initial().with_agents(
497 vec!["claude".to_string(), "codex".to_string()],
498 vec![vec![], vec![]],
499 AgentRole::Developer,
500 );
501
502 let new_chain = chain.switch_to_next_agent();
503 assert_eq!(new_chain.current_agent_index, 1);
504 assert_eq!(new_chain.current_agent(), Some(&"codex".to_string()));
505 assert_eq!(new_chain.retry_cycle, 0);
506 }
507
508 #[test]
509 fn test_agent_chain_exhausted() {
510 let chain = AgentChainState::initial()
511 .with_agents(
512 vec!["claude".to_string()],
513 vec![vec![]],
514 AgentRole::Developer,
515 )
516 .with_max_cycles(3);
517
518 let chain = chain.start_retry_cycle();
519 let chain = chain.start_retry_cycle();
520 let chain = chain.start_retry_cycle();
521
522 assert!(chain.is_exhausted());
523 }
524
525 #[test]
526 fn test_rebase_state_not_started() {
527 let state = RebaseState::NotStarted;
528 assert!(!state.is_terminal());
529 assert!(state.current_head().is_none());
530 assert!(!state.is_in_progress());
531 }
532
533 #[test]
534 fn test_rebase_state_in_progress() {
535 let state = RebaseState::InProgress {
536 original_head: "abc123".to_string(),
537 target_branch: "main".to_string(),
538 };
539 assert!(!state.is_terminal());
540 assert_eq!(state.current_head(), Some("abc123".to_string()));
541 assert!(state.is_in_progress());
542 }
543
544 #[test]
545 fn test_rebase_state_completed() {
546 let state = RebaseState::Completed {
547 new_head: "def456".to_string(),
548 };
549 assert!(state.is_terminal());
550 assert_eq!(state.current_head(), Some("def456".to_string()));
551 assert!(!state.is_in_progress());
552 }
553
554 #[test]
555 fn test_commit_state_not_started() {
556 let state = CommitState::NotStarted;
557 assert!(!state.is_terminal());
558 }
559
560 #[test]
561 fn test_commit_state_generating() {
562 let state = CommitState::Generating {
563 attempt: 1,
564 max_attempts: 3,
565 };
566 assert!(!state.is_terminal());
567 }
568
569 #[test]
570 fn test_commit_state_committed() {
571 let state = CommitState::Committed {
572 hash: "abc123".to_string(),
573 };
574 assert!(state.is_terminal());
575 }
576
577 #[test]
578 fn test_is_complete_during_finalizing() {
579 let state = PipelineState {
582 phase: PipelinePhase::Finalizing,
583 ..PipelineState::initial(5, 2)
584 };
585 assert!(
586 !state.is_complete(),
587 "Finalizing phase should not be complete - event loop must continue"
588 );
589 }
590
591 #[test]
592 fn test_is_complete_after_finalization() {
593 let state = PipelineState {
595 phase: PipelinePhase::Complete,
596 ..PipelineState::initial(5, 2)
597 };
598 assert!(state.is_complete(), "Complete phase should be complete");
599 }
600
601 #[test]
606 fn test_continuation_state_initial() {
607 let state = ContinuationState::new();
608 assert!(!state.is_continuation());
609 assert_eq!(state.continuation_attempt, 0);
610 assert!(state.previous_status.is_none());
611 assert!(state.previous_summary.is_none());
612 assert!(state.previous_files_changed.is_none());
613 assert!(state.previous_next_steps.is_none());
614 }
615
616 #[test]
617 fn test_continuation_state_default() {
618 let state = ContinuationState::default();
619 assert!(!state.is_continuation());
620 assert_eq!(state.continuation_attempt, 0);
621 }
622
623 #[test]
624 fn test_continuation_trigger_partial() {
625 let state = ContinuationState::new();
626 let new_state = state.trigger_continuation(
627 DevelopmentStatus::Partial,
628 "Did some work".to_string(),
629 Some(vec!["file1.rs".to_string()]),
630 Some("Continue with tests".to_string()),
631 );
632
633 assert!(new_state.is_continuation());
634 assert_eq!(new_state.continuation_attempt, 1);
635 assert_eq!(new_state.previous_status, Some(DevelopmentStatus::Partial));
636 assert_eq!(
637 new_state.previous_summary,
638 Some("Did some work".to_string())
639 );
640 assert_eq!(
641 new_state.previous_files_changed,
642 Some(vec!["file1.rs".to_string()])
643 );
644 assert_eq!(
645 new_state.previous_next_steps,
646 Some("Continue with tests".to_string())
647 );
648 }
649
650 #[test]
651 fn test_continuation_trigger_failed() {
652 let state = ContinuationState::new();
653 let new_state = state.trigger_continuation(
654 DevelopmentStatus::Failed,
655 "Build failed".to_string(),
656 None,
657 Some("Fix errors".to_string()),
658 );
659
660 assert!(new_state.is_continuation());
661 assert_eq!(new_state.continuation_attempt, 1);
662 assert_eq!(new_state.previous_status, Some(DevelopmentStatus::Failed));
663 assert_eq!(new_state.previous_summary, Some("Build failed".to_string()));
664 assert!(new_state.previous_files_changed.is_none());
665 assert_eq!(
666 new_state.previous_next_steps,
667 Some("Fix errors".to_string())
668 );
669 }
670
671 #[test]
672 fn test_continuation_reset() {
673 let state = ContinuationState::new().trigger_continuation(
674 DevelopmentStatus::Partial,
675 "Work".to_string(),
676 None,
677 None,
678 );
679
680 let reset = state.reset();
681 assert!(!reset.is_continuation());
682 assert_eq!(reset.continuation_attempt, 0);
683 assert!(reset.previous_status.is_none());
684 assert!(reset.previous_summary.is_none());
685 }
686
687 #[test]
688 fn test_multiple_continuations() {
689 let state = ContinuationState::new()
690 .trigger_continuation(
691 DevelopmentStatus::Partial,
692 "First".to_string(),
693 Some(vec!["a.rs".to_string()]),
694 None,
695 )
696 .trigger_continuation(
697 DevelopmentStatus::Partial,
698 "Second".to_string(),
699 Some(vec!["b.rs".to_string()]),
700 Some("Do more".to_string()),
701 );
702
703 assert_eq!(state.continuation_attempt, 2);
704 assert_eq!(state.previous_summary, Some("Second".to_string()));
705 assert_eq!(state.previous_files_changed, Some(vec!["b.rs".to_string()]));
706 assert_eq!(state.previous_next_steps, Some("Do more".to_string()));
707 }
708
709 #[test]
710 fn test_development_status_display() {
711 assert_eq!(format!("{}", DevelopmentStatus::Completed), "completed");
712 assert_eq!(format!("{}", DevelopmentStatus::Partial), "partial");
713 assert_eq!(format!("{}", DevelopmentStatus::Failed), "failed");
714 }
715
716 #[test]
717 fn test_pipeline_state_initial_has_empty_continuation() {
718 let state = PipelineState::initial(5, 2);
719 assert!(!state.continuation.is_continuation());
720 assert_eq!(state.continuation.continuation_attempt, 0);
721 }
722
723 #[test]
724 fn test_agent_chain_reset_clears_rate_limit_continuation_prompt() {
725 let mut chain = AgentChainState::initial().with_agents(
726 vec!["agent1".to_string(), "agent2".to_string()],
727 vec![vec![], vec![]],
728 AgentRole::Developer,
729 );
730 chain.rate_limit_continuation_prompt = Some("saved".to_string());
731
732 let reset = chain.reset();
733 assert!(
734 reset.rate_limit_continuation_prompt.is_none(),
735 "reset() should clear rate_limit_continuation_prompt"
736 );
737 }
738
739 #[test]
740 fn test_agent_chain_reset_for_role_clears_rate_limit_continuation_prompt() {
741 let mut chain = AgentChainState::initial().with_agents(
742 vec!["agent1".to_string(), "agent2".to_string()],
743 vec![vec![], vec![]],
744 AgentRole::Developer,
745 );
746 chain.rate_limit_continuation_prompt = Some("saved".to_string());
747
748 let reset = chain.reset_for_role(AgentRole::Reviewer);
749 assert!(
750 reset.rate_limit_continuation_prompt.is_none(),
751 "reset_for_role() should clear rate_limit_continuation_prompt"
752 );
753 }
754
755 #[test]
756 fn test_switch_to_next_agent_with_prompt_exhausts_when_single_agent() {
757 let chain = AgentChainState::initial().with_agents(
758 vec!["agent1".to_string()],
759 vec![vec![]],
760 AgentRole::Developer,
761 );
762
763 let next = chain.switch_to_next_agent_with_prompt(Some("prompt".to_string()));
764 assert!(
765 next.is_exhausted(),
766 "single-agent rate limit fallback should exhaust the chain"
767 );
768 }
769
770 #[test]
771 fn test_switch_to_next_agent_with_prompt_exhausts_on_wraparound() {
772 let mut chain = AgentChainState::initial().with_agents(
773 vec!["agent1".to_string(), "agent2".to_string()],
774 vec![vec![], vec![]],
775 AgentRole::Developer,
776 );
777 chain.current_agent_index = 1;
778
779 let next = chain.switch_to_next_agent_with_prompt(Some("prompt".to_string()));
780 assert!(
781 next.is_exhausted(),
782 "rate limit fallback should not wrap and retry a previously rate-limited agent"
783 );
784 }
785}