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}