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