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 pub fn with_artifact(&self, artifact: ArtifactType) -> Self {
15 // Reset XSD retry state when switching artifacts, preserve everything else
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.clone()
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 pub fn trigger_xsd_retry(&self) -> Self {
33 Self {
34 xsd_retry_pending: true,
35 xsd_retry_count: self.xsd_retry_count + 1,
36 xsd_retry_session_reuse_pending: true,
37 ..self.clone()
38 }
39 }
40
41 /// Clear XSD retry pending flag after starting retry.
42 pub fn clear_xsd_retry_pending(&self) -> Self {
43 Self {
44 xsd_retry_pending: false,
45 last_xsd_error: None,
46 last_review_xsd_error: None,
47 last_fix_xsd_error: None,
48 ..self.clone()
49 }
50 }
51
52 /// Check if XSD retries are exhausted.
53 pub fn xsd_retries_exhausted(&self) -> bool {
54 self.xsd_retry_count >= self.max_xsd_retry_count
55 }
56
57 /// Mark a same-agent retry as pending for a transient invocation failure.
58 pub fn trigger_same_agent_retry(&self, reason: SameAgentRetryReason) -> Self {
59 Self {
60 same_agent_retry_pending: true,
61 same_agent_retry_count: self.same_agent_retry_count + 1,
62 same_agent_retry_reason: Some(reason),
63 ..self.clone()
64 }
65 }
66
67 /// Clear same-agent retry pending flag after starting retry.
68 pub fn clear_same_agent_retry_pending(&self) -> Self {
69 Self {
70 same_agent_retry_pending: false,
71 same_agent_retry_reason: None,
72 ..self.clone()
73 }
74 }
75
76 /// Check if same-agent retries are exhausted.
77 pub fn same_agent_retries_exhausted(&self) -> bool {
78 self.same_agent_retry_count >= self.max_same_agent_retry_count
79 }
80
81 /// Mark continuation as pending (output valid but work incomplete).
82 pub fn trigger_continue(&self) -> Self {
83 Self {
84 continue_pending: true,
85 ..self.clone()
86 }
87 }
88
89 /// Clear continue pending flag after starting continuation.
90 pub fn clear_continue_pending(&self) -> Self {
91 Self {
92 continue_pending: false,
93 ..self.clone()
94 }
95 }
96
97 /// Check if continuation attempts are exhausted.
98 ///
99 /// Returns `true` when `continuation_attempt >= max_continue_count`.
100 ///
101 /// # Semantics
102 ///
103 /// The `continuation_attempt` counter tracks how many times work has been attempted:
104 /// - 0: Initial attempt (before any continuation)
105 /// - 1: After first continuation
106 /// - 2: After second continuation
107 /// - etc.
108 ///
109 /// With `max_continue_count = 3`:
110 /// - Attempts 0, 1, 2 are allowed (3 total)
111 /// - Attempt 3+ triggers exhaustion
112 ///
113 /// # Naming Note
114 ///
115 /// The field is named `max_continue_count` rather than `max_total_attempts` because
116 /// it historically represented the maximum number of continuations. The actual
117 /// semantics are "maximum total attempts including initial".
118 pub fn continuations_exhausted(&self) -> bool {
119 self.continuation_attempt >= self.max_continue_count
120 }
121
122 /// Trigger a continuation with context from the previous attempt.
123 ///
124 /// Sets both `context_write_pending` (to write continuation context) and
125 /// `continue_pending` (to trigger the continuation flow in orchestration).
126 pub fn trigger_continuation(
127 &self,
128 status: DevelopmentStatus,
129 summary: String,
130 files_changed: Option<Vec<String>>,
131 next_steps: Option<String>,
132 ) -> Self {
133 Self {
134 previous_status: Some(status),
135 previous_summary: Some(summary),
136 previous_files_changed: files_changed,
137 previous_next_steps: next_steps,
138 continuation_attempt: self.continuation_attempt + 1,
139 invalid_output_attempts: 0,
140 context_write_pending: true,
141 context_cleanup_pending: false,
142 // Reset XSD retry count for new continuation attempt
143 xsd_retry_count: 0,
144 xsd_retry_pending: false,
145 xsd_retry_session_reuse_pending: false,
146 last_xsd_error: None,
147 last_review_xsd_error: None,
148 last_fix_xsd_error: None,
149 // Reset same-agent retry state for new continuation attempt
150 same_agent_retry_count: 0,
151 same_agent_retry_pending: false,
152 same_agent_retry_reason: None,
153 // Set continue_pending to trigger continuation in orchestration
154 continue_pending: true,
155 // Preserve artifact type and limits
156 current_artifact: self.current_artifact.clone(),
157 max_xsd_retry_count: self.max_xsd_retry_count,
158 max_same_agent_retry_count: self.max_same_agent_retry_count,
159 max_continue_count: self.max_continue_count,
160 // Preserve fix continuation fields
161 fix_status: self.fix_status.clone(),
162 fix_previous_summary: self.fix_previous_summary.clone(),
163 fix_continuation_attempt: self.fix_continuation_attempt,
164 fix_continue_pending: self.fix_continue_pending,
165 max_fix_continue_count: self.max_fix_continue_count,
166 // Preserve loop detection fields
167 last_effect_kind: self.last_effect_kind.clone(),
168 consecutive_same_effect_count: self.consecutive_same_effect_count,
169 max_consecutive_same_effect: self.max_consecutive_same_effect,
170 }
171 }
172
173 // =========================================================================
174 // Fix continuation methods
175 // =========================================================================
176
177 /// Check if fix continuations are exhausted.
178 ///
179 /// Semantics match `continuations_exhausted()`: with default `max_fix_continue_count`
180 /// of 3, attempts 0, 1, 2 are allowed (3 total), attempt 3+ is exhausted.
181 pub fn fix_continuations_exhausted(&self) -> bool {
182 self.fix_continuation_attempt >= self.max_fix_continue_count
183 }
184
185 /// Trigger a fix continuation with status context.
186 pub fn trigger_fix_continuation(&self, status: FixStatus, summary: Option<String>) -> Self {
187 Self {
188 fix_status: Some(status),
189 fix_previous_summary: summary,
190 fix_continuation_attempt: self.fix_continuation_attempt + 1,
191 fix_continue_pending: true,
192 // Reset XSD retry state for new continuation
193 xsd_retry_count: 0,
194 xsd_retry_pending: false,
195 xsd_retry_session_reuse_pending: false,
196 last_xsd_error: None,
197 last_review_xsd_error: None,
198 last_fix_xsd_error: None,
199 // Reset invalid output attempts for new continuation
200 invalid_output_attempts: 0,
201 // Clear other pending flags
202 context_write_pending: false,
203 context_cleanup_pending: false,
204 continue_pending: false,
205 // Preserve all other fields via spread operator
206 ..self.clone()
207 }
208 }
209
210 /// Clear fix continuation pending flag after starting continuation.
211 pub fn clear_fix_continue_pending(&self) -> Self {
212 Self {
213 fix_continue_pending: false,
214 ..self.clone()
215 }
216 }
217
218 /// Reset fix continuation state (e.g., when entering a new review pass).
219 pub fn reset_fix_continuation(&self) -> Self {
220 Self {
221 fix_status: None,
222 fix_previous_summary: None,
223 fix_continuation_attempt: 0,
224 fix_continue_pending: false,
225 ..self.clone()
226 }
227 }
228}