Skip to main content

ralph_workflow/reducer/state/
metrics.rs

1// Run-level execution metrics for the pipeline.
2//
3// This is the single source of truth for all iteration/attempt/retry/fallback statistics.
4//
5// # Where Metrics Are Updated
6//
7// Metrics are updated **only** in reducer code paths (`state_reduction/*.rs`):
8//
9// - `development.rs`: dev_iterations_started, dev_iterations_completed,
10//                     dev_attempts_total, dev_continuation_attempt,
11//                     analysis_attempts_*, xsd_retry_development
12// - `review.rs`: review_passes_started, review_passes_completed, review_runs_total,
13//                fix_runs_total, fix_continuations_total, fix_continuation_attempt,
14//                current_review_pass, xsd_retry_review, xsd_retry_fix
15// - `commit.rs`: commits_created_total, xsd_retry_commit
16// - `planning.rs`: xsd_retry_planning
17// - `agent.rs`: same_agent_retry_attempts_total, agent_fallbacks_total,
18//               model_fallbacks_total, retry_cycles_started_total
19//
20// # Event-to-Metric Mapping
21//
22// | Metric                              | Incremented On Event                                      | Notes                                    |
23// |-------------------------------------|-----------------------------------------------------------|------------------------------------------|
24// | dev_iterations_started              | DevelopmentEvent::IterationStarted                        | Not incremented on continuations         |
25// | dev_iterations_completed            | DevelopmentEvent::IterationCompleted { output_valid: true } | Advanced to commit phase                |
26// |                                     | DevelopmentEvent::ContinuationSucceeded                   | Continuation advanced to commit phase    |
27// | dev_attempts_total                  | DevelopmentEvent::AgentInvoked                            | Includes initial + continuations         |
28// | dev_continuation_attempt            | DevelopmentEvent::ContinuationTriggered                   | Reset on IterationStarted                |
29// | analysis_attempts_total             | DevelopmentEvent::AnalysisAgentInvoked                    | Total across all iterations              |
30// | analysis_attempts_in_current_iteration | DevelopmentEvent::AnalysisAgentInvoked                 | Reset on IterationStarted                |
31// | review_passes_started               | ReviewEvent::PassStarted                                  | Increments when pass != previous         |
32// | review_passes_completed             | ReviewEvent::Completed { issues_found: false }            | Clean pass                               |
33// |                                     | ReviewEvent::PassCompletedClean                           | Alternative event for clean pass         |
34// |                                     | ReviewEvent::FixAttemptCompleted                          | Fix completed, pass advances             |
35// | review_runs_total                   | ReviewEvent::AgentInvoked                                 | Total reviewer invocations               |
36// | fix_runs_total                      | ReviewEvent::FixAgentInvoked                              | Total fix invocations                    |
37// | fix_continuations_total             | ReviewEvent::FixContinuationTriggered                     | Fix continuation attempts                |
38// | fix_continuation_attempt            | ReviewEvent::FixContinuationTriggered                     | Reset on PassStarted                     |
39// | current_review_pass                 | ReviewEvent::PassStarted                                  | Tracks current pass number               |
40// | xsd_retry_*                         | *Event::OutputValidationFailed (when will_retry == true)  | Only when retrying, not when exhausted   |
41// | same_agent_retry_attempts_total     | AgentEvent::TimedOut / InternalError (when will_retry)    | Only when retrying same agent            |
42// | timeout_no_output_agent_switches_total | AgentEvent::TimedOut { output_kind: NoOutput }         | NoOutput timeout triggered immediate switch |
43// | agent_fallbacks_total               | AgentEvent::FallbackTriggered                             | Agent switched in chain                  |
44// | model_fallbacks_total               | AgentEvent::ModelFallbackTriggered                        | Model switched for agent                 |
45// | retry_cycles_started_total          | AgentEvent::RetryCycleStarted                             | Chain exhausted, restarting              |
46// | commits_created_total               | CommitEvent::Created                                      | Actual git commit created                |
47//
48// # How to Add New Metrics
49//
50// 1. Add field to `RunMetrics` struct with `#[serde(default)]`
51// 2. Update `RunMetrics::new()` if config-derived display field
52// 3. Update appropriate reducer in `state_reduction/` to increment on event
53// 4. Add unit test in `state_reduction/tests/metrics.rs`
54// 5. Update `finalize_pipeline()` if displayed in final summary
55// 6. Add checkpoint compatibility test
56//
57// # Checkpoint Compatibility
58//
59// All fields have `#[serde(default)]` to ensure old checkpoints can be loaded
60// with new metrics fields defaulting to 0.
61
62/// Run-level execution metrics tracked by the reducer.
63///
64/// This struct provides a complete picture of pipeline execution progress,
65/// including iteration counts, attempt counts, retry counts, and fallback events.
66/// All fields are monotonic counters that only increment during a run.
67///
68/// # Checkpoint Compatibility
69///
70/// All fields have `#[serde(default)]` to ensure backward compatibility when
71/// loading checkpoints created before metrics were added or when new fields
72/// are introduced in future versions.
73///
74/// # Single Source of Truth
75///
76/// The reducer is the **only** code that mutates these metrics. They are
77/// updated deterministically based on events, ensuring:
78/// - Metrics survive checkpoint/resume
79/// - No drift between runtime state and actual progress
80/// - Final summary is always consistent with reducer state
81#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
82pub struct RunMetrics {
83    // Development iteration tracking
84    /// Number of development iterations started.
85    /// Incremented on `DevelopmentEvent::IterationStarted` (not on continuations).
86    #[serde(default)]
87    pub dev_iterations_started: u32,
88    /// Number of development iterations completed (advanced to commit phase).
89    /// A dev iteration is "completed" when the reducer transitions to `PipelinePhase::CommitMessage`
90    /// after dev output is valid, regardless of whether an actual git commit is created.
91    /// Incremented on `DevelopmentEvent::IterationCompleted { output_valid: true }` and
92    /// `DevelopmentEvent::ContinuationSucceeded`.
93    #[serde(default)]
94    pub dev_iterations_completed: u32,
95    /// Total number of developer agent invocations (includes continuations).
96    #[serde(default)]
97    pub dev_attempts_total: u32,
98    /// Current continuation attempt within the current development iteration (0 = initial).
99    /// Reset when starting a new iteration.
100    #[serde(default)]
101    pub dev_continuation_attempt: u32,
102
103    // Analysis tracking
104    /// Total number of analysis agent invocations across all iterations.
105    #[serde(default)]
106    pub analysis_attempts_total: u32,
107    /// Analysis attempts in the current development iteration (reset per iteration).
108    #[serde(default)]
109    pub analysis_attempts_in_current_iteration: u32,
110
111    // Review tracking
112    /// Number of review passes started.
113    /// Incremented on `ReviewEvent::PassStarted` when `pass != previous_pass`.
114    #[serde(default)]
115    pub review_passes_started: u32,
116    /// Number of review passes completed (advanced past without issues or after fixes).
117    /// A review pass is "completed" when it advances to the next pass or to commit phase,
118    /// either because no issues were found or because fixes were successfully applied.
119    /// Incremented on `ReviewEvent::Completed { issues_found: false }`,
120    /// `ReviewEvent::PassCompletedClean`, and `ReviewEvent::FixAttemptCompleted`.
121    #[serde(default)]
122    pub review_passes_completed: u32,
123    /// Total number of reviewer agent invocations.
124    #[serde(default)]
125    pub review_runs_total: u32,
126    /// Total number of fix agent invocations.
127    #[serde(default)]
128    pub fix_runs_total: u32,
129    /// Total number of fix analysis agent invocations.
130    ///
131    /// This tracks the independent verification step after every fix agent invocation.
132    #[serde(default)]
133    pub fix_analysis_runs_total: u32,
134    /// Total number of fix continuation attempts.
135    #[serde(default)]
136    pub fix_continuations_total: u32,
137    /// Current fix continuation attempt within the current review pass (0 = initial).
138    ///
139    /// Reset when starting a new review pass.
140    /// Note: fix-attempt boundaries do not reset this counter; it is scoped to the review pass.
141    #[serde(default)]
142    pub fix_continuation_attempt: u32,
143    /// Current review pass number (for X/Y display).
144    #[serde(default)]
145    pub current_review_pass: u32,
146
147    // XSD retry tracking
148    /// Total XSD retry attempts across all phases.
149    #[serde(default)]
150    pub xsd_retry_attempts_total: u32,
151    /// XSD retry attempts in planning phase.
152    #[serde(default)]
153    pub xsd_retry_planning: u32,
154    /// XSD retry attempts in development/analysis phase.
155    #[serde(default)]
156    pub xsd_retry_development: u32,
157    /// XSD retry attempts in review phase.
158    #[serde(default)]
159    pub xsd_retry_review: u32,
160    /// XSD retry attempts in fix phase.
161    #[serde(default)]
162    pub xsd_retry_fix: u32,
163    /// XSD retry attempts in commit phase.
164    #[serde(default)]
165    pub xsd_retry_commit: u32,
166
167    // Same-agent retry tracking
168    /// Total same-agent retry attempts (for transient failures like timeout).
169    #[serde(default)]
170    pub same_agent_retry_attempts_total: u32,
171
172    /// Agent switches caused by no-output timeouts.
173    ///
174    /// Incremented only in the reducer's `TimedOut { output_kind: NoOutput }` arm.
175    /// Distinct from `agent_fallbacks_total` (which tracks `FallbackTriggered` events).
176    /// Distinct from `same_agent_retry_attempts_total` (`NoOutput` does not retry same agent).
177    #[serde(default)]
178    pub timeout_no_output_agent_switches_total: u32,
179
180    // Agent/model fallback tracking
181    /// Total agent fallback events.
182    #[serde(default)]
183    pub agent_fallbacks_total: u32,
184    /// Total model fallback events.
185    #[serde(default)]
186    pub model_fallbacks_total: u32,
187    /// Total retry cycles started (agent chain exhaustion + restart).
188    #[serde(default)]
189    pub retry_cycles_started_total: u32,
190
191    // Commit tracking
192    /// Total commits created during the run.
193    #[serde(default)]
194    pub commits_created_total: u32,
195
196    // Config-derived display fields (set once at init, not serialized from events)
197    /// Maximum development iterations (from config, for X/Y display).
198    #[serde(default)]
199    pub max_dev_iterations: u32,
200    /// Maximum review passes (from config, for X/Y display).
201    #[serde(default)]
202    pub max_review_passes: u32,
203    /// Maximum XSD retry count (from config, for X/max display).
204    #[serde(default)]
205    pub max_xsd_retry_count: u32,
206    /// Maximum development continuation count (from config, for X/max display).
207    #[serde(default)]
208    pub max_dev_continuation_count: u32,
209    /// Maximum fix continuation count (from config, for X/max display).
210    #[serde(default)]
211    pub max_fix_continuation_count: u32,
212    /// Maximum same-agent retry count (from config, for X/max display).
213    #[serde(default)]
214    pub max_same_agent_retry_count: u32,
215}
216
217impl RunMetrics {
218    /// Create metrics with config-derived display fields.
219    #[must_use]
220    pub fn new(
221        max_dev_iterations: u32,
222        max_review_passes: u32,
223        continuation: &ContinuationState,
224    ) -> Self {
225        Self {
226            max_dev_iterations,
227            max_review_passes,
228            max_xsd_retry_count: continuation.max_xsd_retry_count,
229            max_dev_continuation_count: continuation.max_continue_count,
230            max_fix_continuation_count: continuation.max_fix_continue_count,
231            max_same_agent_retry_count: continuation.max_same_agent_retry_count,
232            ..Self::default()
233        }
234    }
235
236    // =========================================================================
237    // Builder methods for updating metrics without full struct clones
238    // =========================================================================
239    // These methods consume self and return a new instance with the updated field.
240    // This eliminates the need to clone all 40+ fields when updating a single metric.
241
242    // Development metrics
243    #[must_use]
244    pub const fn increment_dev_iterations_started(mut self) -> Self {
245        self.dev_iterations_started += 1;
246        self
247    }
248
249    #[must_use]
250    pub const fn increment_dev_iterations_completed(mut self) -> Self {
251        self.dev_iterations_completed += 1;
252        self
253    }
254
255    #[must_use]
256    pub const fn increment_dev_attempts_total(mut self) -> Self {
257        self.dev_attempts_total += 1;
258        self
259    }
260
261    #[must_use]
262    pub const fn increment_dev_continuation_attempt(mut self) -> Self {
263        self.dev_continuation_attempt += 1;
264        self
265    }
266
267    #[must_use]
268    pub const fn reset_dev_continuation_attempt(mut self) -> Self {
269        self.dev_continuation_attempt = 0;
270        self
271    }
272
273    // Analysis metrics
274    #[must_use]
275    pub const fn increment_analysis_attempts_total(mut self) -> Self {
276        self.analysis_attempts_total += 1;
277        self
278    }
279
280    #[must_use]
281    pub const fn increment_analysis_attempts_in_current_iteration(mut self) -> Self {
282        self.analysis_attempts_in_current_iteration += 1;
283        self
284    }
285
286    #[must_use]
287    pub const fn reset_analysis_attempts_in_current_iteration(mut self) -> Self {
288        self.analysis_attempts_in_current_iteration = 0;
289        self
290    }
291
292    // Review metrics
293    #[must_use]
294    pub const fn increment_review_passes_started(mut self) -> Self {
295        self.review_passes_started += 1;
296        self
297    }
298
299    #[must_use]
300    pub const fn increment_review_passes_completed(mut self) -> Self {
301        self.review_passes_completed += 1;
302        self
303    }
304
305    #[must_use]
306    pub const fn increment_review_runs_total(mut self) -> Self {
307        self.review_runs_total += 1;
308        self
309    }
310
311    #[must_use]
312    pub const fn increment_fix_runs_total(mut self) -> Self {
313        self.fix_runs_total += 1;
314        self
315    }
316
317    #[must_use]
318    pub const fn increment_fix_analysis_runs_total(mut self) -> Self {
319        self.fix_analysis_runs_total += 1;
320        self
321    }
322
323    #[must_use]
324    pub const fn increment_fix_continuations_total(mut self) -> Self {
325        self.fix_continuations_total += 1;
326        self
327    }
328
329    #[must_use]
330    pub const fn increment_fix_continuation_attempt(mut self) -> Self {
331        self.fix_continuation_attempt += 1;
332        self
333    }
334
335    #[must_use]
336    pub const fn reset_fix_continuation_attempt(mut self) -> Self {
337        self.fix_continuation_attempt = 0;
338        self
339    }
340
341    #[must_use]
342    pub const fn set_current_review_pass(mut self, pass: u32) -> Self {
343        self.current_review_pass = pass;
344        self
345    }
346
347    // XSD retry metrics
348    #[must_use]
349    pub const fn increment_xsd_retry_attempts_total(mut self) -> Self {
350        self.xsd_retry_attempts_total += 1;
351        self
352    }
353
354    #[must_use]
355    pub const fn increment_xsd_retry_planning(mut self) -> Self {
356        self.xsd_retry_planning += 1;
357        self.xsd_retry_attempts_total += 1;
358        self
359    }
360
361    #[must_use]
362    pub const fn increment_xsd_retry_development(mut self) -> Self {
363        self.xsd_retry_development += 1;
364        self.xsd_retry_attempts_total += 1;
365        self
366    }
367
368    #[must_use]
369    pub const fn increment_xsd_retry_review(mut self) -> Self {
370        self.xsd_retry_review += 1;
371        self.xsd_retry_attempts_total += 1;
372        self
373    }
374
375    #[must_use]
376    pub const fn increment_xsd_retry_fix(mut self) -> Self {
377        self.xsd_retry_fix += 1;
378        self.xsd_retry_attempts_total += 1;
379        self
380    }
381
382    #[must_use]
383    pub const fn increment_xsd_retry_commit(mut self) -> Self {
384        self.xsd_retry_commit += 1;
385        self.xsd_retry_attempts_total += 1;
386        self
387    }
388
389    // Same-agent retry metrics
390    #[must_use]
391    pub const fn increment_same_agent_retry_attempts_total(mut self) -> Self {
392        self.same_agent_retry_attempts_total += 1;
393        self
394    }
395
396    /// Increment `timeout_no_output_agent_switches_total` counter.
397    ///
398    /// Called only when a `TimedOut { output_kind: NoOutput }` event triggers
399    /// an immediate agent switch.
400    #[must_use]
401    pub const fn increment_timeout_no_output_agent_switches_total(mut self) -> Self {
402        self.timeout_no_output_agent_switches_total += 1;
403        self
404    }
405
406    // Agent/model fallback metrics
407    #[must_use]
408    pub const fn increment_agent_fallbacks_total(mut self) -> Self {
409        self.agent_fallbacks_total += 1;
410        self
411    }
412
413    #[must_use]
414    pub const fn increment_model_fallbacks_total(mut self) -> Self {
415        self.model_fallbacks_total += 1;
416        self
417    }
418
419    #[must_use]
420    pub const fn increment_retry_cycles_started_total(mut self) -> Self {
421        self.retry_cycles_started_total += 1;
422        self
423    }
424
425    // Commit metrics
426    #[must_use]
427    pub const fn increment_commits_created_total(mut self) -> Self {
428        self.commits_created_total += 1;
429        self
430    }
431}