Skip to main content

ralph_workflow/app/runtime/
mod.rs

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
288/// Bundles iteration event data to reduce function argument count.
289struct 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}