Skip to main content

ralph_workflow/reducer/state/
enums.rs

1// State enums and basic types.
2//
3// Contains ArtifactType, PromptMode, DevelopmentStatus, FixStatus, RebaseState, CommitState.
4
5/// Artifact type being processed by the current phase.
6///
7/// Used to track which XML artifact type is expected for XSD validation,
8/// enabling role-specific retry prompts and error messages.
9#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
10pub enum ArtifactType {
11    /// Plan XML from planning phase.
12    Plan,
13    /// DevelopmentResult XML from development phase.
14    DevelopmentResult,
15    /// Issues XML from review phase.
16    Issues,
17    /// FixResult XML from fix phase.
18    FixResult,
19    /// CommitMessage XML from commit message generation.
20    CommitMessage,
21}
22
23/// Prompt rendering mode chosen by the reducer.
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
25pub enum PromptMode {
26    /// Standard prompt rendering.
27    Normal,
28    /// XSD retry prompt rendering for invalid XML outputs.
29    XsdRetry,
30    /// Continuation prompt rendering for partial/failed outputs.
31    Continuation,
32    /// Same-agent retry prompt rendering for transient invocation failures.
33    ///
34    /// Used for timeouts and internal/unknown errors where we want to retry the
35    /// same agent first with additional guidance (reduce scope, chunk work, etc.).
36    SameAgentRetry,
37}
38
39/// Reason a same-agent retry is pending.
40#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
41pub enum SameAgentRetryReason {
42    /// The agent invocation timed out.
43    Timeout,
44    /// The agent invocation failed with an internal/unknown error.
45    InternalError,
46    /// The agent invocation failed with a non-auth, non-rate-limit, non-timeout error.
47    ///
48    /// This is a catch-all category used to ensure immediate agent fallback only happens
49    /// for rate limit (429) and authentication failures.
50    Other,
51}
52
53impl std::fmt::Display for ArtifactType {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::Plan => write!(f, "plan"),
57            Self::DevelopmentResult => write!(f, "development_result"),
58            Self::Issues => write!(f, "issues"),
59            Self::FixResult => write!(f, "fix_result"),
60            Self::CommitMessage => write!(f, "commit_message"),
61        }
62    }
63}
64
65/// Development status from agent output.
66///
67/// These values map to the `<ralph-status>` element in development_result.xml.
68/// Used to track whether work is complete or needs continuation.
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
70pub enum DevelopmentStatus {
71    /// Work completed successfully - no continuation needed.
72    Completed,
73    /// Work partially done - needs continuation.
74    Partial,
75    /// Work failed - needs continuation with different approach.
76    Failed,
77}
78
79/// Fix status from agent output.
80///
81/// These values map to the `<ralph-status>` element in fix_result.xml.
82/// Used to track whether fix work is complete or needs continuation.
83///
84/// # Continuation Semantics
85///
86/// - `AllIssuesAddressed`: Complete, no continuation needed
87/// - `NoIssuesFound`: Complete, no continuation needed
88/// - `IssuesRemain`: Work incomplete, needs continuation
89/// - `Failed`: Fix attempt failed, needs continuation with different approach
90#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
91pub enum FixStatus {
92    /// All issues have been addressed - no continuation needed.
93    #[default]
94    AllIssuesAddressed,
95    /// Issues remain - needs continuation.
96    IssuesRemain,
97    /// No issues were found - nothing to fix.
98    NoIssuesFound,
99    /// Fix attempt failed - needs continuation with different approach.
100    ///
101    /// This status indicates the fix attempt produced valid XML output but
102    /// the agent could not fix the issues (e.g., blocked by external factors,
103    /// needs different strategy). Triggers continuation like `IssuesRemain`.
104    Failed,
105}
106
107impl std::fmt::Display for DevelopmentStatus {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::Completed => write!(f, "completed"),
111            Self::Partial => write!(f, "partial"),
112            Self::Failed => write!(f, "failed"),
113        }
114    }
115}
116
117impl std::fmt::Display for FixStatus {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        match self {
120            Self::AllIssuesAddressed => write!(f, "all_issues_addressed"),
121            Self::IssuesRemain => write!(f, "issues_remain"),
122            Self::NoIssuesFound => write!(f, "no_issues_found"),
123            Self::Failed => write!(f, "failed"),
124        }
125    }
126}
127
128impl FixStatus {
129    /// Parse a fix status string from XML.
130    ///
131    /// This is intentionally not implementing std::str::FromStr because it returns
132    /// Option<Self> for easier handling of unknown values without error types.
133    pub fn parse(s: &str) -> Option<Self> {
134        match s {
135            "all_issues_addressed" => Some(Self::AllIssuesAddressed),
136            "issues_remain" => Some(Self::IssuesRemain),
137            "no_issues_found" => Some(Self::NoIssuesFound),
138            "failed" => Some(Self::Failed),
139            _ => None,
140        }
141    }
142
143    /// Returns true if the fix is complete (no more work needed).
144    pub fn is_complete(&self) -> bool {
145        matches!(self, Self::AllIssuesAddressed | Self::NoIssuesFound)
146    }
147
148    /// Returns true if continuation is needed (incomplete work or failure).
149    ///
150    /// Both `IssuesRemain` and `Failed` trigger continuation:
151    /// - `IssuesRemain`: Some issues fixed, others remain
152    /// - `Failed`: Fix attempt failed, needs different approach
153    pub fn needs_continuation(&self) -> bool {
154        matches!(self, Self::IssuesRemain | Self::Failed)
155    }
156}
157
158/// Rebase operation state.
159///
160/// Tracks rebase progress through the state machine:
161/// NotStarted → InProgress → Conflicted → Completed/Skipped
162#[derive(Clone, Serialize, Deserialize, Debug)]
163pub enum RebaseState {
164    NotStarted,
165    InProgress {
166        original_head: String,
167        target_branch: String,
168    },
169    Conflicted {
170        original_head: String,
171        target_branch: String,
172        files: Vec<PathBuf>,
173        resolution_attempts: u32,
174    },
175    Completed {
176        new_head: String,
177    },
178    Skipped,
179}
180
181impl RebaseState {
182    #[doc(hidden)]
183    #[cfg(any(test, feature = "test-utils"))]
184    pub fn is_terminal(&self) -> bool {
185        matches!(self, RebaseState::Completed { .. } | RebaseState::Skipped)
186    }
187
188    pub fn current_head(&self) -> Option<String> {
189        match self {
190            RebaseState::NotStarted | RebaseState::Skipped => None,
191            RebaseState::InProgress { original_head, .. } => Some(original_head.clone()),
192            RebaseState::Conflicted { .. } => None,
193            RebaseState::Completed { new_head } => Some(new_head.clone()),
194        }
195    }
196
197    #[doc(hidden)]
198    #[cfg(any(test, feature = "test-utils"))]
199    pub fn is_in_progress(&self) -> bool {
200        matches!(
201            self,
202            RebaseState::InProgress { .. } | RebaseState::Conflicted { .. }
203        )
204    }
205}
206
207/// Maximum number of retry attempts when XML/format validation fails.
208///
209/// This applies across the pipeline for:
210/// - Commit message generation validation failures
211/// - Plan generation validation failures
212/// - Development output validation failures
213/// - Review output validation failures
214///
215/// When an agent produces output that fails XML parsing or format validation,
216/// we retry with corrective prompts up to this many times before giving up.
217pub const MAX_VALIDATION_RETRY_ATTEMPTS: u32 = 100;
218
219/// Maximum number of developer validation retry attempts before giving up.
220///
221/// Specifically for developer iterations - this is for XSD validation failures
222/// (malformed XML). After exhausting these retries, the system will fall back
223/// to a continuation attempt with a fresh prompt rather than failing entirely.
224/// This separates XSD retry (can't parse the response) from continuation
225/// (understood the response but work is incomplete).
226pub const MAX_DEV_VALIDATION_RETRY_ATTEMPTS: u32 = 10;
227
228/// Maximum number of invalid XML output reruns before aborting the iteration.
229pub const MAX_DEV_INVALID_OUTPUT_RERUNS: u32 = 2;
230
231/// Maximum number of invalid planning output reruns before switching agents.
232pub const MAX_PLAN_INVALID_OUTPUT_RERUNS: u32 = 2;
233
234/// Commit generation state.
235///
236/// Tracks commit message generation progress through retries:
237/// NotStarted → Generating → Generated → Committed/Skipped
238#[derive(Clone, Serialize, Deserialize, Debug)]
239pub enum CommitState {
240    NotStarted,
241    Generating { attempt: u32, max_attempts: u32 },
242    Generated { message: String },
243    Committed { hash: String },
244    Skipped,
245}
246
247impl CommitState {
248    #[doc(hidden)]
249    #[cfg(any(test, feature = "test-utils"))]
250    pub fn is_terminal(&self) -> bool {
251        matches!(self, CommitState::Committed { .. } | CommitState::Skipped)
252    }
253}
254
255/// Kind of prompt input that may require oversize handling.
256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257pub enum PromptInputKind {
258    Prompt,
259    Plan,
260    Diff,
261    LastOutput,
262}
263
264/// How an input is represented to downstream prompt templates.
265#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
266pub enum PromptInputRepresentation {
267    /// Input is embedded inline in the prompt template.
268    Inline,
269    /// Input is referenced by a workspace-relative file path.
270    ///
271    /// Important: this path is serialized into checkpoints. Storing absolute paths
272    /// would leak local filesystem layout and can break resuming a run from a
273    /// different checkout location.
274    FileReference {
275        /// Workspace-relative path to the materialized artifact.
276        path: PathBuf,
277    },
278}
279
280/// Reason an input was materialized in a non-default way.
281#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
282pub enum PromptMaterializationReason {
283    /// Input was within all configured budgets (no oversize handling required).
284    WithinBudgets,
285    /// Input exceeded the inline-embedding budget and must be referenced by file.
286    InlineBudgetExceeded,
287    /// Input exceeded the model-context budget and was truncated before use.
288    ModelBudgetExceeded,
289    /// Input was referenced even though it was within budgets (explicit policy).
290    PolicyForcedReference,
291}
292
293/// Canonical, reducer-visible record of prompt input materialization.
294///
295/// This records what the downstream prompt template will embed (inline vs file
296/// reference), along with stable identifiers so the reducer can dedupe repeated
297/// attempts in the event loop.
298#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
299pub struct MaterializedPromptInput {
300    pub kind: PromptInputKind,
301    pub content_id_sha256: String,
302    pub consumer_signature_sha256: String,
303    pub original_bytes: u64,
304    pub final_bytes: u64,
305    #[serde(default)]
306    pub model_budget_bytes: Option<u64>,
307    #[serde(default)]
308    pub inline_budget_bytes: Option<u64>,
309    pub representation: PromptInputRepresentation,
310    pub reason: PromptMaterializationReason,
311}