1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
//! Continuation and retry logic reducer
//!
//! Handles events related to:
//! - Continuation flow (`ContinuationTriggered`, `ContinuationSucceeded`, `ContinuationBudgetExhausted`)
//! - XSD retry logic (`OutputValidationFailed`, `XmlMissing`)
//! - Context management (`ContinuationContextWritten`, `ContinuationContextCleaned`)
use crate::agents::DrainMode;
use crate::reducer::event::DevelopmentEvent;
use crate::reducer::state::{ContinuationState, DevelopmentStatus, PipelineState};
use super::reduce_development_event;
pub(super) fn reduce_continuation_event(
state: PipelineState,
event: DevelopmentEvent,
) -> PipelineState {
match event {
DevelopmentEvent::ContinuationTriggered {
iteration,
status,
summary,
files_changed,
next_steps,
} => {
// Trigger continuation with context from the previous attempt
let old_attempt = state.continuation.continuation_attempt;
let new_continuation =
state
.continuation
.trigger_continuation(status, summary, files_changed, next_steps);
let new_attempt = new_continuation.continuation_attempt;
// Only increment metrics if the continuation counter actually incremented.
// The defensive check in trigger_continuation may prevent the increment when
// at the budget boundary, in which case metrics should also not increment.
let metrics = if new_attempt > old_attempt {
state.metrics.increment_dev_continuation_attempt()
} else {
state.metrics
};
PipelineState {
iteration,
agent_chain: state.agent_chain.with_mode(DrainMode::Continuation),
continuation: new_continuation,
development_context_prepared_iteration: None,
development_prompt_prepared_iteration: None,
development_required_files_cleaned_iteration: None,
development_agent_invoked_iteration: None,
// IMPORTANT: analysis must run after EVERY development-agent invocation.
// Reset this marker so the orchestrator will invoke analysis for the new
// continuation attempt within the same iteration.
analysis_agent_invoked_iteration: None,
development_xml_extracted_iteration: None,
development_validated_outcome: None,
development_xml_archived_iteration: None,
metrics,
..state
}
}
DevelopmentEvent::ContinuationSucceeded {
iteration,
total_continuation_attempts: _,
} => {
// Continuation succeeded; proceed to CommitMessage and reset continuation state.
PipelineState {
phase: crate::reducer::event::PipelinePhase::CommitMessage,
previous_phase: Some(crate::reducer::event::PipelinePhase::Development),
iteration,
commit: crate::reducer::state::CommitState::NotStarted,
commit_prompt_prepared: false,
commit_diff_prepared: false,
commit_diff_empty: false,
commit_agent_invoked: false,
commit_required_files_cleaned: false,
commit_xml_extracted: false,
commit_validated_outcome: None,
commit_xml_archived: false,
context_cleaned: false,
continuation: ContinuationState {
context_cleanup_pending: true,
..state.continuation.reset()
},
agent_chain: state.agent_chain.with_mode(DrainMode::Normal),
development_context_prepared_iteration: None,
development_prompt_prepared_iteration: None,
development_required_files_cleaned_iteration: None,
development_agent_invoked_iteration: None,
development_xml_extracted_iteration: None,
development_validated_outcome: None,
development_xml_archived_iteration: None,
metrics: state.metrics.increment_dev_iterations_completed(),
..state
}
}
DevelopmentEvent::OutputValidationFailed { iteration, attempt }
| DevelopmentEvent::XmlMissing { iteration, attempt } => {
// Policy: After configured XSD retries are exhausted, switch to next agent.
// This keeps invalid output retry logic in the reducer, not the handler.
let new_xsd_count = state.continuation.xsd_retry_count + 1;
// Only increment metrics if we're actually retrying (not exhausted)
let will_retry = new_xsd_count < state.continuation.max_xsd_retry_count;
if new_xsd_count >= state.continuation.max_xsd_retry_count {
// XSD retries exhausted - switch to next agent
let new_agent_chain = state.agent_chain.switch_to_next_agent().clear_session_id();
PipelineState {
phase: crate::reducer::event::PipelinePhase::Development,
iteration,
agent_chain: new_agent_chain.with_mode(DrainMode::Normal),
continuation: ContinuationState {
invalid_output_attempts: 0,
xsd_retry_count: 0,
xsd_retry_pending: false,
xsd_retry_session_reuse_pending: false,
same_agent_retry_count: 0,
same_agent_retry_pending: false,
same_agent_retry_reason: None,
..state.continuation
},
// IMPORTANT: XSD retry is for the analysis agent's XML output.
// Preserve developer-agent progress and retry analysis only.
development_context_prepared_iteration: state
.development_context_prepared_iteration,
development_prompt_prepared_iteration: state
.development_prompt_prepared_iteration,
development_required_files_cleaned_iteration: state
.development_required_files_cleaned_iteration,
development_agent_invoked_iteration: state.development_agent_invoked_iteration,
analysis_agent_invoked_iteration: None,
development_xml_extracted_iteration: None,
development_validated_outcome: None,
development_xml_archived_iteration: None,
metrics: if will_retry {
state.metrics.increment_xsd_retry_development()
} else {
state.metrics
},
..state
}
} else {
// Stay in Development, increment attempt counters, set retry pending
PipelineState {
phase: crate::reducer::event::PipelinePhase::Development,
iteration,
agent_chain: state.agent_chain.with_mode(DrainMode::XsdRetry),
continuation: ContinuationState {
invalid_output_attempts: attempt + 1,
xsd_retry_count: new_xsd_count,
xsd_retry_pending: true,
// Reuse last session id for analysis XSD retry when available.
xsd_retry_session_reuse_pending: true,
..state.continuation
},
// Preserve developer-agent progress and retry analysis only.
development_context_prepared_iteration: state
.development_context_prepared_iteration,
development_prompt_prepared_iteration: state
.development_prompt_prepared_iteration,
development_required_files_cleaned_iteration: state
.development_required_files_cleaned_iteration,
development_agent_invoked_iteration: state.development_agent_invoked_iteration,
analysis_agent_invoked_iteration: None,
development_xml_extracted_iteration: None,
development_validated_outcome: None,
development_xml_archived_iteration: None,
metrics: if will_retry {
state.metrics.increment_xsd_retry_development()
} else {
state.metrics
},
..state
}
}
}
DevelopmentEvent::ContinuationBudgetExhausted {
iteration,
total_attempts: _,
last_status,
} => {
// CRITICAL FIX: After continuation budget exhaustion, COMPLETE the iteration
// rather than falling back to another agent within the same iteration.
//
// Previous behavior: Switch to next agent and stay in Development phase
// → Created infinite loop: attempt 1→2→exhaust→switch→restart→1→2→exhaust...
//
// New behavior: Complete the iteration (even if work incomplete) and either:
// 1. Advance to next iteration if dev_iters remain
// 2. Transition to AwaitingDevFix if all iterations exhausted
//
// This ensures bounded execution: after max_continue_count attempts fail,
// the system moves forward rather than cycling indefinitely with fresh agents.
let new_agent_chain = state.agent_chain.switch_to_next_agent().clear_session_id();
// Check if we should transition to remediation flow
if new_agent_chain.is_exhausted()
&& matches!(
last_status,
DevelopmentStatus::Failed | DevelopmentStatus::Partial
)
{
// All agents exhausted AND work incomplete → try dev-fix flow
PipelineState {
phase: crate::reducer::event::PipelinePhase::AwaitingDevFix,
previous_phase: Some(crate::reducer::event::PipelinePhase::Development),
iteration,
agent_chain: new_agent_chain.with_mode(DrainMode::Normal),
dev_fix_triggered: false,
continuation: ContinuationState {
continuation_attempt: 0,
invalid_output_attempts: 0,
xsd_retry_count: 0,
xsd_retry_pending: false,
xsd_retry_session_reuse_pending: false,
same_agent_retry_count: 0,
same_agent_retry_pending: false,
same_agent_retry_reason: None,
context_cleanup_pending: false,
..state.continuation
},
development_context_prepared_iteration: None,
development_prompt_prepared_iteration: None,
development_required_files_cleaned_iteration: None,
development_agent_invoked_iteration: None,
development_xml_extracted_iteration: None,
development_validated_outcome: None,
development_xml_archived_iteration: None,
..state
}
} else {
// Agents remain OR work complete → COMPLETE iteration and advance
//
// CRITICAL: Do NOT stay in Development phase with reset continuation state.
// This would restart the continuation cycle, creating the infinite loop.
//
// Instead, emit IterationCompleted to advance to the next iteration or
// proceed to the next pipeline phase.
let next_event = DevelopmentEvent::IterationCompleted {
iteration,
// Mark as output_valid even if status is Partial/Failed, because we've
// exhausted our continuation budget and need to move forward rather than
// loop indefinitely. The summary will reflect the incomplete status.
output_valid: true,
};
// Reset continuation state and agent chain for next iteration
let state_after_completion = PipelineState {
continuation: ContinuationState {
continuation_attempt: 0,
invalid_output_attempts: 0,
xsd_retry_count: 0,
xsd_retry_pending: false,
xsd_retry_session_reuse_pending: false,
same_agent_retry_count: 0,
same_agent_retry_pending: false,
same_agent_retry_reason: None,
context_cleanup_pending: true,
..state.continuation
},
agent_chain: new_agent_chain.reset().with_mode(DrainMode::Normal),
..state
};
// Process IterationCompleted event through the reducer
reduce_development_event(state_after_completion, next_event)
}
}
DevelopmentEvent::ContinuationContextWritten {
iteration,
attempt: _,
} => {
// Context file was written, state remains unchanged.
// The continuation state is already set by ContinuationTriggered.
PipelineState {
iteration,
continuation: crate::reducer::state::ContinuationState {
context_write_pending: false,
..state.continuation
},
..state
}
}
DevelopmentEvent::ContinuationContextCleaned => {
// Context file was cleaned up, no state change needed.
PipelineState {
continuation: crate::reducer::state::ContinuationState {
context_cleanup_pending: false,
..state.continuation
},
..state
}
}
// These events are handled by iteration_reducer
_ => state,
}
}