Skip to main content

ralph_workflow/reducer/handler/
mod.rs

1//! Effect handler implementation for pipeline side effects.
2//!
3//! This module implements the [`EffectHandler`] trait to execute pipeline effects
4//! through the reducer architecture. Effect handlers perform actual work (agent
5//! invocation, git operations, file I/O) and emit events that drive state transitions.
6//!
7//! # Architecture Contract
8//!
9//! ```text
10//! State → Orchestrator → Effect → Handler → Event → Reducer → State
11//!                                  ^^^^^^^
12//!                                  Impure execution (this module)
13//! ```
14//!
15//! ## Handler Responsibilities
16//!
17//! - **Execute effects**: Perform the I/O operation specified by the effect
18//! - **Report outcomes**: Emit events describing what happened (success/failure)
19//! - **Use workspace abstraction**: All filesystem access via `ctx.workspace`
20//! - **Single-task execution**: Execute exactly one effect, no hidden retry logic
21//!
22//! ## Reducer Responsibilities (NOT handler)
23//!
24//! - **Pure state transitions**: Process events to update state
25//! - **Policy decisions**: Retry, fallback, phase progression
26//! - **Control flow**: Determine what happens next based on events
27//!
28//! # Key Principle: Handlers Report, Reducers Decide
29//!
30//! Handlers must NOT contain decision logic. Examples:
31//!
32//! ```ignore
33//! // WRONG - Handler decides to retry
34//! fn handle_invoke_agent() -> Result<EffectResult> {
35//!     for attempt in 0..3 {  // NO! Reducer controls retry
36//!         if let Ok(output) = invoke_agent() {
37//!             return Ok(output);
38//!         }
39//!     }
40//! }
41//!
42//! // CORRECT - Handler reports outcome, reducer decides
43//! fn handle_invoke_agent() -> Result<EffectResult> {
44//!     match invoke_agent() {
45//!         Ok(output) => Ok(EffectResult::event(
46//!             AgentEvent::InvocationSucceeded { output }
47//!         )),
48//!         Err(e) => Ok(EffectResult::event(
49//!             AgentEvent::InvocationFailed { error: e, retriable: true }
50//!         )),
51//!     }
52//! }
53//! ```
54//!
55//! The reducer processes `InvocationFailed` and decides whether to retry
56//! (increment retry count, emit retry effect) or fallback (advance chain).
57//!
58//! # Workspace Abstraction
59//!
60//! All filesystem operations MUST use `ctx.workspace`:
61//!
62//! ```ignore
63//! // CORRECT
64//! ctx.workspace.write(path, content)?;
65//! let content = ctx.workspace.read(path)?;
66//!
67//! // WRONG - Never use std::fs in handlers
68//! std::fs::write(path, content)?;
69//! ```
70//!
71//! This abstraction enables:
72//! - In-memory testing with `MemoryWorkspace`
73//! - Proper error handling and path resolution
74//! - Consistent file operations across the pipeline
75//!
76//! See [`docs/agents/workspace-trait.md`] for details.
77//!
78//! # Testing Handlers
79//!
80//! Handlers require mocks for I/O (workspace) but NOT for reducer/orchestration:
81//!
82//! ```ignore
83//! #[test]
84//! fn test_invoke_agent_emits_success_event() {
85//!     let workspace = MemoryWorkspace::new_test();
86//!     let mut ctx = create_test_context(&workspace);
87//!
88//!     let result = handler.execute(
89//!         Effect::InvokeAgent { role, agent, prompt },
90//!         &mut ctx
91//!     )?;
92//!
93//!     assert!(matches!(
94//!         result.event,
95//!         PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
96//!     ));
97//! }
98//! ```
99//!
100//! # Module Organization
101//!
102//! - [`agent`] - Agent invocation and chain management
103//! - [`planning`] - Planning phase effects (prompt, XML, validation)
104//! - [`development`] - Development phase effects (iteration, continuation)
105//! - [`review`] - Review phase effects (issue detection, fix application)
106//! - [`commit`] - Commit phase effects (message generation, commit creation)
107//! - [`rebase`] - Rebase effects (conflict resolution, validation)
108//! - [`checkpoint`] - Checkpoint save/restore
109//! - [`context`] - Context preparation and cleanup
110//!
111//! [`docs/agents/workspace-trait.md`]: https://codeberg.org/mistlight/RalphWithReviewer/src/branch/main/docs/agents/workspace-trait.md
112
113mod agent;
114mod analysis;
115mod chain;
116mod checkpoint;
117mod commit;
118mod context;
119mod development;
120mod lifecycle;
121mod planning;
122mod rebase;
123mod retry_guidance;
124mod review;
125
126#[cfg(test)]
127mod tests;
128
129use crate::phases::PhaseContext;
130use crate::reducer::effect::{Effect, EffectHandler, EffectResult};
131use crate::reducer::event::{PipelineEvent, PipelinePhase};
132use crate::reducer::state::PipelineState;
133use crate::reducer::ui_event::UIEvent;
134use anyhow::Result;
135
136/// Main effect handler implementation.
137///
138/// This handler executes effects by calling pipeline subsystems and emitting reducer events.
139pub struct MainEffectHandler {
140    /// Current pipeline state
141    pub state: PipelineState,
142    /// Event log for replay/debugging
143    pub event_log: Vec<PipelineEvent>,
144}
145
146impl MainEffectHandler {
147    /// Create a new effect handler.
148    pub fn new(state: PipelineState) -> Self {
149        Self {
150            state,
151            event_log: Vec::new(),
152        }
153    }
154}
155
156impl<'ctx> EffectHandler<'ctx> for MainEffectHandler {
157    fn execute(&mut self, effect: Effect, ctx: &mut PhaseContext<'_>) -> Result<EffectResult> {
158        let result = self.execute_effect(effect, ctx)?;
159        self.event_log.push(result.event.clone());
160        self.event_log
161            .extend(result.additional_events.iter().cloned());
162        Ok(result)
163    }
164}
165
166impl crate::app::event_loop::StatefulHandler for MainEffectHandler {
167    fn update_state(&mut self, state: PipelineState) {
168        self.state = state;
169    }
170}
171
172impl MainEffectHandler {
173    /// Helper to create phase transition UI event.
174    fn phase_transition_ui(&self, to: PipelinePhase) -> UIEvent {
175        UIEvent::PhaseTransition {
176            from: Some(self.state.phase),
177            to,
178        }
179    }
180
181    fn write_completion_marker(ctx: &PhaseContext<'_>, content: &str, is_failure: bool) -> bool {
182        let marker_dir = std::path::Path::new(".agent/tmp");
183        if let Err(err) = ctx.workspace.create_dir_all(marker_dir) {
184            ctx.logger.warn(&format!(
185                "Failed to create completion marker directory: {}",
186                err
187            ));
188        }
189
190        let marker_path = std::path::Path::new(".agent/tmp/completion_marker");
191        match ctx.workspace.write(marker_path, content) {
192            Ok(()) => {
193                ctx.logger.info(&format!(
194                    "Completion marker written: {}",
195                    if is_failure { "failure" } else { "success" }
196                ));
197                true
198            }
199            Err(err) => {
200                ctx.logger
201                    .warn(&format!("Failed to write completion marker: {}", err));
202                false
203            }
204        }
205    }
206
207    fn execute_effect(
208        &mut self,
209        effect: Effect,
210        ctx: &mut PhaseContext<'_>,
211    ) -> Result<EffectResult> {
212        match effect {
213            Effect::AgentInvocation {
214                role,
215                agent,
216                model,
217                prompt,
218            } => self.invoke_agent(ctx, role, agent, model, prompt),
219
220            Effect::InitializeAgentChain { role } => self.initialize_agent_chain(ctx, role),
221
222            Effect::PreparePlanningPrompt {
223                iteration,
224                prompt_mode,
225            } => self.prepare_planning_prompt(ctx, iteration, prompt_mode),
226
227            Effect::MaterializePlanningInputs { iteration } => {
228                self.materialize_planning_inputs(ctx, iteration)
229            }
230
231            Effect::CleanupPlanningXml { iteration } => self.cleanup_planning_xml(ctx, iteration),
232
233            Effect::InvokePlanningAgent { iteration } => self.invoke_planning_agent(ctx, iteration),
234
235            Effect::ExtractPlanningXml { iteration } => self.extract_planning_xml(ctx, iteration),
236
237            Effect::ValidatePlanningXml { iteration } => self.validate_planning_xml(ctx, iteration),
238
239            Effect::WritePlanningMarkdown { iteration } => {
240                self.write_planning_markdown(ctx, iteration)
241            }
242
243            Effect::ArchivePlanningXml { iteration } => self.archive_planning_xml(ctx, iteration),
244
245            Effect::ApplyPlanningOutcome { iteration, valid } => {
246                self.apply_planning_outcome(ctx, iteration, valid)
247            }
248
249            Effect::PrepareDevelopmentContext { iteration } => {
250                self.prepare_development_context(ctx, iteration)
251            }
252
253            Effect::MaterializeDevelopmentInputs { iteration } => {
254                self.materialize_development_inputs(ctx, iteration)
255            }
256
257            Effect::PrepareDevelopmentPrompt {
258                iteration,
259                prompt_mode,
260            } => self.prepare_development_prompt(ctx, iteration, prompt_mode),
261
262            Effect::CleanupDevelopmentXml { iteration } => {
263                self.cleanup_development_xml(ctx, iteration)
264            }
265
266            Effect::InvokeDevelopmentAgent { iteration } => {
267                self.invoke_development_agent(ctx, iteration)
268            }
269
270            Effect::InvokeAnalysisAgent { iteration } => self.invoke_analysis_agent(ctx, iteration),
271
272            Effect::ExtractDevelopmentXml { iteration } => {
273                self.extract_development_xml(ctx, iteration)
274            }
275
276            Effect::ValidateDevelopmentXml { iteration } => {
277                self.validate_development_xml(ctx, iteration)
278            }
279
280            Effect::ApplyDevelopmentOutcome { iteration } => {
281                self.apply_development_outcome(ctx, iteration)
282            }
283
284            Effect::ArchiveDevelopmentXml { iteration } => {
285                self.archive_development_xml(ctx, iteration)
286            }
287
288            Effect::PrepareReviewContext { pass } => self.prepare_review_context(ctx, pass),
289
290            Effect::MaterializeReviewInputs { pass } => self.materialize_review_inputs(ctx, pass),
291
292            Effect::PrepareReviewPrompt { pass, prompt_mode } => {
293                self.prepare_review_prompt(ctx, pass, prompt_mode)
294            }
295
296            Effect::CleanupReviewIssuesXml { pass } => self.cleanup_review_issues_xml(ctx, pass),
297
298            Effect::InvokeReviewAgent { pass } => self.invoke_review_agent(ctx, pass),
299
300            Effect::ExtractReviewIssuesXml { pass } => self.extract_review_issues_xml(ctx, pass),
301
302            Effect::ValidateReviewIssuesXml { pass } => self.validate_review_issues_xml(ctx, pass),
303
304            Effect::WriteIssuesMarkdown { pass } => self.write_issues_markdown(ctx, pass),
305
306            Effect::ExtractReviewIssueSnippets { pass } => {
307                self.extract_review_issue_snippets(ctx, pass)
308            }
309
310            Effect::ArchiveReviewIssuesXml { pass } => self.archive_review_issues_xml(ctx, pass),
311
312            Effect::ApplyReviewOutcome {
313                pass,
314                issues_found,
315                clean_no_issues,
316            } => self.apply_review_outcome(ctx, pass, issues_found, clean_no_issues),
317
318            Effect::PrepareFixPrompt { pass, prompt_mode } => {
319                self.prepare_fix_prompt(ctx, pass, prompt_mode)
320            }
321
322            Effect::CleanupFixResultXml { pass } => self.cleanup_fix_result_xml(ctx, pass),
323
324            Effect::InvokeFixAgent { pass } => self.invoke_fix_agent(ctx, pass),
325
326            Effect::ExtractFixResultXml { pass } => self.extract_fix_result_xml(ctx, pass),
327
328            Effect::ValidateFixResultXml { pass } => self.validate_fix_result_xml(ctx, pass),
329
330            Effect::ApplyFixOutcome { pass } => self.apply_fix_outcome(ctx, pass),
331
332            Effect::ArchiveFixResultXml { pass } => self.archive_fix_result_xml(ctx, pass),
333
334            Effect::RunRebase {
335                phase,
336                target_branch,
337            } => self.run_rebase(ctx, phase, target_branch),
338
339            Effect::ResolveRebaseConflicts { strategy } => {
340                self.resolve_rebase_conflicts(ctx, strategy)
341            }
342
343            Effect::PrepareCommitPrompt { prompt_mode } => {
344                self.prepare_commit_prompt(ctx, prompt_mode)
345            }
346
347            Effect::CheckCommitDiff => self.check_commit_diff(ctx),
348
349            Effect::MaterializeCommitInputs { attempt } => {
350                self.materialize_commit_inputs(ctx, attempt)
351            }
352
353            Effect::InvokeCommitAgent => self.invoke_commit_agent(ctx),
354
355            Effect::CleanupCommitXml => self.cleanup_commit_xml(ctx),
356
357            Effect::ExtractCommitXml => self.extract_commit_xml(ctx),
358
359            Effect::ValidateCommitXml => self.validate_commit_xml(ctx),
360
361            Effect::ApplyCommitMessageOutcome => self.apply_commit_message_outcome(ctx),
362
363            Effect::ArchiveCommitXml => self.archive_commit_xml(ctx),
364
365            Effect::CreateCommit { message } => self.create_commit(ctx, message),
366
367            Effect::SkipCommit { reason } => self.skip_commit(ctx, reason),
368
369            Effect::BackoffWait {
370                role,
371                cycle,
372                duration_ms,
373            } => {
374                use std::time::Duration;
375                ctx.registry
376                    .retry_timer()
377                    .sleep(Duration::from_millis(duration_ms));
378                Ok(EffectResult::event(
379                    PipelineEvent::agent_retry_cycle_started(role, cycle),
380                ))
381            }
382
383            Effect::ReportAgentChainExhausted { role, phase, cycle } => {
384                use crate::reducer::event::ErrorEvent;
385                Err(ErrorEvent::AgentChainExhausted { role, phase, cycle }.into())
386            }
387
388            Effect::ValidateFinalState => self.validate_final_state(ctx),
389
390            Effect::SaveCheckpoint { trigger } => self.save_checkpoint(ctx, trigger),
391
392            Effect::EnsureGitignoreEntries => self.ensure_gitignore_entries(ctx),
393
394            Effect::CleanupContext => self.cleanup_context(ctx),
395
396            Effect::LockPromptPermissions => self.lock_prompt_permissions(ctx),
397
398            Effect::RestorePromptPermissions => self.restore_prompt_permissions(ctx),
399
400            Effect::WriteContinuationContext(ref data) => {
401                development::write_continuation_context_to_workspace(
402                    ctx.workspace,
403                    ctx.logger,
404                    data,
405                )?;
406                Ok(EffectResult::event(
407                    PipelineEvent::development_continuation_context_written(
408                        data.iteration,
409                        data.attempt,
410                    ),
411                ))
412            }
413
414            Effect::CleanupContinuationContext => self.cleanup_continuation_context(ctx),
415
416            Effect::TriggerLoopRecovery {
417                detected_loop,
418                loop_count,
419            } => self.trigger_loop_recovery(ctx, detected_loop, loop_count),
420
421            Effect::TriggerDevFixFlow {
422                failed_phase,
423                failed_role,
424                retry_cycle,
425            } => self.trigger_dev_fix_flow(ctx, failed_phase, failed_role, retry_cycle),
426
427            Effect::EmitCompletionMarkerAndTerminate { is_failure, reason } => {
428                Self::emit_completion_marker_and_terminate(ctx, is_failure, reason)
429            }
430        }
431    }
432}