Skip to main content

ralph_workflow/reducer/state/continuation/
budget.rs

1//! Budget tracking logic for continuation attempts.
2//!
3//! Provides methods for tracking and checking budget exhaustion for:
4//! - XSD retries
5//! - Same-agent retries
6//! - Development continuations
7//! - Fix continuations
8
9use super::super::{ArtifactType, DevelopmentStatus, FixStatus, SameAgentRetryReason};
10use super::state::ContinuationState;
11
12impl ContinuationState {
13    /// Set the current artifact type being processed.
14    #[must_use]
15    pub fn with_artifact(self, artifact: ArtifactType) -> Self {
16        Self {
17            current_artifact: Some(artifact),
18            xsd_retry_count: 0,
19            xsd_retry_pending: false,
20            xsd_retry_session_reuse_pending: false,
21            last_xsd_error: None,
22            last_review_xsd_error: None,
23            last_fix_xsd_error: None,
24            ..self
25        }
26    }
27
28    /// Mark XSD validation as failed, triggering a retry.
29    ///
30    /// For XSD retry, we want to re-invoke the same agent in the same session when possible,
31    /// to keep retries deterministic and to preserve provider-side context.
32    #[must_use]
33    pub fn trigger_xsd_retry(self) -> Self {
34        Self {
35            xsd_retry_pending: true,
36            xsd_retry_count: self.xsd_retry_count.saturating_add(1),
37            xsd_retry_session_reuse_pending: true,
38            ..self
39        }
40    }
41
42    /// Clear XSD retry pending flag after starting retry.
43    #[must_use]
44    pub fn clear_xsd_retry_pending(self) -> Self {
45        Self {
46            xsd_retry_pending: false,
47            last_xsd_error: None,
48            last_review_xsd_error: None,
49            last_fix_xsd_error: None,
50            ..self
51        }
52    }
53
54    /// Check if XSD retries are exhausted.
55    #[must_use]
56    pub const fn xsd_retries_exhausted(&self) -> bool {
57        self.xsd_retry_count >= self.max_xsd_retry_count
58    }
59
60    /// Mark a same-agent retry as pending for a transient invocation failure.
61    #[must_use]
62    pub fn trigger_same_agent_retry(self, reason: SameAgentRetryReason) -> Self {
63        Self {
64            same_agent_retry_pending: true,
65            same_agent_retry_count: self.same_agent_retry_count.saturating_add(1),
66            same_agent_retry_reason: Some(reason),
67            ..self
68        }
69    }
70
71    /// Clear same-agent retry pending flag after starting retry.
72    #[must_use]
73    pub fn clear_same_agent_retry_pending(self) -> Self {
74        Self {
75            same_agent_retry_pending: false,
76            same_agent_retry_reason: None,
77            ..self
78        }
79    }
80
81    /// Check if same-agent retries are exhausted.
82    #[must_use]
83    pub const fn same_agent_retries_exhausted(&self) -> bool {
84        self.same_agent_retry_count >= self.max_same_agent_retry_count
85    }
86
87    /// Mark continuation as pending (output valid but work incomplete).
88    #[must_use]
89    pub fn trigger_continue(self) -> Self {
90        Self {
91            continue_pending: true,
92            ..self
93        }
94    }
95
96    /// Clear continue pending flag after starting continuation.
97    #[must_use]
98    pub fn clear_continue_pending(self) -> Self {
99        Self {
100            continue_pending: false,
101            ..self
102        }
103    }
104
105    /// Check if continuation attempts are exhausted.
106    ///
107    /// Returns `true` when `continuation_attempt >= max_continue_count`.
108    ///
109    /// # Semantics
110    ///
111    /// The `continuation_attempt` counter tracks how many times work has been attempted:
112    /// - 0: Initial attempt (before any continuation)
113    /// - 1: After first continuation
114    /// - 2: After second continuation
115    /// - etc.
116    ///
117    /// With `max_continue_count = 3`:
118    /// - Attempts 0, 1, 2 are allowed (3 total)
119    /// - Attempt 3+ triggers exhaustion
120    ///
121    /// # Exhaustion Behavior
122    ///
123    /// When continuation budget is exhausted (`ContinuationBudgetExhausted` event):
124    /// - If all agents exhausted AND status is Failed/Partial → transition to `AwaitingDevFix`
125    /// - Otherwise → complete current iteration (via `IterationCompleted`) and advance to next iteration
126    ///
127    /// This ensures bounded execution: the system never restarts the continuation cycle
128    /// with a fresh agent within the same iteration, preventing infinite loops when work
129    /// remains incomplete despite exhausting the continuation budget.
130    ///
131    /// # Naming Note
132    ///
133    /// The field is named `max_continue_count` rather than `max_total_attempts` because
134    /// it historically represented the maximum number of continuations. The actual
135    /// semantics are "maximum total attempts including initial".
136    #[must_use]
137    pub const fn continuations_exhausted(&self) -> bool {
138        self.continuation_attempt >= self.max_continue_count
139    }
140
141    /// Trigger a continuation with context from the previous attempt.
142    ///
143    /// Sets both `context_write_pending` (to write continuation context) and
144    /// `continue_pending` (to trigger the continuation flow in orchestration).
145    #[must_use]
146    pub fn trigger_continuation(
147        self,
148        status: DevelopmentStatus,
149        summary: String,
150        files_changed: Option<Vec<String>>,
151        next_steps: Option<String>,
152    ) -> Self {
153        let next_attempt = self.continuation_attempt.saturating_add(1);
154
155        if next_attempt >= self.max_continue_count {
156            return Self {
157                continue_pending: false,
158                context_write_pending: false,
159                context_cleanup_pending: false,
160                ..self
161            };
162        }
163
164        Self {
165            previous_status: Some(status),
166            previous_summary: Some(summary),
167            previous_files_changed: files_changed.map(std::vec::Vec::into_boxed_slice),
168            previous_next_steps: next_steps,
169            continuation_attempt: next_attempt,
170            invalid_output_attempts: 0,
171            context_write_pending: true,
172            context_cleanup_pending: false,
173            xsd_retry_count: 0,
174            xsd_retry_pending: false,
175            xsd_retry_session_reuse_pending: false,
176            last_xsd_error: None,
177            last_review_xsd_error: None,
178            last_fix_xsd_error: None,
179            same_agent_retry_count: 0,
180            same_agent_retry_pending: false,
181            same_agent_retry_reason: None,
182            continue_pending: true,
183            ..self
184        }
185    }
186
187    // =========================================================================
188    // Fix continuation methods
189    // =========================================================================
190
191    /// Check if fix continuations are exhausted.
192    ///
193    /// Semantics match `continuations_exhausted()`: with default `max_fix_continue_count`
194    /// of 10, attempts 0 through 9 are allowed (10 total), attempt 10+ is exhausted.
195    #[must_use]
196    pub const fn fix_continuations_exhausted(&self) -> bool {
197        self.fix_continuation_attempt >= self.max_fix_continue_count
198    }
199
200    /// Trigger a fix continuation with status context.
201    #[must_use]
202    pub fn trigger_fix_continuation(self, status: FixStatus, summary: Option<String>) -> Self {
203        Self {
204            fix_status: Some(status),
205            fix_previous_summary: summary,
206            fix_continuation_attempt: self.fix_continuation_attempt.saturating_add(1),
207            fix_continue_pending: true,
208            xsd_retry_count: 0,
209            xsd_retry_pending: false,
210            xsd_retry_session_reuse_pending: false,
211            last_xsd_error: None,
212            last_review_xsd_error: None,
213            last_fix_xsd_error: None,
214            invalid_output_attempts: 0,
215            context_write_pending: false,
216            context_cleanup_pending: false,
217            continue_pending: false,
218            ..self
219        }
220    }
221
222    /// Clear fix continuation pending flag after starting continuation.
223    #[must_use]
224    pub fn clear_fix_continue_pending(self) -> Self {
225        Self {
226            fix_continue_pending: false,
227            ..self
228        }
229    }
230
231    /// Reset fix continuation state (e.g., when entering a new review pass).
232    #[must_use]
233    pub fn reset_fix_continuation(self) -> Self {
234        Self {
235            fix_status: None,
236            fix_previous_summary: None,
237            fix_continuation_attempt: 0,
238            fix_continue_pending: false,
239            ..self
240        }
241    }
242}