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. Box<[String]> saves 8 bytes per instance
35 /// vs Vec<String> (no capacity field) since this collection never grows after construction.
36 pub previous_files_changed: Option<Box<[String]>>,
37 /// Agent's recommended next steps from the previous attempt.
38 pub previous_next_steps: Option<String>,
39 /// Current continuation attempt number (0 = first attempt, 1+ = continuation).
40 pub continuation_attempt: u32,
41 /// Count of invalid XML outputs for the current iteration.
42 #[serde(default)]
43 pub invalid_output_attempts: u32,
44 /// Whether a continuation context write is pending.
45 #[serde(default)]
46 pub context_write_pending: bool,
47 /// Whether a continuation context cleanup is pending.
48 #[serde(default)]
49 pub context_cleanup_pending: bool,
50 /// Count of XSD validation retry attempts for current artifact.
51 ///
52 /// Tracks how many times we've retried with the same agent/session due to
53 /// XML parsing or XSD validation failures. Reset when switching agents,
54 /// artifacts, or on successful validation.
55 #[serde(default)]
56 pub xsd_retry_count: u32,
57 /// Whether an XSD retry is pending (validation failed, need to retry).
58 ///
59 /// Set to true when `XsdValidationFailed` event fires.
60 /// Cleared when retry attempt starts or max retries exceeded.
61 #[serde(default)]
62 pub xsd_retry_pending: bool,
63 /// Whether the next agent invocation should reuse the last session id.
64 ///
65 /// XSD retry is derived via `xsd_retry_pending`, but `xsd_retry_pending` is cleared
66 /// as soon as the retry prompt is prepared to avoid re-deriving the prepare-prompt
67 /// effect. This flag preserves the "reuse session id" signal for the subsequent
68 /// invocation effect.
69 #[serde(default)]
70 pub xsd_retry_session_reuse_pending: bool,
71 /// Last validation error message for XSD retry prompts (commit phase).
72 ///
73 /// This is set when validation fails and cleared when the retry attempt starts.
74 #[serde(default)]
75 pub last_xsd_error: Option<String>,
76 /// Last XSD validation error for review issues XML (used in XSD retry prompt).
77 ///
78 /// This is set when review validation fails and cleared when transitioning away
79 /// from review or when validation succeeds.
80 #[serde(default)]
81 pub last_review_xsd_error: Option<String>,
82 /// Last XSD validation error for fix result XML (used in XSD retry prompt).
83 ///
84 /// This is set when fix validation fails and cleared when transitioning away
85 /// from fix or when validation succeeds.
86 #[serde(default)]
87 pub last_fix_xsd_error: Option<String>,
88 /// Count of same-agent retry attempts for transient invocation failures.
89 ///
90 /// This is intentionally separate from XSD retry, which is only for invalid XML outputs.
91 #[serde(default)]
92 pub same_agent_retry_count: u32,
93 /// Whether a same-agent retry is pending.
94 ///
95 /// Set to true by the reducer when a transient invocation failure occurs (timeout/internal).
96 /// Cleared when the retry attempt starts or when switching agents.
97 #[serde(default)]
98 pub same_agent_retry_pending: bool,
99 /// The reason for the pending same-agent retry, for prompt rendering.
100 #[serde(default)]
101 pub same_agent_retry_reason: Option<SameAgentRetryReason>,
102 /// Whether a continuation is pending (output valid but work incomplete).
103 ///
104 /// Set to true when agent output indicates status is "partial" or "failed".
105 /// Cleared when continuation attempt starts or max continuations exceeded.
106 #[serde(default)]
107 pub continue_pending: bool,
108 /// Current artifact type being processed.
109 ///
110 /// Set at the start of each phase to track which XML artifact is expected.
111 /// Used for appropriate retry prompts and error messages.
112 #[serde(default)]
113 pub current_artifact: Option<ArtifactType>,
114 /// Maximum XSD retry attempts (default 10).
115 ///
116 /// Loaded from unified config. After this many retries, falls back to
117 /// agent chain advancement.
118 #[serde(default = "default_max_xsd_retry_count")]
119 pub max_xsd_retry_count: u32,
120 /// Maximum same-agent retry attempts for invocation failures that should not
121 /// immediately trigger agent fallback (default 2).
122 ///
123 /// This is a failure budget for the current agent. For example, with a value of 2:
124 /// - 1st failure → retry the same agent
125 /// - 2nd failure → fall back to the next agent
126 #[serde(default = "default_max_same_agent_retry_count")]
127 pub max_same_agent_retry_count: u32,
128 /// Maximum continuation attempts (default 3).
129 ///
130 /// Loaded from unified config. After this many continuations, marks
131 /// iteration as complete (even if status is partial/failed).
132 #[serde(default = "default_max_continue_count")]
133 pub max_continue_count: u32,
134
135 // =========================================================================
136 // Fix continuation tracking (parallel to development continuation)
137 // =========================================================================
138 /// Status from the previous fix attempt.
139 #[serde(default)]
140 pub fix_status: Option<FixStatus>,
141 /// Summary from the previous fix attempt.
142 #[serde(default)]
143 pub fix_previous_summary: Option<String>,
144 /// Current fix continuation attempt number (0 = first attempt, 1+ = continuation).
145 #[serde(default)]
146 pub fix_continuation_attempt: u32,
147 /// Whether a fix continuation is pending (output valid but work incomplete).
148 ///
149 /// Set to true when fix output indicates status is "`issues_remain`".
150 /// Cleared when continuation attempt starts or max continuations exceeded.
151 #[serde(default)]
152 pub fix_continue_pending: bool,
153 /// Maximum fix continuation attempts (default 3).
154 ///
155 /// After this many continuations, proceeds to commit even if issues remain.
156 #[serde(default = "default_max_continue_count")]
157 pub max_fix_continue_count: u32,
158
159 // =========================================================================
160 // Loop detection fields (to prevent infinite tight loops)
161 // =========================================================================
162 /// Loop detection: last effect executed (for detecting repeats).
163 #[serde(default)]
164 pub last_effect_kind: Option<String>,
165
166 /// Loop detection: count of consecutive identical effects.
167 #[serde(default)]
168 pub consecutive_same_effect_count: u32,
169
170 /// Maximum consecutive identical effects before triggering recovery.
171 #[serde(default = "default_max_consecutive_same_effect")]
172 pub max_consecutive_same_effect: u32,
173
174 // =========================================================================
175 // Timeout context preservation fields (for session-less agent retry)
176 // =========================================================================
177 /// Whether a timeout context file write is pending.
178 ///
179 /// Set when a timeout with `PartialOutput` occurs but the agent has no session ID.
180 /// The context must be extracted from the logfile and written to a temp file
181 /// before the retry prompt is prepared.
182 #[serde(default)]
183 pub timeout_context_write_pending: bool,
184
185 /// Path to the timeout context file (if written).
186 ///
187 /// After `WriteTimeoutContext` effect completes, this stores the path to the
188 /// context file so the retry prompt can reference it.
189 #[serde(default)]
190 pub timeout_context_file_path: Option<String>,
191}
192
193const fn default_max_xsd_retry_count() -> u32 {
194 10
195}
196
197const fn default_max_same_agent_retry_count() -> u32 {
198 2
199}
200
201const fn default_max_continue_count() -> u32 {
202 3
203}
204
205/// Default threshold for consecutive identical effects before triggering loop recovery.
206///
207/// When the same effect is executed this many times consecutively, the system triggers
208/// loop recovery to break potential infinite retry cycles.
209pub const DEFAULT_LOOP_DETECTION_THRESHOLD: u32 = 100;
210
211/// Serde requires a function for `#[serde(default = "...")]`.
212const fn default_max_consecutive_same_effect() -> u32 {
213 DEFAULT_LOOP_DETECTION_THRESHOLD
214}
215
216impl Default for ContinuationState {
217 fn default() -> Self {
218 Self {
219 previous_status: None,
220 previous_summary: None,
221 previous_files_changed: None,
222 previous_next_steps: None,
223 continuation_attempt: 0,
224 invalid_output_attempts: 0,
225 context_write_pending: false,
226 context_cleanup_pending: false,
227 xsd_retry_count: 0,
228 xsd_retry_pending: false,
229 xsd_retry_session_reuse_pending: false,
230 last_xsd_error: None,
231 last_review_xsd_error: None,
232 last_fix_xsd_error: None,
233 same_agent_retry_count: 0,
234 same_agent_retry_pending: false,
235 same_agent_retry_reason: None,
236 continue_pending: false,
237 current_artifact: None,
238 max_xsd_retry_count: default_max_xsd_retry_count(),
239 max_same_agent_retry_count: default_max_same_agent_retry_count(),
240 max_continue_count: default_max_continue_count(),
241 // Fix continuation fields
242 fix_status: None,
243 fix_previous_summary: None,
244 fix_continuation_attempt: 0,
245 fix_continue_pending: false,
246 max_fix_continue_count: default_max_continue_count(),
247 // Loop detection fields
248 last_effect_kind: None,
249 consecutive_same_effect_count: 0,
250 max_consecutive_same_effect: DEFAULT_LOOP_DETECTION_THRESHOLD,
251 // Timeout context preservation fields
252 timeout_context_write_pending: false,
253 timeout_context_file_path: None,
254 }
255 }
256}
257
258impl ContinuationState {
259 /// Create a new empty continuation state.
260 #[must_use]
261 pub fn new() -> Self {
262 Self::default()
263 }
264
265 /// Create continuation state with custom limits (for config loading).
266 #[must_use]
267 pub fn with_limits(
268 max_xsd_retry_count: u32,
269 max_continue_count: u32,
270 max_same_agent_retry_count: u32,
271 ) -> Self {
272 Self {
273 max_xsd_retry_count,
274 max_same_agent_retry_count,
275 max_continue_count,
276 max_fix_continue_count: max_continue_count,
277 ..Self::default()
278 }
279 }
280
281 /// Builder: set max XSD retry count.
282 ///
283 /// Use 0 to disable XSD retries (immediate agent fallback on validation failure).
284 #[must_use]
285 pub const fn with_max_xsd_retry(mut self, max_xsd_retry_count: u32) -> Self {
286 self.max_xsd_retry_count = max_xsd_retry_count;
287 self
288 }
289
290 /// Builder: set max same-agent retry count for transient invocation failures.
291 ///
292 /// Use 0 to disable same-agent retries (immediate agent fallback on timeout/internal error).
293 #[must_use]
294 pub const fn with_max_same_agent_retry(mut self, max_same_agent_retry_count: u32) -> Self {
295 self.max_same_agent_retry_count = max_same_agent_retry_count;
296 self
297 }
298
299 /// Check if this is a continuation attempt (not the first attempt).
300 #[must_use]
301 pub const fn is_continuation(&self) -> bool {
302 self.continuation_attempt > 0
303 }
304
305 /// Reset the continuation state for a new iteration or phase transition.
306 ///
307 /// This performs a **hard reset** of ALL continuation and retry state,
308 /// preserving only the configured limits (`max_xsd_retry_count`, `max_continue_count`,
309 /// `max_fix_continue_count`).
310 ///
311 /// # What gets reset
312 ///
313 /// - `continuation_attempt` -> 0
314 /// - `continue_pending` -> false
315 /// - `invalid_output_attempts` -> 0
316 /// - `xsd_retry_count` -> 0
317 /// - `xsd_retry_pending` -> false
318 /// - `fix_continuation_attempt` -> 0
319 /// - `fix_continue_pending` -> false
320 /// - `fix_status` -> None
321 /// - `current_artifact` -> None
322 /// - `previous_status`, `previous_summary`, etc. -> defaults
323 ///
324 /// # Usage
325 ///
326 /// Call this when transitioning to a completely new phase or iteration
327 /// where prior continuation/retry state should not carry over. For partial
328 /// resets (e.g., resetting only fix continuation while preserving development
329 /// continuation state), use field-level updates instead.
330 #[must_use]
331 pub fn reset(self) -> Self {
332 // Preserve configured limits, reset everything else including loop detection counters.
333 // The struct initialization below explicitly preserves max_* fields,
334 // then the spread operator ..Self::default() resets ALL other fields
335 // (including loop detection fields: last_effect_kind -> None,
336 // consecutive_same_effect_count -> 0). This is intentional during
337 // loop recovery to break the tight loop cycle and start fresh.
338 Self {
339 max_xsd_retry_count: self.max_xsd_retry_count,
340 max_same_agent_retry_count: self.max_same_agent_retry_count,
341 max_continue_count: self.max_continue_count,
342 max_fix_continue_count: self.max_fix_continue_count,
343 max_consecutive_same_effect: self.max_consecutive_same_effect,
344 ..Self::default()
345 }
346 }
347}