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