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    #[must_use]
66    pub const fn from_io_error_kind(kind: std::io::ErrorKind) -> Self {
67        match kind {
68            std::io::ErrorKind::NotFound => Self::NotFound,
69            std::io::ErrorKind::PermissionDenied => Self::PermissionDenied,
70            std::io::ErrorKind::AlreadyExists => Self::AlreadyExists,
71            std::io::ErrorKind::InvalidData => Self::InvalidData,
72            _ => Self::Other,
73        }
74    }
75}
76
77/// Error events for failures requiring reducer handling.
78///
79/// Effect handlers communicate failures by returning `Err()` containing error events
80/// from this namespace. The event loop extracts these error events and feeds them to
81/// the reducer for processing, just like success events.
82///
83/// # Usage
84///
85/// Effect handlers return error events through `Err()`:
86/// ```ignore
87/// return Err(ErrorEvent::ReviewInputsNotMaterialized { pass }.into());
88/// ```
89///
90/// The event loop extracts the error event and feeds it to the reducer.
91///
92/// # Principles
93///
94/// 1. **Errors are events**: All `Err()` returns from effect handlers MUST contain
95///    events from this namespace, NOT strings.
96/// 2. **`Err()` is a carrier**: The `Err()` mechanism just carries error events to the
97///    event loop; it doesn't bypass the reducer.
98/// 3. **Reducer owns recovery**: The reducer processes error events identically to
99///    success events and decides recovery strategy.
100/// 4. **Typed, not strings**: String errors prevent the reducer from handling different
101///    failure modes appropriately.
102#[derive(Clone, Serialize, Deserialize, Debug)]
103pub enum ErrorEvent {
104    /// User requested interruption (Ctrl+C / SIGINT).
105    ///
106    /// This is an external termination request, not an internal pipeline failure.
107    /// The reducer transitions to `PipelinePhase::Interrupted` and sets
108    /// `interrupted_by_user=true` so orchestration can run termination effects
109    /// deterministically (`RestorePromptPermissions`, `SaveCheckpoint`) while skipping
110    /// the pre-termination commit safety check.
111    UserInterruptRequested,
112    /// Review inputs not materialized before `prepare_review_prompt`.
113    ///
114    /// This indicates an effect sequencing bug where `prepare_review_prompt` was called
115    /// without first materializing the review inputs via `materialize_review_inputs`.
116    ReviewInputsNotMaterialized {
117        /// The review pass number.
118        pass: u32,
119    },
120    /// Planning does not support continuation prompts.
121    ///
122    /// This is an invariant violation - continuation mode should not be passed to
123    /// the planning phase.
124    PlanningContinuationNotSupported,
125    /// Review does not support continuation prompts.
126    ///
127    /// This is an invariant violation - continuation mode should not be passed to
128    /// the review phase.
129    ReviewContinuationNotSupported,
130    /// Fix does not support continuation prompts.
131    ///
132    /// This is an invariant violation - continuation mode should not be passed to
133    /// the fix flow.
134    FixContinuationNotSupported,
135    /// Commit message generation does not support continuation prompts.
136    ///
137    /// This is an invariant violation - continuation mode should not be passed to
138    /// the commit phase.
139    CommitContinuationNotSupported,
140    /// Missing fix prompt file when invoking fix agent.
141    ///
142    /// This indicates an effect sequencing bug where `invoke_fix` was called without
143    /// first preparing the fix prompt file at .`agent/tmp/fix_prompt.txt`.
144    FixPromptMissing,
145
146    /// Agent chain exhausted for a phase.
147    ///
148    /// This indicates that all retry attempts have been exhausted for an agent chain
149    /// in a specific phase. The reducer decides whether to terminate the pipeline or
150    /// attempt recovery based on whether progress has been made.
151    AgentChainExhausted {
152        /// The role of the agent chain that was exhausted.
153        role: crate::agents::AgentRole,
154        /// The phase where exhaustion occurred.
155        phase: super::PipelinePhase,
156        /// The retry cycle number when exhaustion occurred.
157        cycle: u32,
158    },
159
160    /// Workspace read failure that must be handled by the reducer.
161    WorkspaceReadFailed {
162        /// Workspace-relative path.
163        path: String,
164        kind: WorkspaceIoErrorKind,
165    },
166    /// Workspace write failure that must be handled by the reducer.
167    WorkspaceWriteFailed {
168        /// Workspace-relative path.
169        path: String,
170        kind: WorkspaceIoErrorKind,
171    },
172    /// Workspace directory creation failure that must be handled by the reducer.
173    WorkspaceCreateDirAllFailed {
174        /// Workspace-relative path.
175        path: String,
176        kind: WorkspaceIoErrorKind,
177    },
178    /// Workspace remove failure that must be handled by the reducer.
179    WorkspaceRemoveFailed {
180        /// Workspace-relative path.
181        path: String,
182        kind: WorkspaceIoErrorKind,
183    },
184
185    /// Failed to stage changes before creating a commit.
186    ///
187    /// Commit creation requires staging (equivalent to `git add -A`). When this fails,
188    /// the error must flow through the reducer as a typed event so the reducer can
189    /// decide whether to retry, fallback, or terminate.
190    GitAddAllFailed { kind: WorkspaceIoErrorKind },
191
192    /// Failed to stage specific files before creating a commit.
193    ///
194    /// This corresponds to selective staging (equivalent to `git add <files>`).
195    /// When this fails, the error must flow through the reducer as a typed event so
196    /// the reducer can decide recovery strategy.
197    GitAddSpecificFailed { kind: WorkspaceIoErrorKind },
198
199    /// Failed to get git status (for pre-termination check).
200    ///
201    /// When checking for uncommitted changes before termination, if `git status` fails,
202    /// this error is raised so the reducer can decide how to handle it.
203    GitStatusFailed { kind: WorkspaceIoErrorKind },
204
205    /// Agent registry lookup failed (unknown agent).
206    AgentNotFound { agent: String },
207
208    /// Planning inputs not materialized before preparing/invoking planning prompt.
209    PlanningInputsNotMaterialized { iteration: u32 },
210    /// Development inputs not materialized before preparing/invoking development prompt.
211    DevelopmentInputsNotMaterialized { iteration: u32 },
212    /// Commit inputs not materialized before preparing commit prompt.
213    CommitInputsNotMaterialized { attempt: u32 },
214
215    /// Prepared planning prompt file missing/unreadable when invoking planning agent.
216    PlanningPromptMissing { iteration: u32 },
217    /// Prepared development prompt file missing/unreadable when invoking development agent.
218    DevelopmentPromptMissing { iteration: u32 },
219    /// Prepared review prompt file missing/unreadable when invoking review agent.
220    ReviewPromptMissing { pass: u32 },
221    /// Prepared commit prompt file missing/unreadable when invoking commit agent.
222    CommitPromptMissing { attempt: u32 },
223
224    /// Commit agent chain not initialized when invoking commit agent.
225    ///
226    /// This is an invariant violation: `InitializeAgentChain` must run before invoking.
227    /// Effect handlers must surface this as a typed error event (never panic) so the
228    /// reducer can deterministically interrupt/checkpoint.
229    CommitAgentNotInitialized { attempt: u32 },
230
231    /// Missing validated planning markdown when writing `.agent/PLAN.md`.
232    ValidatedPlanningMarkdownMissing { iteration: u32 },
233    /// Missing validated development outcome when applying/writing results.
234    ValidatedDevelopmentOutcomeMissing { iteration: u32 },
235    /// Missing validated review outcome when applying/writing results.
236    ValidatedReviewOutcomeMissing { pass: u32 },
237    /// Missing validated fix outcome when applying fixes.
238    ValidatedFixOutcomeMissing { pass: u32 },
239    /// Missing validated commit outcome when applying commit message outcome.
240    ValidatedCommitOutcomeMissing { attempt: u32 },
241}
242
243impl std::fmt::Display for ErrorEvent {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            Self::UserInterruptRequested => {
247                write!(f, "User interrupt requested (SIGINT / Ctrl+C)")
248            }
249            Self::ReviewInputsNotMaterialized { pass } => {
250                write!(
251                    f,
252                    "Review inputs not materialized for pass {pass} (expected materialize_review_inputs before prepare_review_prompt)"
253                )
254            }
255            Self::PlanningContinuationNotSupported => {
256                write!(f, "Planning does not support continuation prompts")
257            }
258            Self::ReviewContinuationNotSupported => {
259                write!(f, "Review does not support continuation prompts")
260            }
261            Self::FixContinuationNotSupported => {
262                write!(f, "Fix does not support continuation prompts")
263            }
264            Self::CommitContinuationNotSupported => {
265                write!(
266                    f,
267                    "Commit message generation does not support continuation prompts"
268                )
269            }
270            Self::FixPromptMissing => {
271                write!(f, "Missing fix prompt at .agent/tmp/fix_prompt.txt")
272            }
273            Self::AgentChainExhausted { role, phase, cycle } => {
274                write!(
275                    f,
276                    "Agent chain exhausted for role {role:?} in phase {phase:?} (cycle {cycle})"
277                )
278            }
279            Self::WorkspaceReadFailed { path, kind } => {
280                write!(f, "Workspace read failed at {path} ({kind:?})")
281            }
282            Self::WorkspaceWriteFailed { path, kind } => {
283                write!(f, "Workspace write failed at {path} ({kind:?})")
284            }
285            Self::WorkspaceCreateDirAllFailed { path, kind } => {
286                write!(f, "Workspace create_dir_all failed at {path} ({kind:?})")
287            }
288            Self::WorkspaceRemoveFailed { path, kind } => {
289                write!(f, "Workspace remove failed at {path} ({kind:?})")
290            }
291            Self::GitAddAllFailed { kind } => {
292                write!(f, "git add -A (stage all changes) failed ({kind:?})")
293            }
294            Self::GitAddSpecificFailed { kind } => {
295                write!(
296                    f,
297                    "git add <files> (stage specific paths) failed ({kind:?})"
298                )
299            }
300            Self::GitStatusFailed { kind } => {
301                write!(f, "git status (pre-termination check) failed ({kind:?})")
302            }
303            Self::AgentNotFound { agent } => {
304                write!(f, "Agent not found: {agent}")
305            }
306            Self::PlanningInputsNotMaterialized { iteration } => {
307                write!(
308                    f,
309                    "Planning inputs not materialized for iteration {iteration} (expected materialize_planning_inputs before prepare/invoke)"
310                )
311            }
312            Self::DevelopmentInputsNotMaterialized { iteration } => {
313                write!(
314                    f,
315                    "Development inputs not materialized for iteration {iteration} (expected materialize_development_inputs before prepare/invoke)"
316                )
317            }
318            Self::CommitInputsNotMaterialized { attempt } => {
319                write!(
320                    f,
321                    "Commit inputs not materialized for attempt {attempt} (expected materialize_commit_inputs before prepare)"
322                )
323            }
324            Self::PlanningPromptMissing { iteration } => {
325                write!(
326                    f,
327                    "Missing planning prompt at .agent/tmp/planning_prompt.txt for iteration {iteration}"
328                )
329            }
330            Self::DevelopmentPromptMissing { iteration } => {
331                write!(
332                    f,
333                    "Missing development prompt at .agent/tmp/development_prompt.txt for iteration {iteration}"
334                )
335            }
336            Self::ReviewPromptMissing { pass } => {
337                write!(
338                    f,
339                    "Missing review prompt at .agent/tmp/review_prompt.txt for pass {pass}"
340                )
341            }
342            Self::CommitPromptMissing { attempt } => {
343                write!(
344                    f,
345                    "Missing commit prompt at .agent/tmp/commit_prompt.txt for attempt {attempt}"
346                )
347            }
348            Self::CommitAgentNotInitialized { attempt } => {
349                write!(
350                    f,
351                    "Commit agent not initialized for attempt {attempt} (expected InitializeAgentChain before invoke_commit_agent)"
352                )
353            }
354            Self::ValidatedPlanningMarkdownMissing { iteration } => {
355                write!(
356                    f,
357                    "Missing validated planning markdown for iteration {iteration}"
358                )
359            }
360            Self::ValidatedDevelopmentOutcomeMissing { iteration } => {
361                write!(
362                    f,
363                    "Missing validated development outcome for iteration {iteration}"
364                )
365            }
366            Self::ValidatedReviewOutcomeMissing { pass } => {
367                write!(f, "Missing validated review outcome for pass {pass}")
368            }
369            Self::ValidatedFixOutcomeMissing { pass } => {
370                write!(f, "Missing validated fix outcome for pass {pass}")
371            }
372            Self::ValidatedCommitOutcomeMissing { attempt } => {
373                write!(f, "Missing validated commit outcome for attempt {attempt}")
374            }
375        }
376    }
377}
378
379impl std::error::Error for ErrorEvent {}
380
381// Note: From<ErrorEvent> for anyhow::Error is provided by anyhow's blanket implementation
382// for all types that implement std::error::Error + Send + Sync + 'static.
383// This automatically preserves ErrorEvent as the error source for downcasting.