1use std::sync::Arc;
7
8use super::backoff::calculate_backoff_delay_ms;
9use super::{AgentChainState, AgentDrain, AgentRole, DrainMode, RateLimitContinuationPrompt};
10
11impl AgentChainState {
12 #[must_use]
13 pub fn advance_to_next_model(&self) -> Self {
14 let start_agent_index = self.current_agent_index;
15
16 let mut next = match self.models_per_agent.get(self.current_agent_index) {
20 Some(models) if !models.is_empty() => {
21 if self.current_model_index + 1 < models.len() {
22 Self {
24 agents: Arc::clone(&self.agents),
25 current_agent_index: self.current_agent_index,
26 models_per_agent: Arc::clone(&self.models_per_agent),
27 current_model_index: self.current_model_index + 1,
28 retry_cycle: self.retry_cycle,
29 max_cycles: self.max_cycles,
30 retry_delay_ms: self.retry_delay_ms,
31 backoff_multiplier: self.backoff_multiplier,
32 max_backoff_ms: self.max_backoff_ms,
33 backoff_pending_ms: self.backoff_pending_ms,
34 current_role: self.current_role,
35 current_drain: self.current_drain,
36 current_mode: self.current_mode,
37 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
38 last_session_id: self.last_session_id.clone(),
39 }
40 } else {
41 self.switch_to_next_agent()
42 }
43 }
44 _ => self.switch_to_next_agent(),
45 };
46
47 if next.current_agent_index != start_agent_index {
48 next.last_session_id = None;
49 }
50
51 next
52 }
53
54 #[must_use]
55 pub fn switch_to_next_agent(&self) -> Self {
56 if self.current_agent_index + 1 < self.agents.len() {
57 Self {
59 agents: Arc::clone(&self.agents),
60 current_agent_index: self.current_agent_index + 1,
61 models_per_agent: Arc::clone(&self.models_per_agent),
62 current_model_index: 0,
63 retry_cycle: self.retry_cycle,
64 max_cycles: self.max_cycles,
65 retry_delay_ms: self.retry_delay_ms,
66 backoff_multiplier: self.backoff_multiplier,
67 max_backoff_ms: self.max_backoff_ms,
68 backoff_pending_ms: None,
69 current_role: self.current_role,
70 current_drain: self.current_drain,
71 current_mode: self.current_mode,
72 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
73 last_session_id: self.last_session_id.clone(),
74 }
75 } else {
76 let new_retry_cycle = self.retry_cycle + 1;
78 let new_backoff_pending_ms = if new_retry_cycle >= self.max_cycles {
79 None
80 } else {
81 let temp = Self {
83 agents: Arc::clone(&self.agents),
84 current_agent_index: 0,
85 models_per_agent: Arc::clone(&self.models_per_agent),
86 current_model_index: 0,
87 retry_cycle: new_retry_cycle,
88 max_cycles: self.max_cycles,
89 retry_delay_ms: self.retry_delay_ms,
90 backoff_multiplier: self.backoff_multiplier,
91 max_backoff_ms: self.max_backoff_ms,
92 backoff_pending_ms: None,
93 current_role: self.current_role,
94 current_drain: self.current_drain,
95 current_mode: self.current_mode,
96 rate_limit_continuation_prompt: None,
97 last_session_id: None,
98 };
99 Some(temp.calculate_backoff_delay_ms_for_retry_cycle())
100 };
101
102 Self {
103 agents: Arc::clone(&self.agents),
104 current_agent_index: 0,
105 models_per_agent: Arc::clone(&self.models_per_agent),
106 current_model_index: 0,
107 retry_cycle: new_retry_cycle,
108 max_cycles: self.max_cycles,
109 retry_delay_ms: self.retry_delay_ms,
110 backoff_multiplier: self.backoff_multiplier,
111 max_backoff_ms: self.max_backoff_ms,
112 backoff_pending_ms: new_backoff_pending_ms,
113 current_role: self.current_role,
114 current_drain: self.current_drain,
115 current_mode: self.current_mode,
116 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
117 last_session_id: self.last_session_id.clone(),
118 }
119 }
120 }
121
122 #[must_use]
127 pub fn switch_to_agent_named(&self, to_agent: &str) -> Self {
128 let Some(target_index) = self.agents.iter().position(|a| a == to_agent) else {
129 return self.switch_to_next_agent();
130 };
131
132 if target_index == self.current_agent_index {
133 return Self {
135 agents: Arc::clone(&self.agents),
136 current_agent_index: self.current_agent_index,
137 models_per_agent: Arc::clone(&self.models_per_agent),
138 current_model_index: 0,
139 retry_cycle: self.retry_cycle,
140 max_cycles: self.max_cycles,
141 retry_delay_ms: self.retry_delay_ms,
142 backoff_multiplier: self.backoff_multiplier,
143 max_backoff_ms: self.max_backoff_ms,
144 backoff_pending_ms: None,
145 current_role: self.current_role,
146 current_drain: self.current_drain,
147 current_mode: self.current_mode,
148 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
149 last_session_id: self.last_session_id.clone(),
150 };
151 }
152
153 if target_index <= self.current_agent_index {
154 let new_retry_cycle = self.retry_cycle + 1;
156 let new_backoff_pending_ms = if new_retry_cycle >= self.max_cycles && target_index == 0
157 {
158 None
159 } else {
160 let temp = Self {
162 agents: Arc::clone(&self.agents),
163 current_agent_index: target_index,
164 models_per_agent: Arc::clone(&self.models_per_agent),
165 current_model_index: 0,
166 retry_cycle: new_retry_cycle,
167 max_cycles: self.max_cycles,
168 retry_delay_ms: self.retry_delay_ms,
169 backoff_multiplier: self.backoff_multiplier,
170 max_backoff_ms: self.max_backoff_ms,
171 backoff_pending_ms: None,
172 current_role: self.current_role,
173 current_drain: self.current_drain,
174 current_mode: self.current_mode,
175 rate_limit_continuation_prompt: None,
176 last_session_id: None,
177 };
178 Some(temp.calculate_backoff_delay_ms_for_retry_cycle())
179 };
180
181 Self {
182 agents: Arc::clone(&self.agents),
183 current_agent_index: target_index,
184 models_per_agent: Arc::clone(&self.models_per_agent),
185 current_model_index: 0,
186 retry_cycle: new_retry_cycle,
187 max_cycles: self.max_cycles,
188 retry_delay_ms: self.retry_delay_ms,
189 backoff_multiplier: self.backoff_multiplier,
190 max_backoff_ms: self.max_backoff_ms,
191 backoff_pending_ms: new_backoff_pending_ms,
192 current_role: self.current_role,
193 current_drain: self.current_drain,
194 current_mode: self.current_mode,
195 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
196 last_session_id: self.last_session_id.clone(),
197 }
198 } else {
199 Self {
201 agents: Arc::clone(&self.agents),
202 current_agent_index: target_index,
203 models_per_agent: Arc::clone(&self.models_per_agent),
204 current_model_index: 0,
205 retry_cycle: self.retry_cycle,
206 max_cycles: self.max_cycles,
207 retry_delay_ms: self.retry_delay_ms,
208 backoff_multiplier: self.backoff_multiplier,
209 max_backoff_ms: self.max_backoff_ms,
210 backoff_pending_ms: None,
211 current_role: self.current_role,
212 current_drain: self.current_drain,
213 current_mode: self.current_mode,
214 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
215 last_session_id: self.last_session_id.clone(),
216 }
217 }
218 }
219
220 #[must_use]
227 pub fn switch_to_next_agent_with_prompt(&self, prompt: Option<String>) -> Self {
228 let base = self.switch_to_next_agent();
229 Self {
231 agents: base.agents,
232 current_agent_index: base.current_agent_index,
233 models_per_agent: base.models_per_agent,
234 current_model_index: base.current_model_index,
235 retry_cycle: base.retry_cycle,
236 max_cycles: base.max_cycles,
237 retry_delay_ms: base.retry_delay_ms,
238 backoff_multiplier: base.backoff_multiplier,
239 max_backoff_ms: base.max_backoff_ms,
240 backoff_pending_ms: base.backoff_pending_ms,
241 current_role: base.current_role,
242 current_drain: base.current_drain,
243 current_mode: base.current_mode,
244 rate_limit_continuation_prompt: prompt.map(|p| RateLimitContinuationPrompt {
245 drain: base.current_drain,
246 role: base.current_role,
247 prompt: p,
248 }),
249 last_session_id: base.last_session_id,
250 }
251 }
252
253 #[must_use]
255 pub fn switch_to_next_agent_with_prompt_for_role(
256 &self,
257 role: AgentRole,
258 prompt: Option<String>,
259 ) -> Self {
260 let base = self.switch_to_next_agent();
261 Self {
262 agents: base.agents,
263 current_agent_index: base.current_agent_index,
264 models_per_agent: base.models_per_agent,
265 current_model_index: base.current_model_index,
266 retry_cycle: base.retry_cycle,
267 max_cycles: base.max_cycles,
268 retry_delay_ms: base.retry_delay_ms,
269 backoff_multiplier: base.backoff_multiplier,
270 max_backoff_ms: base.max_backoff_ms,
271 backoff_pending_ms: base.backoff_pending_ms,
272 current_role: base.current_role,
273 current_drain: base.current_drain,
274 current_mode: base.current_mode,
275 rate_limit_continuation_prompt: prompt.map(|p| RateLimitContinuationPrompt {
276 drain: base.current_drain,
277 role,
278 prompt: p,
279 }),
280 last_session_id: base.last_session_id,
281 }
282 }
283
284 #[must_use]
289 pub fn clear_continuation_prompt(&self) -> Self {
290 Self {
291 agents: Arc::clone(&self.agents),
292 current_agent_index: self.current_agent_index,
293 models_per_agent: Arc::clone(&self.models_per_agent),
294 current_model_index: self.current_model_index,
295 retry_cycle: self.retry_cycle,
296 max_cycles: self.max_cycles,
297 retry_delay_ms: self.retry_delay_ms,
298 backoff_multiplier: self.backoff_multiplier,
299 max_backoff_ms: self.max_backoff_ms,
300 backoff_pending_ms: self.backoff_pending_ms,
301 current_role: self.current_role,
302 current_drain: self.current_drain,
303 current_mode: self.current_mode,
304 rate_limit_continuation_prompt: None,
305 last_session_id: self.last_session_id.clone(),
306 }
307 }
308
309 #[must_use]
310 pub fn reset_for_drain(&self, drain: AgentDrain) -> Self {
311 Self {
312 agents: Arc::clone(&self.agents),
313 current_agent_index: 0,
314 models_per_agent: Arc::clone(&self.models_per_agent),
315 current_model_index: 0,
316 retry_cycle: 0,
317 max_cycles: self.max_cycles,
318 retry_delay_ms: self.retry_delay_ms,
319 backoff_multiplier: self.backoff_multiplier,
320 max_backoff_ms: self.max_backoff_ms,
321 backoff_pending_ms: None,
322 current_role: drain.role(),
323 current_drain: drain,
324 current_mode: DrainMode::Normal,
325 rate_limit_continuation_prompt: None,
326 last_session_id: None,
327 }
328 }
329
330 #[must_use]
331 pub fn reset_for_role(&self, role: AgentRole) -> Self {
332 self.reset_for_drain(match role {
333 AgentRole::Developer => AgentDrain::Development,
334 AgentRole::Reviewer => AgentDrain::Review,
335 AgentRole::Commit => AgentDrain::Commit,
336 AgentRole::Analysis => AgentDrain::Analysis,
337 })
338 }
339
340 #[must_use]
341 pub fn reset(&self) -> Self {
342 Self {
343 agents: Arc::clone(&self.agents),
344 current_agent_index: 0,
345 models_per_agent: Arc::clone(&self.models_per_agent),
346 current_model_index: 0,
347 retry_cycle: self.retry_cycle,
348 max_cycles: self.max_cycles,
349 retry_delay_ms: self.retry_delay_ms,
350 backoff_multiplier: self.backoff_multiplier,
351 max_backoff_ms: self.max_backoff_ms,
352 backoff_pending_ms: None,
353 current_role: self.current_role,
354 current_drain: self.current_drain,
355 current_mode: DrainMode::Normal,
356 rate_limit_continuation_prompt: None,
357 last_session_id: None,
358 }
359 }
360
361 #[must_use]
363 pub fn with_session_id(&self, session_id: Option<String>) -> Self {
364 Self {
365 agents: Arc::clone(&self.agents),
366 current_agent_index: self.current_agent_index,
367 models_per_agent: Arc::clone(&self.models_per_agent),
368 current_model_index: self.current_model_index,
369 retry_cycle: self.retry_cycle,
370 max_cycles: self.max_cycles,
371 retry_delay_ms: self.retry_delay_ms,
372 backoff_multiplier: self.backoff_multiplier,
373 max_backoff_ms: self.max_backoff_ms,
374 backoff_pending_ms: self.backoff_pending_ms,
375 current_role: self.current_role,
376 current_drain: self.current_drain,
377 current_mode: self.current_mode,
378 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
379 last_session_id: session_id,
380 }
381 }
382
383 #[must_use]
385 pub fn clear_session_id(&self) -> Self {
386 Self {
387 agents: Arc::clone(&self.agents),
388 current_agent_index: self.current_agent_index,
389 models_per_agent: Arc::clone(&self.models_per_agent),
390 current_model_index: self.current_model_index,
391 retry_cycle: self.retry_cycle,
392 max_cycles: self.max_cycles,
393 retry_delay_ms: self.retry_delay_ms,
394 backoff_multiplier: self.backoff_multiplier,
395 max_backoff_ms: self.max_backoff_ms,
396 backoff_pending_ms: self.backoff_pending_ms,
397 current_role: self.current_role,
398 current_drain: self.current_drain,
399 current_mode: self.current_mode,
400 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
401 last_session_id: None,
402 }
403 }
404
405 #[must_use]
406 pub fn start_retry_cycle(&self) -> Self {
407 let new_retry_cycle = self.retry_cycle + 1;
408 let new_backoff_pending_ms = if new_retry_cycle >= self.max_cycles {
409 None
410 } else {
411 let temp = Self {
413 agents: Arc::clone(&self.agents),
414 current_agent_index: 0,
415 models_per_agent: Arc::clone(&self.models_per_agent),
416 current_model_index: 0,
417 retry_cycle: new_retry_cycle,
418 max_cycles: self.max_cycles,
419 retry_delay_ms: self.retry_delay_ms,
420 backoff_multiplier: self.backoff_multiplier,
421 max_backoff_ms: self.max_backoff_ms,
422 backoff_pending_ms: None,
423 current_role: self.current_role,
424 current_drain: self.current_drain,
425 current_mode: self.current_mode,
426 rate_limit_continuation_prompt: None,
427 last_session_id: None,
428 };
429 Some(temp.calculate_backoff_delay_ms_for_retry_cycle())
430 };
431
432 Self {
433 agents: Arc::clone(&self.agents),
434 current_agent_index: 0,
435 models_per_agent: Arc::clone(&self.models_per_agent),
436 current_model_index: 0,
437 retry_cycle: new_retry_cycle,
438 max_cycles: self.max_cycles,
439 retry_delay_ms: self.retry_delay_ms,
440 backoff_multiplier: self.backoff_multiplier,
441 max_backoff_ms: self.max_backoff_ms,
442 backoff_pending_ms: new_backoff_pending_ms,
443 current_role: self.current_role,
444 current_drain: self.current_drain,
445 current_mode: self.current_mode,
446 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
447 last_session_id: self.last_session_id.clone(),
448 }
449 }
450
451 #[must_use]
452 pub fn clear_backoff_pending(&self) -> Self {
453 Self {
454 agents: Arc::clone(&self.agents),
455 current_agent_index: self.current_agent_index,
456 models_per_agent: Arc::clone(&self.models_per_agent),
457 current_model_index: self.current_model_index,
458 retry_cycle: self.retry_cycle,
459 max_cycles: self.max_cycles,
460 retry_delay_ms: self.retry_delay_ms,
461 backoff_multiplier: self.backoff_multiplier,
462 max_backoff_ms: self.max_backoff_ms,
463 backoff_pending_ms: None,
464 current_role: self.current_role,
465 current_drain: self.current_drain,
466 current_mode: self.current_mode,
467 rate_limit_continuation_prompt: self.rate_limit_continuation_prompt.clone(),
468 last_session_id: self.last_session_id.clone(),
469 }
470 }
471
472 pub(super) fn calculate_backoff_delay_ms_for_retry_cycle(&self) -> u64 {
473 let cycle_index = self.retry_cycle.saturating_sub(1);
475 calculate_backoff_delay_ms(
476 self.retry_delay_ms,
477 self.backoff_multiplier,
478 self.max_backoff_ms,
479 cycle_index,
480 )
481 }
482}
483
484#[cfg(test)]
485mod backoff_semantics_tests {
486 use super::*;
487
488 #[test]
489 fn test_switch_to_agent_named_preserves_backoff_when_retry_cycle_hits_max_but_state_is_not_exhausted(
490 ) {
491 let mut state = AgentChainState::initial().with_agents(
492 vec!["a".to_string(), "b".to_string(), "c".to_string()],
493 vec![vec![], vec![], vec![]],
494 AgentRole::Developer,
495 );
496 state.max_cycles = 2;
497 state.retry_cycle = 1;
498 state.current_agent_index = 2;
499
500 let next = state.switch_to_agent_named("b");
501
502 assert_eq!(next.current_agent_index, 1);
503 assert_eq!(next.retry_cycle, 2);
504 assert!(
505 next.backoff_pending_ms.is_some(),
506 "backoff should remain pending unless the state is fully exhausted"
507 );
508 }
509}