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 10).
155 ///
156 /// After this many continuations, proceeds to commit even if issues remain.
157 #[serde(default = "default_max_fix_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
206const fn default_max_fix_continue_count() -> u32 {
207 10
208}
209
210/// Default threshold for consecutive identical effects before triggering loop recovery.
211///
212/// When the same effect is executed this many times consecutively, the system triggers
213/// loop recovery to break potential infinite retry cycles.
214pub const DEFAULT_LOOP_DETECTION_THRESHOLD: u32 = 100;
215
216/// Serde requires a function for `#[serde(default = "...")]`.
217const fn default_max_consecutive_same_effect() -> u32 {
218 DEFAULT_LOOP_DETECTION_THRESHOLD
219}
220
221impl Default for ContinuationState {
222 fn default() -> Self {
223 Self {
224 previous_status: None,
225 previous_summary: None,
226 previous_files_changed: None,
227 previous_next_steps: None,
228 continuation_attempt: 0,
229 invalid_output_attempts: 0,
230 context_write_pending: false,
231 context_cleanup_pending: false,
232 xsd_retry_count: 0,
233 xsd_retry_pending: false,
234 xsd_retry_session_reuse_pending: false,
235 last_xsd_error: None,
236 last_review_xsd_error: None,
237 last_fix_xsd_error: None,
238 same_agent_retry_count: 0,
239 same_agent_retry_pending: false,
240 same_agent_retry_reason: None,
241 continue_pending: false,
242 current_artifact: None,
243 max_xsd_retry_count: default_max_xsd_retry_count(),
244 max_same_agent_retry_count: default_max_same_agent_retry_count(),
245 max_continue_count: default_max_continue_count(),
246 // Fix continuation fields
247 fix_status: None,
248 fix_previous_summary: None,
249 fix_continuation_attempt: 0,
250 fix_continue_pending: false,
251 max_fix_continue_count: default_max_fix_continue_count(),
252 // Loop detection fields
253 last_effect_kind: None,
254 consecutive_same_effect_count: 0,
255 max_consecutive_same_effect: DEFAULT_LOOP_DETECTION_THRESHOLD,
256 // Timeout context preservation fields
257 timeout_context_write_pending: false,
258 timeout_context_file_path: None,
259 }
260 }
261}
262
263impl ContinuationState {
264 /// Create a new empty continuation state.
265 #[must_use]
266 pub fn new() -> Self {
267 Self::default()
268 }
269
270 /// Create continuation state with custom limits (for config loading).
271 #[must_use]
272 pub fn with_limits(
273 max_xsd_retry_count: u32,
274 max_continue_count: u32,
275 max_same_agent_retry_count: u32,
276 ) -> Self {
277 Self {
278 max_xsd_retry_count,
279 max_same_agent_retry_count,
280 max_continue_count,
281 max_fix_continue_count: default_max_fix_continue_count(),
282 ..Self::default()
283 }
284 }
285
286 /// Builder: set max XSD retry count.
287 ///
288 /// Use 0 to disable XSD retries (immediate agent fallback on validation failure).
289 #[must_use]
290 pub fn with_max_xsd_retry(self, max_xsd_retry_count: u32) -> Self {
291 Self {
292 max_xsd_retry_count,
293 ..self
294 }
295 }
296
297 /// Builder: set max same-agent retry count for transient invocation failures.
298 ///
299 /// Use 0 to disable same-agent retries (immediate agent fallback on timeout/internal error).
300 #[must_use]
301 pub fn with_max_same_agent_retry(self, max_same_agent_retry_count: u32) -> Self {
302 Self {
303 max_same_agent_retry_count,
304 ..self
305 }
306 }
307
308 /// Check if this is a continuation attempt (not the first attempt).
309 #[must_use]
310 pub const fn is_continuation(&self) -> bool {
311 self.continuation_attempt > 0
312 }
313
314 /// Whether the active runtime drain has a pending continuation.
315 #[must_use]
316 pub const fn pending_continuation_for_drain(&self, drain: AgentDrain) -> bool {
317 match drain {
318 AgentDrain::Development => self.continue_pending,
319 AgentDrain::Fix => self.fix_continue_pending,
320 AgentDrain::Planning
321 | AgentDrain::Review
322 | AgentDrain::Commit
323 | AgentDrain::Analysis => false,
324 }
325 }
326
327 /// Whether the active runtime drain has exhausted its continuation budget.
328 #[must_use]
329 pub const fn continuation_exhausted_for_drain(&self, drain: AgentDrain) -> bool {
330 match drain {
331 AgentDrain::Development => self.continuation_attempt >= self.max_continue_count,
332 AgentDrain::Fix => self.fix_continuation_attempt >= self.max_fix_continue_count,
333 AgentDrain::Planning
334 | AgentDrain::Review
335 | AgentDrain::Commit
336 | AgentDrain::Analysis => false,
337 }
338 }
339
340 /// Reset the continuation state for a new iteration or phase transition.
341 ///
342 /// This performs a **hard reset** of ALL continuation and retry state,
343 /// preserving only the configured limits (`max_xsd_retry_count`, `max_continue_count`,
344 /// `max_fix_continue_count`).
345 ///
346 /// # What gets reset
347 ///
348 /// - `continuation_attempt` -> 0
349 /// - `continue_pending` -> false
350 /// - `invalid_output_attempts` -> 0
351 /// - `xsd_retry_count` -> 0
352 /// - `xsd_retry_pending` -> false
353 /// - `fix_continuation_attempt` -> 0
354 /// - `fix_continue_pending` -> false
355 /// - `fix_status` -> None
356 /// - `current_artifact` -> None
357 /// - `previous_status`, `previous_summary`, etc. -> defaults
358 ///
359 /// # Usage
360 ///
361 /// Call this when transitioning to a completely new phase or iteration
362 /// where prior continuation/retry state should not carry over. For partial
363 /// resets (e.g., resetting only fix continuation while preserving development
364 /// continuation state), use field-level updates instead.
365 #[must_use]
366 pub fn reset(self) -> Self {
367 // Preserve configured limits, reset everything else including loop detection counters.
368 // The struct initialization below explicitly preserves max_* fields,
369 // then the spread operator ..Self::default() resets ALL other fields
370 // (including loop detection fields: last_effect_kind -> None,
371 // consecutive_same_effect_count -> 0). This is intentional during
372 // loop recovery to break the tight loop cycle and start fresh.
373 Self {
374 max_xsd_retry_count: self.max_xsd_retry_count,
375 max_same_agent_retry_count: self.max_same_agent_retry_count,
376 max_continue_count: self.max_continue_count,
377 max_fix_continue_count: self.max_fix_continue_count,
378 max_consecutive_same_effect: self.max_consecutive_same_effect,
379 ..Self::default()
380 }
381 }
382}