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