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