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}