Skip to main content

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 `PartialResult` 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}