ralph_workflow/reducer/state/continuation/state.rs
1//! ContinuationState struct definition.
2//!
3//! Contains the core state structure for tracking continuation and retry attempts
4//! across development and fix iterations.
5
6use super::super::{ArtifactType, DevelopmentStatus, FixStatus, SameAgentRetryReason};
7use serde::{Deserialize, Serialize};
8
9/// Continuation state for development iterations.
10///
11/// Tracks context from previous attempts within the same iteration to enable
12/// continuation-aware prompting when status is "partial" or "failed".
13///
14/// # When Fields Are Populated
15///
16/// - `previous_status`: Set when DevelopmentIterationContinuationTriggered event fires
17/// - `previous_summary`: Set when DevelopmentIterationContinuationTriggered event fires
18/// - `previous_files_changed`: Set when DevelopmentIterationContinuationTriggered event fires
19/// - `previous_next_steps`: Set when DevelopmentIterationContinuationTriggered event fires
20/// - `continuation_attempt`: Incremented on each continuation within same iteration
21///
22/// # Reset Triggers
23///
24/// The continuation state is reset (cleared) when:
25/// - A new iteration starts (DevelopmentIterationStarted event)
26/// - Status becomes "completed" (ContinuationSucceeded event)
27/// - Phase transitions away from Development
28#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
29pub struct ContinuationState {
30 /// Status from the previous attempt ("partial" or "failed").
31 pub previous_status: Option<DevelopmentStatus>,
32 /// Summary of what was accomplished in the previous attempt.
33 pub previous_summary: Option<String>,
34 /// Files changed in the previous attempt.
35 pub previous_files_changed: Option<Vec<String>>,
36 /// Agent's recommended next steps from the previous attempt.
37 pub previous_next_steps: Option<String>,
38 /// Current continuation attempt number (0 = first attempt, 1+ = continuation).
39 pub continuation_attempt: u32,
40 /// Count of invalid XML outputs for the current iteration.
41 #[serde(default)]
42 pub invalid_output_attempts: u32,
43 /// Whether a continuation context write is pending.
44 #[serde(default)]
45 pub context_write_pending: bool,
46 /// Whether a continuation context cleanup is pending.
47 #[serde(default)]
48 pub context_cleanup_pending: bool,
49 /// Count of XSD validation retry attempts for current artifact.
50 ///
51 /// Tracks how many times we've retried with the same agent/session due to
52 /// XML parsing or XSD validation failures. Reset when switching agents,
53 /// artifacts, or on successful validation.
54 #[serde(default)]
55 pub xsd_retry_count: u32,
56 /// Whether an XSD retry is pending (validation failed, need to retry).
57 ///
58 /// Set to true when XsdValidationFailed event fires.
59 /// Cleared when retry attempt starts or max retries exceeded.
60 #[serde(default)]
61 pub xsd_retry_pending: bool,
62 /// Whether the next agent invocation should reuse the last session id.
63 ///
64 /// XSD retry is derived via `xsd_retry_pending`, but `xsd_retry_pending` is cleared
65 /// as soon as the retry prompt is prepared to avoid re-deriving the prepare-prompt
66 /// effect. This flag preserves the "reuse session id" signal for the subsequent
67 /// invocation effect.
68 #[serde(default)]
69 pub xsd_retry_session_reuse_pending: bool,
70 /// Last validation error message for XSD retry prompts (commit phase).
71 ///
72 /// This is set when validation fails and cleared when the retry attempt starts.
73 #[serde(default)]
74 pub last_xsd_error: Option<String>,
75 /// Last XSD validation error for review issues XML (used in XSD retry prompt).
76 ///
77 /// This is set when review validation fails and cleared when transitioning away
78 /// from review or when validation succeeds.
79 #[serde(default)]
80 pub last_review_xsd_error: Option<String>,
81 /// Last XSD validation error for fix result XML (used in XSD retry prompt).
82 ///
83 /// This is set when fix validation fails and cleared when transitioning away
84 /// from fix or when validation succeeds.
85 #[serde(default)]
86 pub last_fix_xsd_error: Option<String>,
87 /// Count of same-agent retry attempts for transient invocation failures.
88 ///
89 /// This is intentionally separate from XSD retry, which is only for invalid XML outputs.
90 #[serde(default)]
91 pub same_agent_retry_count: u32,
92 /// Whether a same-agent retry is pending.
93 ///
94 /// Set to true by the reducer when a transient invocation failure occurs (timeout/internal).
95 /// Cleared when the retry attempt starts or when switching agents.
96 #[serde(default)]
97 pub same_agent_retry_pending: bool,
98 /// The reason for the pending same-agent retry, for prompt rendering.
99 #[serde(default)]
100 pub same_agent_retry_reason: Option<SameAgentRetryReason>,
101 /// Whether a continuation is pending (output valid but work incomplete).
102 ///
103 /// Set to true when agent output indicates status is "partial" or "failed".
104 /// Cleared when continuation attempt starts or max continuations exceeded.
105 #[serde(default)]
106 pub continue_pending: bool,
107 /// Current artifact type being processed.
108 ///
109 /// Set at the start of each phase to track which XML artifact is expected.
110 /// Used for appropriate retry prompts and error messages.
111 #[serde(default)]
112 pub current_artifact: Option<ArtifactType>,
113 /// Maximum XSD retry attempts (default 10).
114 ///
115 /// Loaded from unified config. After this many retries, falls back to
116 /// agent chain advancement.
117 #[serde(default = "default_max_xsd_retry_count")]
118 pub max_xsd_retry_count: u32,
119 /// Maximum same-agent retry attempts for invocation failures that should not
120 /// immediately trigger agent fallback (default 2).
121 ///
122 /// This is a failure budget for the current agent. For example, with a value of 2:
123 /// - 1st failure → retry the same agent
124 /// - 2nd failure → fall back to the next agent
125 #[serde(default = "default_max_same_agent_retry_count")]
126 pub max_same_agent_retry_count: u32,
127 /// Maximum continuation attempts (default 3).
128 ///
129 /// Loaded from unified config. After this many continuations, marks
130 /// iteration as complete (even if status is partial/failed).
131 #[serde(default = "default_max_continue_count")]
132 pub max_continue_count: u32,
133
134 // =========================================================================
135 // Fix continuation tracking (parallel to development continuation)
136 // =========================================================================
137 /// Status from the previous fix attempt.
138 #[serde(default)]
139 pub fix_status: Option<FixStatus>,
140 /// Summary from the previous fix attempt.
141 #[serde(default)]
142 pub fix_previous_summary: Option<String>,
143 /// Current fix continuation attempt number (0 = first attempt, 1+ = continuation).
144 #[serde(default)]
145 pub fix_continuation_attempt: u32,
146 /// Whether a fix continuation is pending (output valid but work incomplete).
147 ///
148 /// Set to true when fix output indicates status is "issues_remain".
149 /// Cleared when continuation attempt starts or max continuations exceeded.
150 #[serde(default)]
151 pub fix_continue_pending: bool,
152 /// Maximum fix continuation attempts (default 3).
153 ///
154 /// After this many continuations, proceeds to commit even if issues remain.
155 #[serde(default = "default_max_continue_count")]
156 pub max_fix_continue_count: u32,
157
158 // =========================================================================
159 // Loop detection fields (to prevent infinite tight loops)
160 // =========================================================================
161 /// Loop detection: last effect executed (for detecting repeats).
162 #[serde(default)]
163 pub last_effect_kind: Option<String>,
164
165 /// Loop detection: count of consecutive identical effects.
166 #[serde(default)]
167 pub consecutive_same_effect_count: u32,
168
169 /// Maximum consecutive identical effects before triggering recovery.
170 #[serde(default = "default_max_consecutive_same_effect")]
171 pub max_consecutive_same_effect: u32,
172}
173
174const fn default_max_xsd_retry_count() -> u32 {
175 10
176}
177
178const fn default_max_same_agent_retry_count() -> u32 {
179 2
180}
181
182const fn default_max_continue_count() -> u32 {
183 3
184}
185
186const fn default_max_consecutive_same_effect() -> u32 {
187 20
188}
189
190impl Default for ContinuationState {
191 fn default() -> Self {
192 Self {
193 previous_status: None,
194 previous_summary: None,
195 previous_files_changed: None,
196 previous_next_steps: None,
197 continuation_attempt: 0,
198 invalid_output_attempts: 0,
199 context_write_pending: false,
200 context_cleanup_pending: false,
201 xsd_retry_count: 0,
202 xsd_retry_pending: false,
203 xsd_retry_session_reuse_pending: false,
204 last_xsd_error: None,
205 last_review_xsd_error: None,
206 last_fix_xsd_error: None,
207 same_agent_retry_count: 0,
208 same_agent_retry_pending: false,
209 same_agent_retry_reason: None,
210 continue_pending: false,
211 current_artifact: None,
212 max_xsd_retry_count: default_max_xsd_retry_count(),
213 max_same_agent_retry_count: default_max_same_agent_retry_count(),
214 max_continue_count: default_max_continue_count(),
215 // Fix continuation fields
216 fix_status: None,
217 fix_previous_summary: None,
218 fix_continuation_attempt: 0,
219 fix_continue_pending: false,
220 max_fix_continue_count: default_max_continue_count(),
221 // Loop detection fields
222 last_effect_kind: None,
223 consecutive_same_effect_count: 0,
224 max_consecutive_same_effect: default_max_consecutive_same_effect(),
225 }
226 }
227}
228
229impl ContinuationState {
230 /// Create a new empty continuation state.
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 /// Create continuation state with custom limits (for config loading).
236 pub fn with_limits(
237 max_xsd_retry_count: u32,
238 max_continue_count: u32,
239 max_same_agent_retry_count: u32,
240 ) -> Self {
241 Self {
242 max_xsd_retry_count,
243 max_same_agent_retry_count,
244 max_continue_count,
245 max_fix_continue_count: max_continue_count,
246 ..Self::default()
247 }
248 }
249
250 /// Builder: set max XSD retry count.
251 ///
252 /// Use 0 to disable XSD retries (immediate agent fallback on validation failure).
253 pub fn with_max_xsd_retry(mut self, max_xsd_retry_count: u32) -> Self {
254 self.max_xsd_retry_count = max_xsd_retry_count;
255 self
256 }
257
258 /// Builder: set max same-agent retry count for transient invocation failures.
259 ///
260 /// Use 0 to disable same-agent retries (immediate agent fallback on timeout/internal error).
261 pub fn with_max_same_agent_retry(mut self, max_same_agent_retry_count: u32) -> Self {
262 self.max_same_agent_retry_count = max_same_agent_retry_count;
263 self
264 }
265
266 /// Check if this is a continuation attempt (not the first attempt).
267 pub fn is_continuation(&self) -> bool {
268 self.continuation_attempt > 0
269 }
270
271 /// Reset the continuation state for a new iteration or phase transition.
272 ///
273 /// This performs a **hard reset** of ALL continuation and retry state,
274 /// preserving only the configured limits (max_xsd_retry_count, max_continue_count,
275 /// max_fix_continue_count).
276 ///
277 /// # What gets reset
278 ///
279 /// - `continuation_attempt` -> 0
280 /// - `continue_pending` -> false
281 /// - `invalid_output_attempts` -> 0
282 /// - `xsd_retry_count` -> 0
283 /// - `xsd_retry_pending` -> false
284 /// - `fix_continuation_attempt` -> 0
285 /// - `fix_continue_pending` -> false
286 /// - `fix_status` -> None
287 /// - `current_artifact` -> None
288 /// - `previous_status`, `previous_summary`, etc. -> defaults
289 ///
290 /// # Usage
291 ///
292 /// Call this when transitioning to a completely new phase or iteration
293 /// where prior continuation/retry state should not carry over. For partial
294 /// resets (e.g., resetting only fix continuation while preserving development
295 /// continuation state), use field-level updates instead.
296 pub fn reset(&self) -> Self {
297 // Preserve configured limits, reset everything else including loop detection counters.
298 // The struct initialization below explicitly preserves max_* fields,
299 // then the spread operator ..Self::default() resets ALL other fields
300 // (including loop detection fields: last_effect_kind -> None,
301 // consecutive_same_effect_count -> 0). This is intentional during
302 // loop recovery to break the tight loop cycle and start fresh.
303 Self {
304 max_xsd_retry_count: self.max_xsd_retry_count,
305 max_same_agent_retry_count: self.max_same_agent_retry_count,
306 max_continue_count: self.max_continue_count,
307 max_fix_continue_count: self.max_fix_continue_count,
308 max_consecutive_same_effect: self.max_consecutive_same_effect,
309 ..Self::default()
310 }
311 }
312}