Skip to main content

ralph_workflow/reducer/event/
mod.rs

1//! Pipeline event types for reducer architecture.
2//!
3//! Defines all possible events that can occur during pipeline execution.
4//! Each event represents a **fact** about what happened, not a command about
5//! what to do next.
6//!
7//! # Event-Sourced Reducer Architecture
8//!
9//! Ralph's pipeline follows an event-sourced reducer pattern:
10//!
11//! ```text
12//! State → Orchestrate → Effect → Handle → Event → Reduce → State'
13//! ```
14//!
15//! ## The Core Contract
16//!
17//! | Component | Pure? | Responsibility |
18//! |-----------|-------|----------------|
19//! | **Orchestration** | ✓ | Derives next effect from state only |
20//! | **Handler** | ✗ | Executes effect, emits events describing outcome |
21//! | **Reducer** | ✓ | Decides new state based on event |
22//!
23//! **Events are facts, not commands:**
24//!
25//! ```rust,ignore
26//! // ✓ GOOD - Describes what happened (fact)
27//! AgentEvent::InvocationFailed { retriable: true, error }
28//!
29//! // ✗ BAD - Commands what to do next (decision)
30//! AgentEvent::RetryAgent { with_backoff: true }
31//! ```
32//!
33//! **The handler reports, the reducer decides:**
34//!
35//! - Handler: "Agent invocation failed (retriable)"
36//! - Reducer: "Increment retry count, stay in same phase"
37//! - Orchestration: "Retry count < max? Emit InvokeAgent effect"
38//!
39//! # Event Categories
40//!
41//! Events are organized into logical categories for type-safe routing to
42//! category-specific reducers. Each category has a dedicated enum:
43//!
44//! - [`LifecycleEvent`] - Pipeline start/stop/resume
45//! - [`PlanningEvent`] - Plan generation events
46//! - [`DevelopmentEvent`] - Development iteration and continuation events
47//! - [`ReviewEvent`] - Review pass and fix attempt events
48//! - [`AgentEvent`] - Agent invocation and chain management events
49//! - [`RebaseEvent`] - Git rebase operation events
50//! - [`CommitEvent`] - Commit generation events
51//! - [`AwaitingDevFixEvent`] - Dev-fix flow events
52//! - [`PromptInputEvent`] - Prompt materialization events
53//! - [`ErrorEvent`] - Typed error events from handlers
54//!
55//! The main [`PipelineEvent`] enum wraps these category enums to enable
56//! type-safe dispatch in the reducer.
57//!
58//! # Why This File Is Large (514 lines)
59//!
60//! This file exceeds the 500-line recommended limit (currently 514 lines) but is an acceptable
61//! exception to the 300-line guideline because it's a **comprehensive enum module** with 10+ event
62//! category types that must remain together for:
63//! - Type-safe event category dispatch in reducers
64//! - Exhaustiveness checking across all event variants
65//! - Single source of truth for the event vocabulary
66//!
67//! Splitting would break pattern matching and scatter the event contract across many files, which
68//! would be significantly worse for maintainability than the current size. The tradeoff of exceeding
69//! the 500-line limit is justified by the cohesion and type-safety benefits.
70//!
71//! # Module Organization
72//!
73//! - [`types`] - Core event type definitions (all event enums)
74//! - [`constructors`] - Convenience constructors for building events
75//! - `development` - DevelopmentEvent and constructors
76//! - `review` - ReviewEvent and constructors
77//! - `agent` - AgentEvent and constructors
78//! - `error` - ErrorEvent and error types
79//!
80//! # Example: Handler Emitting Events
81//!
82//! ```rust,ignore
83//! use ralph_workflow::reducer::event::{PipelineEvent, AgentEvent};
84//! use ralph_workflow::reducer::effect::EffectResult;
85//!
86//! fn handle_invoke_agent(ctx: &mut PhaseContext) -> Result<EffectResult> {
87//!     match invoke_agent_process(ctx) {
88//!         Ok(output) => {
89//!             // Report fact: invocation succeeded
90//!             Ok(EffectResult::event(PipelineEvent::Agent(
91//!                 AgentEvent::InvocationSucceeded {
92//!                     role: ctx.role,
93//!                     output,
94//!                 }
95//!             )))
96//!         }
97//!         Err(e) if is_retriable(&e) => {
98//!             // Report fact: invocation failed (retriable)
99//!             Ok(EffectResult::event(PipelineEvent::Agent(
100//!                 AgentEvent::InvocationFailed {
101//!                     role: ctx.role,
102//!                     error: e.to_string(),
103//!                     retriable: true,
104//!                 }
105//!             )))
106//!         }
107//!         Err(e) => {
108//!             // Report fact: invocation failed (not retriable)
109//!             Ok(EffectResult::event(PipelineEvent::Agent(
110//!                 AgentEvent::InvocationFailed {
111//!                     role: ctx.role,
112//!                     error: e.to_string(),
113//!                     retriable: false,
114//!                 }
115//!             )))
116//!         }
117//!     }
118//! }
119//! ```
120//!
121//! # Example: Reducer Making Decisions
122//!
123//! ```rust,ignore
124//! use ralph_workflow::reducer::event::{PipelineEvent, AgentEvent};
125//! use ralph_workflow::reducer::state::PipelineState;
126//!
127//! fn reduce(state: PipelineState, event: PipelineEvent) -> PipelineState {
128//!     match event {
129//!         PipelineEvent::Agent(AgentEvent::InvocationFailed { retriable, .. }) => {
130//!             if retriable && state.retry_count < state.max_retries {
131//!                 // Decision: retry same agent
132//!                 PipelineState {
133//!                     retry_count: state.retry_count + 1,
134//!                     ..state
135//!                 }
136//!             } else if retriable {
137//!                 // Decision: switch to next agent in chain
138//!                 PipelineState {
139//!                     agent_chain_index: state.agent_chain_index + 1,
140//!                     retry_count: 0,
141//!                     ..state
142//!                 }
143//!             } else {
144//!                 // Decision: non-retriable failure, transition to AwaitingDevFix
145//!                 PipelineState {
146//!                     phase: PipelinePhase::AwaitingDevFix,
147//!                     ..state
148//!                 }
149//!             }
150//!         }
151//!         _ => state,
152//!     }
153//! }
154//! ```
155//!
156//! # Frozen Policy
157//!
158//! Both [`LifecycleEvent`] and [`PipelineEvent`] are **FROZEN** - adding new variants
159//! is prohibited. See their documentation for rationale and alternatives.
160//!
161//! # See Also
162//!
163//! - `docs/architecture/event-loop-and-reducers.md` - Detailed architecture doc
164//! - `reducer::state_reduction` - Reducer implementations
165//! - `reducer::orchestration` - Effect orchestration logic
166//! - `reducer::handler` - Effect handler implementations
167
168use crate::agents::AgentRole;
169use crate::reducer::state::DevelopmentStatus;
170use serde::{Deserialize, Serialize};
171use std::path::PathBuf;
172
173// ============================================================================
174// Type Definitions Module
175// ============================================================================
176
177#[path = "types.rs"]
178mod types;
179
180// Re-export all type definitions
181pub use types::{
182    AgentErrorKind, AwaitingDevFixEvent, CheckpointTrigger, CommitEvent, ConflictStrategy,
183    LifecycleEvent, MaterializedPromptInput, PlanningEvent, PromptInputEvent, PromptInputKind,
184    RebaseEvent, RebasePhase,
185};
186
187// ============================================================================
188// Category Event Modules
189// ============================================================================
190
191#[path = "development.rs"]
192mod development;
193pub use development::DevelopmentEvent;
194
195#[path = "review.rs"]
196mod review;
197pub use review::ReviewEvent;
198
199#[path = "agent.rs"]
200mod agent;
201pub use agent::AgentEvent;
202
203#[path = "error.rs"]
204mod error;
205pub use error::ErrorEvent;
206pub use error::WorkspaceIoErrorKind;
207
208// ============================================================================
209// Constructor Module
210// ============================================================================
211
212#[path = "constructors.rs"]
213mod constructors;
214
215// ============================================================================
216// Main Event Enum and Supporting Types
217// ============================================================================
218
219/// Pipeline phases for checkpoint tracking.
220///
221/// These phases represent the major stages of the Ralph pipeline.
222/// Reducers transition between phases based on events.
223///
224/// # Phase Transitions
225///
226/// ```text
227/// Planning → Development → Review → CommitMessage → FinalValidation → Finalizing → Complete
228///              ↓             ↓            ↓
229///         AwaitingDevFix → Interrupted
230/// ```
231///
232/// # Phase Descriptions
233///
234/// - **Planning**: Generate implementation plan for the iteration
235/// - **Development**: Execute plan, write code
236/// - **Review**: Review code changes, identify issues
237/// - **CommitMessage**: Generate commit message
238/// - **FinalValidation**: Final checks before completion
239/// - **Finalizing**: Cleanup operations (restore permissions, etc.)
240/// - **Complete**: Pipeline completed successfully
241/// - **AwaitingDevFix**: Terminal failure occurred, dev agent diagnosing
242/// - **Interrupted**: Pipeline terminated (success or failure)
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244pub enum PipelinePhase {
245    Planning,
246    Development,
247    Review,
248    CommitMessage,
249    FinalValidation,
250    /// Finalizing phase for cleanup operations before completion.
251    ///
252    /// This phase handles:
253    /// - Restoring PROMPT.md write permissions
254    /// - Any other cleanup that must go through the effect system
255    Finalizing,
256    Complete,
257    /// Awaiting development agent to fix pipeline failure.
258    ///
259    /// This phase occurs when the pipeline encounters a terminal failure condition
260    /// (e.g., agent chain exhausted) but before transitioning to Interrupted. It
261    /// signals that the development agent should be invoked to diagnose and fix
262    /// the failure root cause.
263    ///
264    /// ## Failure Handling Flow
265    ///
266    /// 1. ErrorEvent::AgentChainExhausted occurs in any phase
267    /// 2. Reducer transitions state to AwaitingDevFix
268    /// 3. Orchestration determines Effect::TriggerDevFixFlow
269    /// 4. Handler executes TriggerDevFixFlow:
270    ///    a. Writes completion marker to .agent/tmp/completion_marker (failure status)
271    ///    b. Emits DevFixTriggered event
272    ///    c. Dispatches dev-fix agent
273    ///    d. Emits DevFixCompleted event
274    ///    e. Emits CompletionMarkerEmitted event
275    /// 5. DevFixTriggered/DevFixCompleted events: no state change (stays in AwaitingDevFix)
276    /// 6. CompletionMarkerEmitted event: transitions to Interrupted
277    /// 7. Orchestration determines Effect::SaveCheckpoint for Interrupted
278    /// 8. Handler saves checkpoint, increments checkpoint_saved_count
279    /// 9. Event loop recognizes is_complete() == true and exits successfully
280    ///
281    /// ## Event Loop Termination Guarantees
282    ///
283    /// The event loop MUST NOT exit with completed=false when in AwaitingDevFix phase.
284    /// The failure handling flow is designed to always complete with:
285    /// - Completion marker written to filesystem
286    /// - State transitioned to Interrupted
287    /// - Checkpoint saved (checkpoint_saved_count > 0)
288    /// - Event loop returning completed=true
289    ///
290    /// If the event loop exits with completed=false from AwaitingDevFix, this indicates
291    /// a critical bug (e.g., max iterations reached before checkpoint saved).
292    ///
293    /// ## Completion Marker Requirement
294    ///
295    /// The completion marker MUST be written before transitioning to Interrupted.
296    /// This ensures external orchestration systems (CI, monitoring) can detect
297    /// pipeline termination even if the event loop exits unexpectedly.
298    ///
299    /// ## Agent Chain Exhaustion Handling
300    ///
301    /// When in AwaitingDevFix phase with an exhausted agent chain, orchestration
302    /// falls through to phase-specific logic (TriggerDevFixFlow) instead of reporting
303    /// exhaustion again. This prevents infinite loops where exhaustion is reported
304    /// repeatedly.
305    ///
306    /// Transitions:
307    /// - From: Any phase where AgentChainExhausted error occurs
308    /// - To: Interrupted (after dev-fix attempt completes or fails)
309    AwaitingDevFix,
310    Interrupted,
311}
312
313impl std::fmt::Display for PipelinePhase {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        match self {
316            Self::Planning => write!(f, "Planning"),
317            Self::Development => write!(f, "Development"),
318            Self::Review => write!(f, "Review"),
319            Self::CommitMessage => write!(f, "Commit Message"),
320            Self::FinalValidation => write!(f, "Final Validation"),
321            Self::Finalizing => write!(f, "Finalizing"),
322            Self::Complete => write!(f, "Complete"),
323            Self::AwaitingDevFix => write!(f, "Awaiting Dev Fix"),
324            Self::Interrupted => write!(f, "Interrupted"),
325        }
326    }
327}
328
329/// Pipeline events representing all state transitions.
330///
331/// Events are organized into logical categories for type-safe routing
332/// to category-specific reducers. Each category has a dedicated inner enum.
333///
334/// # Event Categories
335///
336/// - `Lifecycle` - Pipeline start/stop/resume
337/// - `Planning` - Plan generation events
338/// - `Development` - Development iteration and continuation events
339/// - `Review` - Review pass and fix attempt events
340/// - `Agent` - Agent invocation and chain management events
341/// - `Rebase` - Git rebase operation events
342/// - `Commit` - Commit generation events
343/// - Miscellaneous events (context cleanup, checkpoints, finalization)
344///
345/// # Example
346///
347/// ```rust,ignore
348/// // Type-safe event construction
349/// let event = PipelineEvent::Agent(AgentEvent::InvocationStarted {
350///     role: AgentRole::Developer,
351///     agent: "claude".to_string(),
352///     model: Some("opus".to_string()),
353/// });
354///
355/// // Pattern matching routes to category handlers
356/// match event {
357///     PipelineEvent::Agent(agent_event) => reduce_agent_event(state, agent_event),
358///     // ...
359/// }
360/// ```
361///
362/// # ⚠️ FROZEN - DO NOT ADD VARIANTS ⚠️
363///
364/// This enum is **FROZEN**. Adding new top-level variants is **PROHIBITED**.
365///
366/// ## Why is this frozen?
367///
368/// `PipelineEvent` provides category-based event routing to the reducer. The existing
369/// categories (Lifecycle, Planning, Development, Review, etc.) cover all pipeline phases.
370/// Adding new top-level variants would indicate a missing architectural abstraction or
371/// an attempt to bypass phase-specific event handling.
372///
373/// ## What to do instead
374///
375/// 1. **Express events through existing categories** - Use the category enums:
376///    - `PlanningEvent` for planning phase observations
377///    - `DevelopmentEvent` for development phase observations
378///    - `ReviewEvent` for review phase observations
379///    - `CommitEvent` for commit generation observations
380///    - `AgentEvent` for agent invocation observations
381///    - `RebaseEvent` for rebase state machine transitions
382///
383/// 2. **Return errors for unrecoverable failures** - Don't create events for conditions
384///    that should terminate the pipeline. Return `Err` from the effect handler instead.
385///
386/// 3. **Extend category enums if needed** - If you truly need a new event within an
387///    existing phase, add it to that phase's category enum (e.g., add a new variant to
388///    `ReviewEvent` rather than creating a new top-level category).
389///
390/// ## Enforcement
391///
392/// The freeze policy is enforced by the `pipeline_event_is_frozen` test in this module,
393/// which will fail to compile if new variants are added. This is intentional.
394///
395/// See `LifecycleEvent` documentation for additional context on the freeze policy rationale.
396#[derive(Clone, Serialize, Deserialize, Debug)]
397pub enum PipelineEvent {
398    /// Pipeline lifecycle events (start, stop, resume).
399    Lifecycle(LifecycleEvent),
400    /// Planning phase events.
401    Planning(PlanningEvent),
402    /// Development phase events.
403    Development(DevelopmentEvent),
404    /// Review phase events.
405    Review(ReviewEvent),
406    /// Prompt input materialization events.
407    PromptInput(PromptInputEvent),
408    /// Agent invocation and chain events.
409    Agent(AgentEvent),
410    /// Rebase operation events.
411    Rebase(RebaseEvent),
412    /// Commit generation events.
413    Commit(CommitEvent),
414    /// AwaitingDevFix phase events.
415    AwaitingDevFix(AwaitingDevFixEvent),
416
417    // ========================================================================
418    // Miscellaneous events that don't fit a category
419    // ========================================================================
420    /// Context cleanup completed.
421    ContextCleaned,
422    /// Checkpoint saved.
423    CheckpointSaved {
424        /// What triggered the checkpoint save.
425        trigger: CheckpointTrigger,
426    },
427    /// Finalization phase started.
428    FinalizingStarted,
429    /// PROMPT.md permissions restored.
430    PromptPermissionsRestored,
431    /// Loop recovery triggered (tight loop detected and broken).
432    LoopRecoveryTriggered {
433        /// String representation of the detected loop.
434        detected_loop: String,
435        /// Number of times the loop was repeated.
436        loop_count: u32,
437    },
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_pipeline_phase_display() {
446        assert_eq!(format!("{}", PipelinePhase::Planning), "Planning");
447        assert_eq!(format!("{}", PipelinePhase::Development), "Development");
448        assert_eq!(format!("{}", PipelinePhase::Review), "Review");
449        assert_eq!(
450            format!("{}", PipelinePhase::CommitMessage),
451            "Commit Message"
452        );
453        assert_eq!(
454            format!("{}", PipelinePhase::FinalValidation),
455            "Final Validation"
456        );
457        assert_eq!(format!("{}", PipelinePhase::Finalizing), "Finalizing");
458        assert_eq!(format!("{}", PipelinePhase::Complete), "Complete");
459        assert_eq!(
460            format!("{}", PipelinePhase::AwaitingDevFix),
461            "Awaiting Dev Fix"
462        );
463        assert_eq!(format!("{}", PipelinePhase::Interrupted), "Interrupted");
464    }
465
466    /// This test enforces the FROZEN policy on LifecycleEvent.
467    ///
468    /// If you're here because this test failed to compile after adding
469    /// a variant, you are violating the freeze policy. See the FROZEN
470    /// comment on LifecycleEvent for alternatives.
471    #[test]
472    fn lifecycle_event_is_frozen() {
473        fn exhaustive_match(e: LifecycleEvent) -> &'static str {
474            match e {
475                LifecycleEvent::Started => "started",
476                LifecycleEvent::Resumed { .. } => "resumed",
477                LifecycleEvent::Completed => "completed",
478                LifecycleEvent::GitignoreEntriesEnsured { .. } => "gitignore_ensured",
479                // DO NOT ADD _ WILDCARD - intentionally exhaustive
480            }
481        }
482        // Just needs to compile; actual call proves exhaustiveness
483        let _ = exhaustive_match(LifecycleEvent::Started);
484    }
485
486    /// This test enforces the FROZEN policy on PipelineEvent.
487    ///
488    /// If you're here because this test failed to compile after adding
489    /// a variant, you are violating the freeze policy. See the FROZEN
490    /// comment on PipelineEvent for alternatives.
491    #[test]
492    fn pipeline_event_is_frozen() {
493        fn exhaustive_match(e: PipelineEvent) -> &'static str {
494            match e {
495                PipelineEvent::Lifecycle(_) => "lifecycle",
496                PipelineEvent::Planning(_) => "planning",
497                PipelineEvent::Development(_) => "development",
498                PipelineEvent::Review(_) => "review",
499                PipelineEvent::PromptInput(_) => "prompt_input",
500                PipelineEvent::Agent(_) => "agent",
501                PipelineEvent::Rebase(_) => "rebase",
502                PipelineEvent::Commit(_) => "commit",
503                PipelineEvent::AwaitingDevFix(_) => "awaiting_dev_fix",
504                PipelineEvent::ContextCleaned => "context_cleaned",
505                PipelineEvent::CheckpointSaved { .. } => "checkpoint_saved",
506                PipelineEvent::FinalizingStarted => "finalizing_started",
507                PipelineEvent::PromptPermissionsRestored => "prompt_permissions_restored",
508                PipelineEvent::LoopRecoveryTriggered { .. } => "loop_recovery_triggered",
509                // DO NOT ADD _ WILDCARD - intentionally exhaustive
510            }
511        }
512        let _ = exhaustive_match(PipelineEvent::ContextCleaned);
513    }
514}