1mod agent;
114mod analysis;
115mod chain;
116mod checkpoint;
117mod cloud;
118mod commit;
119mod connectivity;
120mod context;
121mod development;
122mod development_prompt;
123mod io_agent;
124mod io_commit;
125mod lifecycle;
126mod planning;
127mod rebase;
128pub(crate) mod retry_guidance;
129mod run_fix;
130mod run_review;
131mod run_review_prompt;
132
133#[cfg(test)]
134mod tests;
135
136use crate::phases::PhaseContext;
137use crate::prompts::{PromptHistoryEntry, PromptScopeKey};
138use crate::reducer::effect::{Effect, EffectHandler, EffectResult};
139use crate::reducer::event::{PipelineEvent, PipelinePhase};
140use crate::reducer::state::PipelineState;
141use crate::reducer::ui_event::UIEvent;
142use anyhow::Result;
143use std::hash::BuildHasher;
144
145fn execute_backoff_wait(
146 ctx: &mut PhaseContext<'_>,
147 role: crate::agents::AgentRole,
148 cycle: u32,
149 duration_ms: u64,
150) -> Result<EffectResult> {
151 ctx.registry
152 .retry_timer()
153 .sleep(std::time::Duration::from_millis(duration_ms));
154 Ok(EffectResult::event(
155 PipelineEvent::agent_retry_cycle_started(role, cycle),
156 ))
157}
158
159fn execute_write_continuation_context(
160 ctx: &mut PhaseContext<'_>,
161 data: &crate::reducer::effect::ContinuationContextData,
162) -> Result<EffectResult> {
163 development::write_continuation_context_to_workspace(ctx.workspace, ctx.logger, data)?;
164 Ok(EffectResult::event(
165 PipelineEvent::development_continuation_context_written(data.iteration, data.attempt),
166 ))
167}
168
169fn ensure_completion_marker_dir(ctx: &PhaseContext<'_>) {
170 if let Err(err) = ctx
171 .workspace
172 .create_dir_all(std::path::Path::new(".agent/tmp"))
173 {
174 ctx.logger.warn(&format!(
175 "Failed to create completion marker directory: {err}"
176 ));
177 }
178}
179
180fn write_completion_marker_content(
181 ctx: &PhaseContext<'_>,
182 content: &str,
183 is_failure: bool,
184) -> std::result::Result<(), String> {
185 let marker_path = std::path::Path::new(".agent/tmp/completion_marker");
186 match ctx.workspace.write(marker_path, content) {
187 Ok(()) => {
188 ctx.logger.info(&format!(
189 "Completion marker written: {}",
190 if is_failure { "failure" } else { "success" }
191 ));
192 Ok(())
193 }
194 Err(err) => {
195 ctx.logger
196 .warn(&format!("Failed to write completion marker: {err}"));
197 Err(err.to_string())
198 }
199 }
200}
201
202fn get_stored_or_generate_prompt_with_validation<F, S: BuildHasher>(
203 scope_key: &PromptScopeKey,
204 prompt_history: &std::collections::HashMap<String, PromptHistoryEntry, S>,
205 current_content_id: Option<&str>,
206 generator: F,
207) -> (String, bool, bool)
208where
209 F: FnOnce() -> (String, bool),
210{
211 let key = scope_key.to_string();
212 match prompt_history.get(&key) {
213 Some(entry) if !content_id_mismatch(entry, current_content_id) => {
214 (entry.content.clone(), true, false)
215 }
216 _ => {
217 let (prompt, should_validate) = generator();
218 (prompt, false, should_validate)
219 }
220 }
221}
222
223fn content_id_mismatch(entry: &PromptHistoryEntry, current_content_id: Option<&str>) -> bool {
224 current_content_id.is_some_and(|current_id| entry.content_id.as_deref() != Some(current_id))
225}
226
227pub struct MainEffectHandler {
231 pub state: PipelineState,
233 pub event_log: Vec<PipelineEvent>,
235}
236
237impl MainEffectHandler {
238 #[must_use]
240 pub const fn new(state: PipelineState) -> Self {
241 Self {
242 state,
243 event_log: Vec::new(),
244 }
245 }
246}
247
248impl EffectHandler<'_> for MainEffectHandler {
249 fn execute(&mut self, effect: Effect, ctx: &mut PhaseContext<'_>) -> Result<EffectResult> {
250 let result = self.execute_effect(effect, ctx)?;
251 self.event_log.push(result.event.clone());
252 self.event_log
253 .extend(result.additional_events.iter().cloned());
254 Ok(result)
255 }
256}
257
258impl crate::app::event_loop::StatefulHandler for MainEffectHandler {
259 fn update_state(&mut self, state: PipelineState) {
260 self.state = state;
261 }
262}
263
264impl MainEffectHandler {
265 const fn phase_transition_ui(&self, to: PipelinePhase) -> UIEvent {
267 UIEvent::PhaseTransition {
268 from: Some(self.state.phase),
269 to,
270 }
271 }
272
273 fn write_completion_marker(
274 ctx: &PhaseContext<'_>,
275 content: &str,
276 is_failure: bool,
277 ) -> std::result::Result<(), String> {
278 ensure_completion_marker_dir(ctx);
279 write_completion_marker_content(ctx, content, is_failure)
280 }
281
282 fn execute_effect(
283 &mut self,
284 effect: Effect,
285 ctx: &mut PhaseContext<'_>,
286 ) -> Result<EffectResult> {
287 match effect {
288 Effect::AgentInvocation {
289 role,
290 agent,
291 model,
292 prompt,
293 } => self.execute_agent_invocation_effect(ctx, role, agent, model, prompt),
294 Effect::InitializeAgentChain { drain, .. } => {
295 Ok(self.initialize_agent_chain(ctx, drain))
296 }
297 e => self.execute_non_agent_effect(e, ctx),
298 }
299 }
300
301 fn execute_agent_invocation_effect(
302 &mut self,
303 ctx: &mut PhaseContext<'_>,
304 role: crate::agents::AgentRole,
305 agent: String,
306 model: Option<String>,
307 prompt: String,
308 ) -> Result<EffectResult> {
309 self.invoke_agent(
310 ctx,
311 crate::agents::AgentDrain::from(role),
312 role,
313 &agent,
314 model.as_deref(),
315 prompt,
316 )
317 }
318
319 fn execute_non_agent_effect(
320 &mut self,
321 effect: Effect,
322 ctx: &mut PhaseContext<'_>,
323 ) -> Result<EffectResult> {
324 match effect {
325 Effect::BackoffWait {
326 role,
327 cycle,
328 duration_ms,
329 } => execute_backoff_wait(ctx, role, cycle, duration_ms),
330 Effect::ReportAgentChainExhausted { role, phase, cycle } => Err(
331 crate::reducer::event::ErrorEvent::AgentChainExhausted { role, phase, cycle }
332 .into(),
333 ),
334 e => self.execute_phase_effect(e, ctx),
335 }
336 }
337
338 fn execute_phase_effect(
339 &mut self,
340 effect: Effect,
341 ctx: &mut PhaseContext<'_>,
342 ) -> Result<EffectResult> {
343 match effect {
344 e @ (Effect::PreparePlanningPrompt { .. }
345 | Effect::MaterializePlanningInputs { .. }
346 | Effect::CleanupRequiredFiles { .. }
347 | Effect::InvokePlanningAgent { .. }
348 | Effect::ExtractPlanningXml { .. }
349 | Effect::ValidatePlanningXml { .. }
350 | Effect::WritePlanningMarkdown { .. }
351 | Effect::ArchivePlanningXml { .. }
352 | Effect::ApplyPlanningOutcome { .. }) => self.execute_planning_effect(e, ctx),
353 e @ (Effect::PrepareDevelopmentContext { .. }
354 | Effect::MaterializeDevelopmentInputs { .. }
355 | Effect::PrepareDevelopmentPrompt { .. }
356 | Effect::InvokeDevelopmentAgent { .. }
357 | Effect::InvokeAnalysisAgent { .. }
358 | Effect::ExtractDevelopmentXml { .. }
359 | Effect::ValidateDevelopmentXml { .. }
360 | Effect::ApplyDevelopmentOutcome { .. }
361 | Effect::ArchiveDevelopmentXml { .. }) => self.execute_development_effect(e, ctx),
362 e => self.execute_phase_effect_b(e, ctx),
363 }
364 }
365
366 fn execute_phase_effect_b(
367 &mut self,
368 effect: Effect,
369 ctx: &mut PhaseContext<'_>,
370 ) -> Result<EffectResult> {
371 match effect {
372 e @ (Effect::PrepareReviewContext { .. }
373 | Effect::MaterializeReviewInputs { .. }
374 | Effect::PrepareReviewPrompt { .. }
375 | Effect::InvokeReviewAgent { .. }
376 | Effect::ExtractReviewIssuesXml { .. }
377 | Effect::ValidateReviewIssuesXml { .. }
378 | Effect::WriteIssuesMarkdown { .. }
379 | Effect::ExtractReviewIssueSnippets { .. }
380 | Effect::ArchiveReviewIssuesXml { .. }
381 | Effect::ApplyReviewOutcome { .. }
382 | Effect::PrepareFixPrompt { .. }
383 | Effect::InvokeFixAgent { .. }
384 | Effect::InvokeFixAnalysisAgent { .. }
385 | Effect::ExtractFixResultXml { .. }
386 | Effect::ValidateFixResultXml { .. }
387 | Effect::ApplyFixOutcome { .. }
388 | Effect::ArchiveFixResultXml { .. }) => self.execute_review_effect(e, ctx),
389 e @ (Effect::PrepareCommitPrompt { .. }
390 | Effect::CheckCommitDiff
391 | Effect::MaterializeCommitInputs { .. }
392 | Effect::InvokeCommitAgent
393 | Effect::ExtractCommitXml
394 | Effect::ValidateCommitXml
395 | Effect::ApplyCommitMessageOutcome
396 | Effect::ArchiveCommitXml
397 | Effect::CreateCommit { .. }
398 | Effect::SkipCommit { .. }
399 | Effect::CheckResidualFiles { .. }
400 | Effect::CheckUncommittedChangesBeforeTermination) => {
401 self.execute_commit_effect(e, ctx)
402 }
403 e @ (Effect::RunRebase { .. } | Effect::ResolveRebaseConflicts { .. }) => {
404 self.execute_rebase_effect(e, ctx)
405 }
406 e => self.execute_lifecycle_effect(e, ctx),
407 }
408 }
409
410 fn execute_planning_effect(
411 &mut self,
412 effect: Effect,
413 ctx: &mut PhaseContext<'_>,
414 ) -> Result<EffectResult> {
415 match effect {
416 Effect::PreparePlanningPrompt {
417 iteration,
418 prompt_mode,
419 } => self.prepare_planning_prompt(ctx, iteration, prompt_mode),
420 Effect::MaterializePlanningInputs { iteration } => {
421 self.materialize_planning_inputs(ctx, iteration)
422 }
423 Effect::CleanupRequiredFiles { files } => Ok(self.cleanup_required_files(ctx, &files)),
424 Effect::InvokePlanningAgent { iteration } => self.invoke_planning_agent(ctx, iteration),
425 e => self.execute_planning_effect_b(e, ctx),
426 }
427 }
428
429 fn execute_planning_effect_b(
430 &mut self,
431 effect: Effect,
432 ctx: &mut PhaseContext<'_>,
433 ) -> Result<EffectResult> {
434 match effect {
435 Effect::ExtractPlanningXml { iteration } => {
436 Ok(self.extract_planning_xml(ctx, iteration))
437 }
438 Effect::ValidatePlanningXml { iteration } => self.validate_planning_xml(ctx, iteration),
439 Effect::WritePlanningMarkdown { iteration } => {
440 self.write_planning_markdown(ctx, iteration)
441 }
442 Effect::ArchivePlanningXml { iteration } => {
443 Ok(Self::archive_planning_xml(ctx, iteration))
444 }
445 Effect::ApplyPlanningOutcome { iteration, valid } => {
446 Ok(self.apply_planning_outcome(ctx, iteration, valid))
447 }
448 _ => unreachable!("execute_planning_effect called with non-planning effect"),
449 }
450 }
451
452 fn execute_development_effect(
453 &mut self,
454 effect: Effect,
455 ctx: &mut PhaseContext<'_>,
456 ) -> Result<EffectResult> {
457 match effect {
458 Effect::PrepareDevelopmentContext { iteration } => {
459 Ok(Self::prepare_development_context(ctx, iteration))
460 }
461 Effect::MaterializeDevelopmentInputs { iteration } => {
462 self.materialize_development_inputs(ctx, iteration)
463 }
464 Effect::PrepareDevelopmentPrompt {
465 iteration,
466 prompt_mode,
467 } => self.prepare_development_prompt(ctx, iteration, prompt_mode),
468 Effect::InvokeDevelopmentAgent { iteration } => {
469 self.invoke_development_agent(ctx, iteration)
470 }
471 e => self.execute_development_effect_b(e, ctx),
472 }
473 }
474
475 fn execute_development_effect_b(
476 &mut self,
477 effect: Effect,
478 ctx: &mut PhaseContext<'_>,
479 ) -> Result<EffectResult> {
480 match effect {
481 Effect::InvokeAnalysisAgent { iteration } => self.invoke_analysis_agent(ctx, iteration),
482 Effect::ExtractDevelopmentXml { iteration } => {
483 Ok(self.extract_development_xml(ctx, iteration))
484 }
485 Effect::ValidateDevelopmentXml { iteration } => {
486 Ok(self.validate_development_xml(ctx, iteration))
487 }
488 Effect::ApplyDevelopmentOutcome { iteration } => {
489 self.apply_development_outcome(ctx, iteration)
490 }
491 Effect::ArchiveDevelopmentXml { iteration } => {
492 Ok(Self::archive_development_xml(ctx, iteration))
493 }
494 _ => unreachable!("execute_development_effect called with non-development effect"),
495 }
496 }
497
498 fn execute_review_effect(
499 &mut self,
500 effect: Effect,
501 ctx: &mut PhaseContext<'_>,
502 ) -> Result<EffectResult> {
503 match effect {
504 Effect::PrepareReviewContext { pass } => Ok(self.prepare_review_context(ctx, pass)),
505 Effect::MaterializeReviewInputs { pass } => self.materialize_review_inputs(ctx, pass),
506 Effect::PrepareReviewPrompt { pass, prompt_mode } => {
507 self.prepare_review_prompt(ctx, pass, prompt_mode)
508 }
509 Effect::InvokeReviewAgent { pass } => self.invoke_review_agent(ctx, pass),
510 Effect::ExtractReviewIssuesXml { pass } => {
511 Ok(self.extract_review_issues_xml(ctx, pass))
512 }
513 Effect::ValidateReviewIssuesXml { pass } => {
514 Ok(self.validate_review_issues_xml(ctx, pass))
515 }
516 e => self.execute_fix_effect(e, ctx),
517 }
518 }
519
520 fn execute_fix_effect(
521 &mut self,
522 effect: Effect,
523 ctx: &mut PhaseContext<'_>,
524 ) -> Result<EffectResult> {
525 match effect {
526 Effect::WriteIssuesMarkdown { pass } => self.write_issues_markdown(ctx, pass),
527 Effect::ExtractReviewIssueSnippets { pass } => {
528 self.extract_review_issue_snippets(ctx, pass)
529 }
530 Effect::ArchiveReviewIssuesXml { pass } => {
531 Ok(Self::archive_review_issues_xml(ctx, pass))
532 }
533 e => self.execute_fix_outcome_or_agent_effect(e, ctx),
534 }
535 }
536
537 fn execute_fix_outcome_or_agent_effect(
538 &mut self,
539 effect: Effect,
540 ctx: &mut PhaseContext<'_>,
541 ) -> Result<EffectResult> {
542 match effect {
543 Effect::ApplyReviewOutcome {
544 pass,
545 issues_found,
546 clean_no_issues,
547 } => Ok(Self::apply_review_outcome(
548 ctx,
549 pass,
550 issues_found,
551 clean_no_issues,
552 )),
553 e => self.execute_fix_agent_effect(e, ctx),
554 }
555 }
556
557 fn execute_fix_agent_effect(
558 &mut self,
559 effect: Effect,
560 ctx: &mut PhaseContext<'_>,
561 ) -> Result<EffectResult> {
562 match effect {
563 Effect::PrepareFixPrompt { pass, prompt_mode } => {
564 self.prepare_fix_prompt(ctx, pass, prompt_mode)
565 }
566 Effect::InvokeFixAgent { pass } => self.invoke_fix_agent(ctx, pass),
567 Effect::InvokeFixAnalysisAgent { pass } => self.invoke_fix_analysis_agent(ctx, pass),
568 Effect::ExtractFixResultXml { pass } => Ok(self.extract_fix_result_xml(ctx, pass)),
569 Effect::ValidateFixResultXml { pass } => Ok(self.validate_fix_result_xml(ctx, pass)),
570 Effect::ApplyFixOutcome { pass } => self.apply_fix_outcome(ctx, pass),
571 Effect::ArchiveFixResultXml { pass } => Ok(self.archive_fix_result_xml(ctx, pass)),
572 _ => unreachable!("execute_fix_effect called with non-fix effect"),
573 }
574 }
575
576 fn execute_commit_effect(
577 &mut self,
578 effect: Effect,
579 ctx: &mut PhaseContext<'_>,
580 ) -> Result<EffectResult> {
581 match effect {
582 Effect::PrepareCommitPrompt { prompt_mode } => {
583 self.prepare_commit_prompt(ctx, prompt_mode)
584 }
585 Effect::CheckCommitDiff => Self::check_commit_diff(ctx),
586 Effect::MaterializeCommitInputs { attempt } => {
587 self.materialize_commit_inputs(ctx, attempt)
588 }
589 Effect::InvokeCommitAgent => self.invoke_commit_agent(ctx),
590 Effect::ExtractCommitXml => Ok(self.extract_commit_xml(ctx)),
591 Effect::ValidateCommitXml => Ok(self.validate_commit_xml(ctx)),
592 e => self.execute_commit_finalization_effect(e, ctx),
593 }
594 }
595
596 fn execute_commit_finalization_effect(
597 &mut self,
598 effect: Effect,
599 ctx: &mut PhaseContext<'_>,
600 ) -> Result<EffectResult> {
601 match effect {
602 Effect::ApplyCommitMessageOutcome => self.apply_commit_message_outcome(ctx),
603 Effect::ArchiveCommitXml => Ok(self.archive_commit_xml(ctx)),
604 Effect::CreateCommit {
605 message,
606 files,
607 excluded_files,
608 } => Self::create_commit(ctx, message, &files, &excluded_files),
609 Effect::SkipCommit { reason } => Ok(Self::skip_commit(ctx, reason)),
610 Effect::CheckResidualFiles { pass } => Self::check_residual_files(ctx, pass),
611 Effect::CheckUncommittedChangesBeforeTermination => {
612 Self::check_uncommitted_changes_before_termination(ctx)
613 }
614 _ => unreachable!("execute_commit_effect called with non-commit effect"),
615 }
616 }
617
618 fn execute_rebase_effect(
619 &mut self,
620 effect: Effect,
621 ctx: &mut PhaseContext<'_>,
622 ) -> Result<EffectResult> {
623 match effect {
624 Effect::RunRebase {
625 phase,
626 target_branch,
627 } => self.run_rebase(ctx, phase, &target_branch),
628 Effect::ResolveRebaseConflicts { strategy } => {
629 Ok(Self::resolve_rebase_conflicts(ctx, strategy))
630 }
631 _ => unreachable!("execute_rebase_effect called with non-rebase effect"),
632 }
633 }
634
635 fn execute_lifecycle_effect(
636 &mut self,
637 effect: Effect,
638 ctx: &mut PhaseContext<'_>,
639 ) -> Result<EffectResult> {
640 match effect {
641 Effect::ValidateFinalState => Ok(self.validate_final_state(ctx)),
642 Effect::SaveCheckpoint { trigger } => Ok(self.save_checkpoint(ctx, trigger)),
643 Effect::EnsureGitignoreEntries => Ok(Self::ensure_gitignore_entries(ctx)),
644 Effect::CleanupContext => Self::cleanup_context(ctx),
645 Effect::LockPromptPermissions => Ok(Self::lock_prompt_permissions(ctx)),
646 Effect::RestorePromptPermissions => Ok(self.restore_prompt_permissions(ctx)),
647 Effect::WriteContinuationContext(ref data) => {
648 execute_write_continuation_context(ctx, data)
649 }
650 Effect::CleanupContinuationContext => Self::cleanup_continuation_context(ctx),
651 Effect::WriteTimeoutContext {
652 role,
653 logfile_path,
654 context_path,
655 } => Self::write_timeout_context(ctx, role, &logfile_path, &context_path),
656 e => self.execute_lifecycle_effect_b(e, ctx),
657 }
658 }
659
660 fn execute_lifecycle_effect_b(
661 &mut self,
662 effect: Effect,
663 ctx: &mut PhaseContext<'_>,
664 ) -> Result<EffectResult> {
665 match effect {
666 Effect::TriggerLoopRecovery {
667 detected_loop,
668 loop_count,
669 } => Ok(Self::trigger_loop_recovery(ctx, &detected_loop, loop_count)),
670 Effect::EmitRecoveryReset {
671 reset_type,
672 target_phase,
673 } => Ok(self.emit_recovery_reset(ctx, &reset_type, target_phase)),
674 Effect::CheckNetworkConnectivity => Ok(connectivity::check_network_connectivity(
675 &self.state.connectivity,
676 )),
677 Effect::PollForConnectivity { interval_ms } => Ok(connectivity::poll_for_connectivity(
678 interval_ms,
679 &self.state.connectivity,
680 )),
681 e => self.execute_lifecycle_effect_recovery_or_c(e, ctx),
682 }
683 }
684
685 fn execute_lifecycle_effect_recovery_or_c(
686 &mut self,
687 effect: Effect,
688 ctx: &mut PhaseContext<'_>,
689 ) -> Result<EffectResult> {
690 match effect {
691 Effect::AttemptRecovery {
692 level,
693 attempt_count,
694 } => Ok(self.attempt_recovery(ctx, level, attempt_count)),
695 Effect::EmitRecoverySuccess {
696 level,
697 total_attempts,
698 } => Ok(Self::emit_recovery_success(ctx, level, total_attempts)),
699 e => self.execute_lifecycle_effect_c(e, ctx),
700 }
701 }
702
703 fn execute_lifecycle_effect_c(
704 &mut self,
705 effect: Effect,
706 ctx: &mut PhaseContext<'_>,
707 ) -> Result<EffectResult> {
708 match effect {
709 Effect::TriggerDevFixFlow {
710 failed_phase,
711 failed_role,
712 retry_cycle,
713 } => Ok(self.trigger_dev_fix_flow(ctx, failed_phase, failed_role, retry_cycle)),
714 Effect::EmitCompletionMarkerAndTerminate { is_failure, reason } => Ok(
715 Self::emit_completion_marker_and_terminate(ctx, is_failure, reason),
716 ),
717 Effect::ConfigureGitAuth { auth_method } => {
718 Ok(Self::handle_configure_git_auth(ctx, &auth_method))
719 }
720 e => Self::execute_lifecycle_git_effect(ctx, e),
721 }
722 }
723
724 fn execute_lifecycle_git_effect(
725 ctx: &mut PhaseContext<'_>,
726 effect: Effect,
727 ) -> Result<EffectResult> {
728 match effect {
729 Effect::PushToRemote {
730 remote,
731 branch,
732 force,
733 commit_sha,
734 } => Ok(Self::handle_push_to_remote(
735 ctx, remote, branch, force, commit_sha,
736 )),
737 Effect::CreatePullRequest {
738 base_branch,
739 head_branch,
740 title,
741 body,
742 } => Ok(Self::handle_create_pull_request(
743 ctx,
744 &base_branch,
745 &head_branch,
746 &title,
747 &body,
748 )),
749 _ => unreachable!("execute_lifecycle_effect called with unexpected effect"),
750 }
751 }
752}