Skip to main content

ralph_workflow/reducer/event/
error.rs

1//! Error events for recoverable and unrecoverable failures.
2//!
3//! This module implements the error event pattern where ALL errors from effect handlers
4//! are represented as typed events that flow through the reducer, enabling the reducer
5//! to decide recovery strategy based on semantic meaning.
6//!
7//! # Error Handling Architecture
8//!
9//! ## The Pattern
10//!
11//! 1. **Effect handler encounters error**
12//!    ```rust,ignore
13//!    return Err(ErrorEvent::ReviewInputsNotMaterialized { pass }.into());
14//!    ```
15//!
16//! 2. **Event loop extracts error event**
17//!    The event loop catches `Err()`, downcasts to `ErrorEvent`, and re-emits it as
18//!    `PipelineEvent::PromptInput(PromptInputEvent::HandlerError { ... })` so the
19//!    reducer can decide recovery strategy without adding new top-level `PipelineEvent`
20//!    variants.
21//!
22//! 3. **Reducer decides recovery strategy**
23//!    The reducer processes the error identically to other events (it is still routed
24//!    through the main `reduce` function), deciding whether to retry, fallback, skip,
25//!    or terminate based on the specific error variant.
26//!
27//! 4. **Event loop acts on reducer decision**
28//!    If the reducer transitions to Interrupted phase, the event loop terminates.
29//!    Otherwise, execution continues with the next effect (e.g., by clearing a
30//!    "prepared" flag to force re-materialization after a checkpoint resume).
31//!
32//! ## Why Not String Errors?
33//!
34//! String errors (`Err(anyhow::anyhow!("..."))`) would bypass the reducer and prevent
35//! recovery logic. The reducer cannot distinguish between "missing optional file"
36//! (use fallback) and "permission denied" (abort pipeline) when errors are strings.
37//!
38//! ## Current Error Categories
39//!
40//! All current error events represent **invariant violations** (effect sequencing bugs,
41//! continuation mode misuse) or **terminal conditions** (agent chain exhaustion). These
42//! terminate the pipeline because they indicate bugs in the orchestration logic or
43//! exhaustion of all retry attempts.
44//!
45//! Future error events for recoverable conditions (network timeouts, transient file I/O)
46//! will implement retry/fallback strategies in the reducer.
47
48use serde::{Deserialize, Serialize};
49
50/// Serializable subset of `std::io::ErrorKind`.
51///
52/// `std::io::Error` / `ErrorKind` are not serde-serializable, but reducer error events
53/// must be persisted in checkpoints. This enum captures the subset of error kinds we
54/// need for recovery policy decisions.
55#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
56pub enum WorkspaceIoErrorKind {
57    NotFound,
58    PermissionDenied,
59    AlreadyExists,
60    InvalidData,
61    Other,
62}
63
64impl WorkspaceIoErrorKind {
65    pub fn from_io_error_kind(kind: std::io::ErrorKind) -> Self {
66        match kind {
67            std::io::ErrorKind::NotFound => Self::NotFound,
68            std::io::ErrorKind::PermissionDenied => Self::PermissionDenied,
69            std::io::ErrorKind::AlreadyExists => Self::AlreadyExists,
70            std::io::ErrorKind::InvalidData => Self::InvalidData,
71            _ => Self::Other,
72        }
73    }
74}
75
76/// Error events for failures requiring reducer handling.
77///
78/// Effect handlers communicate failures by returning `Err()` containing error events
79/// from this namespace. The event loop extracts these error events and feeds them to
80/// the reducer for processing, just like success events.
81///
82/// # Usage
83///
84/// Effect handlers return error events through `Err()`:
85/// ```ignore
86/// return Err(ErrorEvent::ReviewInputsNotMaterialized { pass }.into());
87/// ```
88///
89/// The event loop extracts the error event and feeds it to the reducer.
90///
91/// # Principles
92///
93/// 1. **Errors are events**: All `Err()` returns from effect handlers MUST contain
94///    events from this namespace, NOT strings.
95/// 2. **Err() is a carrier**: The `Err()` mechanism just carries error events to the
96///    event loop; it doesn't bypass the reducer.
97/// 3. **Reducer owns recovery**: The reducer processes error events identically to
98///    success events and decides recovery strategy.
99/// 4. **Typed, not strings**: String errors prevent the reducer from handling different
100///    failure modes appropriately.
101#[derive(Clone, Serialize, Deserialize, Debug)]
102pub enum ErrorEvent {
103    /// Review inputs not materialized before prepare_review_prompt.
104    ///
105    /// This indicates an effect sequencing bug where prepare_review_prompt was called
106    /// without first materializing the review inputs via materialize_review_inputs.
107    ReviewInputsNotMaterialized {
108        /// The review pass number.
109        pass: u32,
110    },
111    /// Planning does not support continuation prompts.
112    ///
113    /// This is an invariant violation - continuation mode should not be passed to
114    /// the planning phase.
115    PlanningContinuationNotSupported,
116    /// Review does not support continuation prompts.
117    ///
118    /// This is an invariant violation - continuation mode should not be passed to
119    /// the review phase.
120    ReviewContinuationNotSupported,
121    /// Fix does not support continuation prompts.
122    ///
123    /// This is an invariant violation - continuation mode should not be passed to
124    /// the fix flow.
125    FixContinuationNotSupported,
126    /// Commit message generation does not support continuation prompts.
127    ///
128    /// This is an invariant violation - continuation mode should not be passed to
129    /// the commit phase.
130    CommitContinuationNotSupported,
131    /// Missing fix prompt file when invoking fix agent.
132    ///
133    /// This indicates an effect sequencing bug where invoke_fix was called without
134    /// first preparing the fix prompt file at .agent/tmp/fix_prompt.txt.
135    FixPromptMissing,
136
137    /// Agent chain exhausted for a phase.
138    ///
139    /// This indicates that all retry attempts have been exhausted for an agent chain
140    /// in a specific phase. The reducer decides whether to terminate the pipeline or
141    /// attempt recovery based on whether progress has been made.
142    AgentChainExhausted {
143        /// The role of the agent chain that was exhausted.
144        role: crate::agents::AgentRole,
145        /// The phase where exhaustion occurred.
146        phase: super::PipelinePhase,
147        /// The retry cycle number when exhaustion occurred.
148        cycle: u32,
149    },
150
151    /// Workspace read failure that must be handled by the reducer.
152    WorkspaceReadFailed {
153        /// Workspace-relative path.
154        path: String,
155        kind: WorkspaceIoErrorKind,
156    },
157    /// Workspace write failure that must be handled by the reducer.
158    WorkspaceWriteFailed {
159        /// Workspace-relative path.
160        path: String,
161        kind: WorkspaceIoErrorKind,
162    },
163    /// Workspace directory creation failure that must be handled by the reducer.
164    WorkspaceCreateDirAllFailed {
165        /// Workspace-relative path.
166        path: String,
167        kind: WorkspaceIoErrorKind,
168    },
169    /// Workspace remove failure that must be handled by the reducer.
170    WorkspaceRemoveFailed {
171        /// Workspace-relative path.
172        path: String,
173        kind: WorkspaceIoErrorKind,
174    },
175
176    /// Failed to stage changes before creating a commit.
177    ///
178    /// Commit creation requires staging (equivalent to `git add -A`). When this fails,
179    /// the error must flow through the reducer as a typed event so the reducer can
180    /// decide whether to retry, fallback, or terminate.
181    GitAddAllFailed { kind: WorkspaceIoErrorKind },
182
183    /// Agent registry lookup failed (unknown agent).
184    AgentNotFound { agent: String },
185
186    /// Planning inputs not materialized before preparing/invoking planning prompt.
187    PlanningInputsNotMaterialized { iteration: u32 },
188    /// Development inputs not materialized before preparing/invoking development prompt.
189    DevelopmentInputsNotMaterialized { iteration: u32 },
190    /// Commit inputs not materialized before preparing commit prompt.
191    CommitInputsNotMaterialized { attempt: u32 },
192
193    /// Prepared planning prompt file missing/unreadable when invoking planning agent.
194    PlanningPromptMissing { iteration: u32 },
195    /// Prepared development prompt file missing/unreadable when invoking development agent.
196    DevelopmentPromptMissing { iteration: u32 },
197    /// Prepared review prompt file missing/unreadable when invoking review agent.
198    ReviewPromptMissing { pass: u32 },
199    /// Prepared commit prompt file missing/unreadable when invoking commit agent.
200    CommitPromptMissing { attempt: u32 },
201
202    /// Commit agent chain not initialized when invoking commit agent.
203    ///
204    /// This is an invariant violation: InitializeAgentChain must run before invoking.
205    /// Effect handlers must surface this as a typed error event (never panic) so the
206    /// reducer can deterministically interrupt/checkpoint.
207    CommitAgentNotInitialized { attempt: u32 },
208
209    /// Missing validated planning markdown when writing `.agent/PLAN.md`.
210    ValidatedPlanningMarkdownMissing { iteration: u32 },
211    /// Missing validated development outcome when applying/writing results.
212    ValidatedDevelopmentOutcomeMissing { iteration: u32 },
213    /// Missing validated review outcome when applying/writing results.
214    ValidatedReviewOutcomeMissing { pass: u32 },
215    /// Missing validated fix outcome when applying fixes.
216    ValidatedFixOutcomeMissing { pass: u32 },
217    /// Missing validated commit outcome when applying commit message outcome.
218    ValidatedCommitOutcomeMissing { attempt: u32 },
219}
220
221impl std::fmt::Display for ErrorEvent {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        match self {
224            ErrorEvent::ReviewInputsNotMaterialized { pass } => {
225                write!(
226                    f,
227                    "Review inputs not materialized for pass {pass} (expected materialize_review_inputs before prepare_review_prompt)"
228                )
229            }
230            ErrorEvent::PlanningContinuationNotSupported => {
231                write!(f, "Planning does not support continuation prompts")
232            }
233            ErrorEvent::ReviewContinuationNotSupported => {
234                write!(f, "Review does not support continuation prompts")
235            }
236            ErrorEvent::FixContinuationNotSupported => {
237                write!(f, "Fix does not support continuation prompts")
238            }
239            ErrorEvent::CommitContinuationNotSupported => {
240                write!(
241                    f,
242                    "Commit message generation does not support continuation prompts"
243                )
244            }
245            ErrorEvent::FixPromptMissing => {
246                write!(f, "Missing fix prompt at .agent/tmp/fix_prompt.txt")
247            }
248            ErrorEvent::AgentChainExhausted { role, phase, cycle } => {
249                write!(
250                    f,
251                    "Agent chain exhausted for role {:?} in phase {:?} (cycle {})",
252                    role, phase, cycle
253                )
254            }
255            ErrorEvent::WorkspaceReadFailed { path, kind } => {
256                write!(f, "Workspace read failed at {path} ({kind:?})")
257            }
258            ErrorEvent::WorkspaceWriteFailed { path, kind } => {
259                write!(f, "Workspace write failed at {path} ({kind:?})")
260            }
261            ErrorEvent::WorkspaceCreateDirAllFailed { path, kind } => {
262                write!(f, "Workspace create_dir_all failed at {path} ({kind:?})")
263            }
264            ErrorEvent::WorkspaceRemoveFailed { path, kind } => {
265                write!(f, "Workspace remove failed at {path} ({kind:?})")
266            }
267            ErrorEvent::GitAddAllFailed { kind } => {
268                write!(f, "git add -A (stage all changes) failed ({kind:?})")
269            }
270            ErrorEvent::AgentNotFound { agent } => {
271                write!(f, "Agent not found: {agent}")
272            }
273            ErrorEvent::PlanningInputsNotMaterialized { iteration } => {
274                write!(
275                    f,
276                    "Planning inputs not materialized for iteration {iteration} (expected materialize_planning_inputs before prepare/invoke)"
277                )
278            }
279            ErrorEvent::DevelopmentInputsNotMaterialized { iteration } => {
280                write!(
281                    f,
282                    "Development inputs not materialized for iteration {iteration} (expected materialize_development_inputs before prepare/invoke)"
283                )
284            }
285            ErrorEvent::CommitInputsNotMaterialized { attempt } => {
286                write!(
287                    f,
288                    "Commit inputs not materialized for attempt {attempt} (expected materialize_commit_inputs before prepare)"
289                )
290            }
291            ErrorEvent::PlanningPromptMissing { iteration } => {
292                write!(
293                    f,
294                    "Missing planning prompt at .agent/tmp/planning_prompt.txt for iteration {iteration}"
295                )
296            }
297            ErrorEvent::DevelopmentPromptMissing { iteration } => {
298                write!(
299                    f,
300                    "Missing development prompt at .agent/tmp/development_prompt.txt for iteration {iteration}"
301                )
302            }
303            ErrorEvent::ReviewPromptMissing { pass } => {
304                write!(
305                    f,
306                    "Missing review prompt at .agent/tmp/review_prompt.txt for pass {pass}"
307                )
308            }
309            ErrorEvent::CommitPromptMissing { attempt } => {
310                write!(
311                    f,
312                    "Missing commit prompt at .agent/tmp/commit_prompt.txt for attempt {attempt}"
313                )
314            }
315            ErrorEvent::CommitAgentNotInitialized { attempt } => {
316                write!(
317                    f,
318                    "Commit agent not initialized for attempt {attempt} (expected InitializeAgentChain before invoke_commit_agent)"
319                )
320            }
321            ErrorEvent::ValidatedPlanningMarkdownMissing { iteration } => {
322                write!(
323                    f,
324                    "Missing validated planning markdown for iteration {iteration}"
325                )
326            }
327            ErrorEvent::ValidatedDevelopmentOutcomeMissing { iteration } => {
328                write!(
329                    f,
330                    "Missing validated development outcome for iteration {iteration}"
331                )
332            }
333            ErrorEvent::ValidatedReviewOutcomeMissing { pass } => {
334                write!(f, "Missing validated review outcome for pass {pass}")
335            }
336            ErrorEvent::ValidatedFixOutcomeMissing { pass } => {
337                write!(f, "Missing validated fix outcome for pass {pass}")
338            }
339            ErrorEvent::ValidatedCommitOutcomeMissing { attempt } => {
340                write!(f, "Missing validated commit outcome for attempt {attempt}")
341            }
342        }
343    }
344}
345
346impl std::error::Error for ErrorEvent {}
347
348// Note: From<ErrorEvent> for anyhow::Error is provided by anyhow's blanket implementation
349// for all types that implement std::error::Error + Send + Sync + 'static.
350// This automatically preserves ErrorEvent as the error source for downcasting.