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.