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