1pub fn check_no_resume_prompt() -> bool {
2 crate::app::io::effect_io::check_no_resume_prompt()
3}
4
5pub fn is_terminal_io() -> bool {
6 crate::app::io::effect_io::is_terminal_io()
7}
8
9pub fn get_current_dir() -> std::path::PathBuf {
10 crate::app::io::effect_io::get_current_dir()
11}
12
13pub fn set_current_dir(path: &std::path::Path) -> std::io::Result<()> {
14 crate::app::io::effect_io::set_current_dir(path)
15}
16
17pub fn get_args() -> Vec<String> {
18 crate::app::io::effect_io::get_args()
19}
20
21pub fn get_program_args() -> Vec<String> {
22 crate::app::io::effect_io::get_program_args()
23}
24
25pub fn get_process_id() -> u32 {
26 crate::app::io::effect_io::get_process_id()
27}
28
29pub fn exit_with_code(code: i32) -> ! {
30 crate::app::io::effect_io::exit_with_code(code)
31}
32
33use crate::logging::EventLoopLogger;
34use crate::phases::PhaseContext;
35use crate::reducer::event::{ErrorEvent, PipelineEvent, PipelinePhase, PromptInputEvent};
36use crate::reducer::{determine_next_effect, reduce, EffectHandler, PipelineState};
37use anyhow::Result;
38use std::time::Instant;
39
40use crate::app::cloud_progress::report_cloud_progress;
41use crate::app::config::{create_initial_state_with_config, EventLoopConfig, EventLoopResult};
42use crate::app::core::StatefulHandler;
43use crate::app::error_handling::{
44 execute_effect_guarded, handle_panic, handle_unrecoverable_error, ErrorRecoveryContext,
45 GuardedEffectResult,
46};
47use crate::app::iteration::{should_exit_after_effect, should_exit_before_effect};
48use crate::app::logging::log_effect_execution;
49use crate::app::recovery::{
50 handle_forced_checkpoint_after_completion, handle_max_iterations_in_awaiting_dev_fix,
51 RecoveryResult,
52};
53use crate::app::trace::{
54 build_trace_entry, dump_event_loop_trace, EventTraceBuffer, DEFAULT_EVENT_LOOP_TRACE_CAPACITY,
55};
56
57struct LoopRuntime {
58 state: PipelineState,
59 events_processed: usize,
60 trace: EventTraceBuffer,
61 event_loop_logger: EventLoopLogger,
62}
63
64#[derive(PartialEq, Eq)]
65enum IterationResult {
66 Continue,
67 Break,
68}
69
70enum EffectExecutionOutcome {
71 Continue,
72 EffectResult(Box<crate::reducer::effect::EffectResult>),
73}
74
75struct MaxIterationRecovery {
76 recovery_failed: bool,
77}
78
79fn create_event_loop_logger(ctx: &PhaseContext<'_>) -> EventLoopLogger {
80 let event_loop_log_path = ctx.run_log_context.event_loop_log();
81 match EventLoopLogger::from_existing_log(ctx.workspace, &event_loop_log_path) {
82 Ok(logger) => logger,
83 Err(e) => {
84 ctx.logger.warn(&format!(
85 "Failed to read existing event loop log, starting fresh: {e}"
86 ));
87 EventLoopLogger::new()
88 }
89 }
90}
91
92fn handle_user_interrupt<'ctx, H>(
93 ctx: &PhaseContext<'_>,
94 handler: &mut H,
95 runtime: &mut LoopRuntime,
96) -> bool
97where
98 H: EffectHandler<'ctx> + StatefulHandler,
99{
100 if !crate::interrupt::take_user_interrupt_request() {
101 return false;
102 }
103
104 let effect_str = "Signal(SIGINT)".to_string();
105 let interrupt_event = PipelineEvent::PromptInput(PromptInputEvent::HandlerError {
106 phase: runtime.state.phase,
107 error: ErrorEvent::UserInterruptRequested,
108 });
109 let event_str = format!("{interrupt_event:?}");
110 let start_time = Instant::now();
111 let new_state = reduce(runtime.state.clone(), interrupt_event);
112 let duration_ms = u64::try_from(start_time.elapsed().as_millis()).unwrap_or(u64::MAX);
113
114 log_effect_execution(
115 ctx,
116 &mut runtime.event_loop_logger,
117 &new_state,
118 &effect_str,
119 &event_str,
120 &[],
121 duration_ms,
122 );
123
124 runtime.trace =
125 std::mem::replace(&mut runtime.trace, EventTraceBuffer::new(1)).append(build_trace_entry(
126 runtime.events_processed,
127 &new_state,
128 &effect_str,
129 &event_str,
130 ));
131 handler.update_state(new_state.clone());
132 runtime.state = new_state;
133 runtime.events_processed = runtime.events_processed.saturating_add(1);
134 true
135}
136
137fn execute_effect_with_recovery<'ctx, H>(
138 ctx: &mut PhaseContext<'_>,
139 handler: &mut H,
140 runtime: &mut LoopRuntime,
141 effect_str: &str,
142 start_time: Instant,
143 effect: crate::reducer::effect::Effect,
144) -> EffectExecutionOutcome
145where
146 H: EffectHandler<'ctx> + StatefulHandler,
147{
148 match execute_effect_guarded(handler, effect, ctx, &runtime.state) {
149 GuardedEffectResult::Ok(result) => EffectExecutionOutcome::EffectResult(result),
150 GuardedEffectResult::Unrecoverable(err) => {
151 let mut recovery_ctx = ErrorRecoveryContext {
152 ctx,
153 trace: &runtime.trace,
154 state: &runtime.state,
155 effect_str,
156 start_time,
157 handler,
158 event_loop_logger: &mut runtime.event_loop_logger,
159 };
160 runtime.state = handle_unrecoverable_error(&mut recovery_ctx, &err);
161 runtime.events_processed = runtime.events_processed.saturating_add(1);
162 EffectExecutionOutcome::Continue
163 }
164 GuardedEffectResult::Panic => {
165 let mut recovery_ctx = ErrorRecoveryContext {
166 ctx,
167 trace: &runtime.trace,
168 state: &runtime.state,
169 effect_str,
170 start_time,
171 handler,
172 event_loop_logger: &mut runtime.event_loop_logger,
173 };
174 runtime.state = handle_panic(&mut recovery_ctx, runtime.events_processed);
175 runtime.events_processed = runtime.events_processed.saturating_add(1);
176 EffectExecutionOutcome::Continue
177 }
178 }
179}
180
181fn process_primary_event<'ctx, H>(
182 ctx: &PhaseContext<'_>,
183 handler: &mut H,
184 runtime: &mut LoopRuntime,
185 effect_str: &str,
186 event: PipelineEvent,
187 additional_events: &[PipelineEvent],
188 duration_ms: u64,
189) where
190 H: EffectHandler<'ctx> + StatefulHandler,
191{
192 let event_str = format!("{event:?}");
193 let new_state = reduce(runtime.state.clone(), event);
194
195 log_effect_execution(
196 ctx,
197 &mut runtime.event_loop_logger,
198 &new_state,
199 effect_str,
200 &event_str,
201 additional_events,
202 duration_ms,
203 );
204
205 runtime.trace = std::mem::replace(&mut runtime.trace, EventTraceBuffer::new(1)).append(
206 build_trace_entry(runtime.events_processed, &new_state, effect_str, &event_str),
207 );
208 handler.update_state(new_state.clone());
209 runtime.state = new_state;
210 runtime.events_processed = runtime.events_processed.saturating_add(1);
211}
212
213fn process_additional_events<'ctx, H>(
214 handler: &mut H,
215 runtime: &mut LoopRuntime,
216 effect_str: &str,
217 additional_events: Vec<PipelineEvent>,
218) where
219 H: EffectHandler<'ctx> + StatefulHandler,
220{
221 let base_events_processed = runtime.events_processed;
222 let trace_data: Vec<_> = additional_events
223 .iter()
224 .enumerate()
225 .map(|(i, event)| {
226 let event_str = format!("{event:?}");
227 let state = reduce(runtime.state.clone(), event.clone());
228 (
229 build_trace_entry(base_events_processed + i, &state, effect_str, &event_str),
230 state,
231 )
232 })
233 .collect();
234
235 let final_state = trace_data
236 .last()
237 .map(|(_, s)| s.clone())
238 .unwrap_or_else(|| runtime.state.clone());
239
240 trace_data.into_iter().for_each(|(entry, state)| {
241 runtime.trace =
242 std::mem::replace(&mut runtime.trace, EventTraceBuffer::new(1)).append(entry);
243 handler.update_state(state);
244 });
245
246 runtime.state = final_state;
247 runtime.events_processed = base_events_processed.saturating_add(additional_events.len());
248}
249
250fn update_loop_detection_state<'ctx, H>(handler: &mut H, runtime: &mut LoopRuntime)
251where
252 H: EffectHandler<'ctx> + StatefulHandler,
253{
254 let current_fingerprint = crate::reducer::compute_effect_fingerprint(&runtime.state);
255 let continuation = runtime
256 .state
257 .continuation
258 .clone()
259 .update_loop_detection_counters(current_fingerprint);
260 runtime.state = PipelineState {
261 continuation,
262 ..runtime.state.clone()
263 };
264 handler.update_state(runtime.state.clone());
265}
266
267const AWAITING_DEV_FIX_INTERRUPTION_WARNING: &str =
268 "Interrupted phase reached from AwaitingDevFix without checkpoint saved. SaveCheckpoint effect should execute on next iteration.";
269
270fn completion_transition_warning(state: &PipelineState) -> Option<&'static str> {
271 if matches!(state.phase, PipelinePhase::Interrupted)
272 && matches!(state.previous_phase, Some(PipelinePhase::AwaitingDevFix))
273 && state.checkpoint_saved_count == 0
274 {
275 Some(AWAITING_DEV_FIX_INTERRUPTION_WARNING)
276 } else {
277 None
278 }
279}
280
281fn log_ui_events(ctx: &PhaseContext<'_>, ui_events: &[crate::reducer::ui_event::UIEvent]) {
282 ui_events.iter().for_each(|ui_event| {
283 ctx.logger
284 .info(&crate::rendering::render_ui_event(ui_event));
285 });
286}
287
288struct IterationEvents {
290 event: PipelineEvent,
291 additional_events: Vec<PipelineEvent>,
292 ui_events: Vec<crate::reducer::ui_event::UIEvent>,
293}
294
295fn finalize_iteration<'ctx, H>(
296 ctx: &mut PhaseContext<'_>,
297 handler: &mut H,
298 runtime: &mut LoopRuntime,
299 effect_str: &str,
300 iteration_events: IterationEvents,
301 duration_ms: u64,
302) -> Result<bool>
303where
304 H: EffectHandler<'ctx> + StatefulHandler,
305{
306 log_ui_events(ctx, &iteration_events.ui_events);
307
308 process_primary_event(
309 ctx,
310 handler,
311 runtime,
312 effect_str,
313 iteration_events.event,
314 &iteration_events.additional_events,
315 duration_ms,
316 );
317 process_additional_events(
318 handler,
319 runtime,
320 effect_str,
321 iteration_events.additional_events,
322 );
323 update_loop_detection_state(handler, runtime);
324 report_cloud_progress(ctx, &runtime.state, &iteration_events.ui_events)?;
325
326 Ok(log_completion_transition_if_needed(ctx, &runtime.state))
327}
328
329fn iteration_duration(start_time: Instant) -> u64 {
330 u64::try_from(start_time.elapsed().as_millis()).unwrap_or(u64::MAX)
331}
332
333fn prepare_next_effect(state: &PipelineState) -> (crate::reducer::effect::Effect, String, Instant) {
334 let effect = determine_next_effect(state);
335 let effect_str = format!("{effect:?}");
336 (effect, effect_str, Instant::now())
337}
338
339fn pre_iteration_check<'ctx, H>(
340 ctx: &mut PhaseContext<'_>,
341 handler: &mut H,
342 runtime: &mut LoopRuntime,
343) -> Option<IterationResult>
344where
345 H: EffectHandler<'ctx> + StatefulHandler,
346{
347 if should_exit_before_effect(&runtime.state) {
348 ctx.logger.info(&format!(
349 "Event loop: state already complete (phase: {:?}, checkpoint_saved_count: {})",
350 runtime.state.phase, runtime.state.checkpoint_saved_count
351 ));
352 return Some(IterationResult::Break);
353 }
354
355 if handle_user_interrupt(ctx, handler, runtime) {
356 return Some(IterationResult::Continue);
357 }
358
359 None
360}
361
362fn log_completion_transition_if_needed(ctx: &PhaseContext<'_>, state: &PipelineState) -> bool {
363 if !should_exit_after_effect(state) {
364 return false;
365 }
366
367 ctx.logger.info(&format!(
368 "Event loop: state became complete (phase: {:?}, checkpoint_saved_count: {})",
369 state.phase, state.checkpoint_saved_count
370 ));
371
372 if let Some(warning) = completion_transition_warning(state) {
373 ctx.logger.warn(warning);
374 }
375
376 true
377}
378
379enum RecoveryImpact {
380 Applied {
381 state: Box<PipelineState>,
382 events_processed: usize,
383 trace_dumped: bool,
384 failed: bool,
385 },
386 NotNeeded,
387}
388
389fn recovery_impact(result: RecoveryResult) -> RecoveryImpact {
390 match result {
391 RecoveryResult::Success(state, events_processed, dumped) => RecoveryImpact::Applied {
392 state: Box::new(state),
393 events_processed,
394 trace_dumped: dumped,
395 failed: false,
396 },
397 RecoveryResult::FailedUnrecoverable(state, events_processed, dumped) => {
398 RecoveryImpact::Applied {
399 state: Box::new(state),
400 events_processed,
401 trace_dumped: dumped,
402 failed: true,
403 }
404 }
405 RecoveryResult::NotNeeded => RecoveryImpact::NotNeeded,
406 }
407}
408
409fn execute_effect_and_capture_result<'ctx, H>(
410 ctx: &mut PhaseContext<'_>,
411 handler: &mut H,
412 runtime: &mut LoopRuntime,
413 effect_str: &str,
414 start_time: Instant,
415 effect: crate::reducer::effect::Effect,
416) -> Option<crate::reducer::effect::EffectResult>
417where
418 H: EffectHandler<'ctx> + StatefulHandler,
419{
420 match execute_effect_with_recovery(ctx, handler, runtime, effect_str, start_time, effect) {
421 EffectExecutionOutcome::Continue => None,
422 EffectExecutionOutcome::EffectResult(result) => Some(*result),
423 }
424}
425
426fn execute_effect_and_finalize<'ctx, H>(
427 ctx: &mut PhaseContext<'_>,
428 handler: &mut H,
429 runtime: &mut LoopRuntime,
430 effect_str: &str,
431 start_time: Instant,
432 effect: crate::reducer::effect::Effect,
433) -> Result<IterationResult>
434where
435 H: EffectHandler<'ctx> + StatefulHandler,
436{
437 let Some(result) =
438 execute_effect_and_capture_result(ctx, handler, runtime, effect_str, start_time, effect)
439 else {
440 return Ok(IterationResult::Continue);
441 };
442
443 finalize_effect_result(ctx, handler, runtime, effect_str, start_time, result)
444}
445
446fn execute_single_iteration<'ctx, H>(
447 ctx: &mut PhaseContext<'_>,
448 handler: &mut H,
449 runtime: &mut LoopRuntime,
450) -> Result<IterationResult>
451where
452 H: EffectHandler<'ctx> + StatefulHandler,
453{
454 if let Some(pre_check) = pre_iteration_check(ctx, handler, runtime) {
455 return Ok(pre_check);
456 }
457
458 let (effect, effect_str, start_time) = prepare_next_effect(&runtime.state);
459
460 execute_effect_and_finalize(ctx, handler, runtime, &effect_str, start_time, effect)
461}
462
463fn finalize_effect_result<'ctx, H>(
464 ctx: &mut PhaseContext<'_>,
465 handler: &mut H,
466 runtime: &mut LoopRuntime,
467 effect_str: &str,
468 start_time: Instant,
469 result: crate::reducer::effect::EffectResult,
470) -> Result<IterationResult>
471where
472 H: EffectHandler<'ctx> + StatefulHandler,
473{
474 let crate::reducer::effect::EffectResult {
475 event,
476 additional_events,
477 ui_events,
478 } = result;
479
480 let duration_ms = iteration_duration(start_time);
481 if finalize_iteration(
482 ctx,
483 handler,
484 runtime,
485 effect_str,
486 IterationEvents {
487 event,
488 additional_events,
489 ui_events,
490 },
491 duration_ms,
492 )? {
493 return Ok(IterationResult::Break);
494 }
495
496 Ok(IterationResult::Continue)
497}
498
499fn apply_recovery_result(
500 recovery: RecoveryResult,
501 runtime: &mut LoopRuntime,
502 trace_already_dumped: bool,
503) -> (bool, bool) {
504 match recovery_impact(recovery) {
505 RecoveryImpact::Applied {
506 state,
507 events_processed,
508 trace_dumped,
509 failed,
510 } => {
511 runtime.state = *state;
512 runtime.events_processed = events_processed;
513 (trace_already_dumped || trace_dumped, failed)
514 }
515 RecoveryImpact::NotNeeded => (trace_already_dumped, false),
516 }
517}
518
519fn handle_max_iteration_recovery<'ctx, H>(
520 ctx: &mut PhaseContext<'_>,
521 handler: &mut H,
522 config: EventLoopConfig,
523 runtime: &mut LoopRuntime,
524) -> MaxIterationRecovery
525where
526 H: EffectHandler<'ctx> + StatefulHandler,
527{
528 if !exceeded_max_iterations(runtime, &config) {
529 return MaxIterationRecovery {
530 recovery_failed: false,
531 };
532 }
533
534 let context = collect_max_iteration_recovery_context(ctx, handler, runtime);
535
536 ensure_trace_dumped_after_max_iterations(
537 ctx,
538 runtime,
539 context.trace_already_dumped,
540 config.max_iterations,
541 );
542
543 log_max_iterations_exit_if_needed(ctx, runtime, context.forced_completion);
544
545 MaxIterationRecovery {
546 recovery_failed: context.recovery_failed,
547 }
548}
549
550fn exceeded_max_iterations(runtime: &LoopRuntime, config: &EventLoopConfig) -> bool {
551 runtime.events_processed >= config.max_iterations
552}
553
554fn attempt_forced_checkpoint_recovery<'ctx, H>(
555 ctx: &mut PhaseContext<'_>,
556 handler: &mut H,
557 runtime: &mut LoopRuntime,
558) -> (bool, bool)
559where
560 H: EffectHandler<'ctx> + StatefulHandler,
561{
562 let checkpoint_result = handle_forced_checkpoint_after_completion(
563 ctx,
564 handler,
565 runtime.state.clone(),
566 runtime.events_processed,
567 &mut runtime.trace,
568 );
569 apply_recovery_result(checkpoint_result, runtime, false)
570}
571
572fn attempt_dev_fix_completion<'ctx, H>(
573 ctx: &mut PhaseContext<'_>,
574 handler: &mut H,
575 runtime: &mut LoopRuntime,
576 trace_already_dumped: bool,
577 recovery_failed: bool,
578) -> (bool, bool, bool)
579where
580 H: EffectHandler<'ctx> + StatefulHandler,
581{
582 if runtime.state.is_complete() || recovery_failed {
583 return (trace_already_dumped, recovery_failed, false);
584 }
585
586 let dev_fix_result = handle_max_iterations_in_awaiting_dev_fix(
587 ctx,
588 handler,
589 runtime.state.clone(),
590 runtime.events_processed,
591 &mut runtime.trace,
592 );
593 let (trace_already_dumped, recovery_failed) =
594 apply_recovery_result(dev_fix_result, runtime, trace_already_dumped);
595 (trace_already_dumped, recovery_failed, !recovery_failed)
596}
597
598struct MaxIterationRecoveryContext {
599 trace_already_dumped: bool,
600 recovery_failed: bool,
601 forced_completion: bool,
602}
603
604fn collect_max_iteration_recovery_context<'ctx, H>(
605 ctx: &mut PhaseContext<'_>,
606 handler: &mut H,
607 runtime: &mut LoopRuntime,
608) -> MaxIterationRecoveryContext
609where
610 H: EffectHandler<'ctx> + StatefulHandler,
611{
612 let (trace_already_dumped, recovery_failed) =
613 attempt_forced_checkpoint_recovery(ctx, handler, runtime);
614
615 let (trace_already_dumped, recovery_failed, forced_completion) =
616 attempt_dev_fix_completion(ctx, handler, runtime, trace_already_dumped, recovery_failed);
617
618 MaxIterationRecoveryContext {
619 trace_already_dumped,
620 recovery_failed,
621 forced_completion,
622 }
623}
624
625fn ensure_trace_dumped_after_max_iterations(
626 ctx: &mut PhaseContext<'_>,
627 runtime: &LoopRuntime,
628 trace_already_dumped: bool,
629 max_iterations: usize,
630) {
631 if trace_already_dumped {
632 return;
633 }
634
635 let dumped = dump_event_loop_trace(ctx, &runtime.trace, &runtime.state, "max_iterations");
636 if dumped {
637 let trace_path = ctx.run_log_context.event_loop_trace();
638 ctx.logger.warn(&format!(
639 "Event loop reached max iterations ({}) without completion (trace: {})",
640 max_iterations,
641 trace_path.display()
642 ));
643 } else {
644 ctx.logger.warn(&format!(
645 "Event loop reached max iterations ({}) without completion",
646 max_iterations
647 ));
648 }
649}
650
651fn log_max_iterations_exit(ctx: &PhaseContext<'_>, runtime: &LoopRuntime) {
652 ctx.logger.error(&format!(
653 "Event loop exiting: reason=max_iterations, phase={:?}, checkpoint_saved_count={}, events_processed={}",
654 runtime.state.phase,
655 runtime.state.checkpoint_saved_count,
656 runtime.events_processed
657 ));
658}
659
660fn log_max_iterations_exit_if_needed(
661 ctx: &PhaseContext<'_>,
662 runtime: &LoopRuntime,
663 forced_completion: bool,
664) {
665 if forced_completion || runtime.state.is_complete() {
666 return;
667 }
668
669 log_max_iterations_exit(ctx, runtime);
670}
671
672fn create_loop_runtime(
673 ctx: &PhaseContext<'_>,
674 initial_state: Option<PipelineState>,
675) -> LoopRuntime {
676 LoopRuntime {
677 state: initial_state.unwrap_or_else(|| create_initial_state_with_config(ctx)),
678 events_processed: 0,
679 trace: EventTraceBuffer::new(DEFAULT_EVENT_LOOP_TRACE_CAPACITY),
680 event_loop_logger: create_event_loop_logger(ctx),
681 }
682}
683
684fn run_event_loop_iterations<'ctx, H>(
685 ctx: &mut PhaseContext<'_>,
686 handler: &mut H,
687 config: &EventLoopConfig,
688 runtime: &mut LoopRuntime,
689) -> Result<()>
690where
691 H: EffectHandler<'ctx> + StatefulHandler,
692{
693 for _ in 0..config.max_iterations {
694 if execute_single_iteration(ctx, handler, runtime)? == IterationResult::Break {
695 break;
696 }
697 }
698
699 Ok(())
700}
701
702pub fn run_event_loop_driver<'ctx, H>(
703 ctx: &mut PhaseContext<'_>,
704 initial_state: Option<PipelineState>,
705 config: EventLoopConfig,
706 handler: &mut H,
707) -> Result<EventLoopResult>
708where
709 H: EffectHandler<'ctx> + StatefulHandler,
710{
711 let mut runtime = create_loop_runtime(ctx, initial_state);
712
713 handler.update_state(runtime.state.clone());
714 ctx.logger.info("Starting reducer-based event loop");
715
716 let _event_loop_guard = crate::interrupt::event_loop_active_guard();
717
718 run_event_loop_iterations(ctx, handler, &config, &mut runtime)?;
719
720 let recovery = handle_max_iteration_recovery(ctx, handler, config, &mut runtime);
721 let completed = runtime.state.is_complete() && !recovery.recovery_failed;
722
723 if !completed {
724 log_event_loop_non_completion(ctx, &runtime, recovery.recovery_failed);
725 }
726
727 Ok(EventLoopResult {
728 completed,
729 events_processed: runtime.events_processed,
730 final_phase: runtime.state.phase,
731 final_state: runtime.state.clone(),
732 })
733}
734
735fn log_event_loop_non_completion(
736 ctx: &PhaseContext<'_>,
737 runtime: &LoopRuntime,
738 recovery_failed: bool,
739) {
740 ctx.logger.warn(&format!(
741 "Event loop exiting without completion: phase={:?}, checkpoint_saved_count={}, \
742 previous_phase={:?}, events_processed={}, recovery_failed={}",
743 runtime.state.phase,
744 runtime.state.checkpoint_saved_count,
745 runtime.state.previous_phase,
746 runtime.events_processed,
747 recovery_failed
748 ));
749 ctx.logger.info(&format!(
750 "Final state: agent_chain.retry_cycle={}, agent_chain.active_role={:?}",
751 runtime.state.agent_chain.retry_cycle,
752 runtime.state.agent_chain.active_role()
753 ));
754}