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}