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