use crate::app::core::StatefulHandler;
use crate::app::trace::{dump_event_loop_trace, EventTraceBuffer};
use crate::logging::EventLoopLogger;
use crate::phases::PhaseContext;
use crate::reducer::effect::{Effect, EffectResult};
use crate::reducer::event::PipelineEvent;
use crate::reducer::PipelineState;
use crate::reducer::{reduce, EffectHandler};
use std::path::Path;
use std::time::Instant;
pub(super) fn extract_error_event(
err: &anyhow::Error,
) -> Option<crate::reducer::event::ErrorEvent> {
err.chain()
.find_map(|cause| cause.downcast_ref::<crate::reducer::event::ErrorEvent>())
.cloned()
}
pub(super) enum GuardedEffectResult {
Ok(Box<EffectResult>),
Unrecoverable(anyhow::Error),
Panic,
}
pub(super) fn execute_effect_guarded<'ctx, H>(
handler: &mut H,
effect: Effect,
ctx: &mut PhaseContext<'_>,
state: &PipelineState,
) -> GuardedEffectResult
where
H: EffectHandler<'ctx>,
{
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handler.execute(effect, ctx)
})) {
Ok(Ok(result)) => GuardedEffectResult::Ok(Box::new(result)),
Ok(Err(err)) => extract_error_event(&err).map_or_else(
|| GuardedEffectResult::Unrecoverable(err),
|error_event| {
GuardedEffectResult::Ok(Box::new(crate::reducer::effect::EffectResult::event(
crate::reducer::event::PipelineEvent::PromptInput(
crate::reducer::event::PromptInputEvent::HandlerError {
phase: state.phase,
error: error_event,
},
),
)))
},
),
Err(_) => GuardedEffectResult::Panic,
}
}
pub(super) fn write_completion_marker_on_error(
ctx: &PhaseContext<'_>,
err: &anyhow::Error,
) -> bool {
if let Err(err) = ctx.workspace.create_dir_all(Path::new(".agent/tmp")) {
ctx.logger.error(&format!(
"Failed to create completion marker directory: {err}"
));
return false;
}
let marker_path = Path::new(".agent/tmp/completion_marker");
let content = format!("failure\nUnrecoverable handler error: {err}");
match ctx.workspace.write(marker_path, &content) {
Ok(()) => true,
Err(err) => {
ctx.logger.error(&format!(
"Failed to write completion marker for unrecoverable handler error: {err}"
));
false
}
}
}
pub(super) struct ErrorRecoveryContext<'a, 'b, H>
where
H: StatefulHandler,
{
pub(super) ctx: &'a mut PhaseContext<'b>,
pub(super) trace: &'a EventTraceBuffer,
pub(super) state: &'a PipelineState,
pub(super) effect_str: &'a str,
pub(super) start_time: Instant,
pub(super) handler: &'a mut H,
pub(super) event_loop_logger: &'a mut EventLoopLogger,
}
pub(super) fn handle_unrecoverable_error<'ctx, H>(
recovery_ctx: &mut ErrorRecoveryContext<'_, '_, H>,
err: &anyhow::Error,
) -> PipelineState
where
H: EffectHandler<'ctx> + StatefulHandler,
{
let ErrorRecoveryContext {
ctx,
trace,
state,
effect_str,
start_time,
handler,
event_loop_logger,
} = recovery_ctx;
let dumped = dump_event_loop_trace(ctx, trace, state, "unrecoverable_handler_error");
let marker_written = write_completion_marker_on_error(ctx, err);
if dumped {
let trace_path = ctx.run_log_context.event_loop_trace();
ctx.logger.error(&format!(
"Event loop encountered unrecoverable handler error (trace: {})",
trace_path.display()
));
} else {
ctx.logger
.error("Event loop encountered unrecoverable handler error");
}
if marker_written {
ctx.logger
.info("Completion marker written for unrecoverable handler error");
}
let failure_event =
PipelineEvent::PromptInput(crate::reducer::event::PromptInputEvent::HandlerError {
phase: state.phase,
error: crate::reducer::event::ErrorEvent::WorkspaceWriteFailed {
path: "(unrecoverable_handler_error)".to_string(),
kind: crate::reducer::event::WorkspaceIoErrorKind::Other,
},
});
let event_str = format!("{failure_event:?}");
let duration_ms = u64::try_from(start_time.elapsed().as_millis()).unwrap_or(u64::MAX);
let new_state = reduce(state.clone(), failure_event);
super::logging::log_effect_execution(
ctx,
event_loop_logger,
&new_state,
effect_str,
&event_str,
&[],
duration_ms,
);
handler.update_state(new_state.clone());
new_state
}
pub(super) fn handle_panic<'ctx, H>(
recovery_ctx: &mut ErrorRecoveryContext<'_, '_, H>,
events_processed: usize,
) -> PipelineState
where
H: EffectHandler<'ctx> + StatefulHandler,
{
let ErrorRecoveryContext {
ctx,
trace,
state,
effect_str,
start_time,
handler,
event_loop_logger,
} = recovery_ctx;
let dumped = dump_event_loop_trace(ctx, trace, state, "panic");
if dumped {
let trace_path = ctx.run_log_context.event_loop_trace();
ctx.logger.error(&format!(
"Event loop recovered from panic (trace: {})",
trace_path.display()
));
} else {
ctx.logger.error("Event loop recovered from panic");
}
if let Err(err) = ctx.workspace.create_dir_all(Path::new(".agent/tmp")) {
ctx.logger.error(&format!(
"Failed to create completion marker directory: {err}"
));
}
let marker_path = Path::new(".agent/tmp/completion_marker");
let content = format!(
"failure\nHandler panic in effect execution (phase={:?}, events_processed={})",
state.phase, events_processed
);
if let Err(err) = ctx.workspace.write(marker_path, &content) {
ctx.logger.error(&format!(
"Failed to write completion marker for handler panic: {err}"
));
}
let failure_event =
PipelineEvent::PromptInput(crate::reducer::event::PromptInputEvent::HandlerError {
phase: state.phase,
error: crate::reducer::event::ErrorEvent::WorkspaceWriteFailed {
path: "(handler_panic)".to_string(),
kind: crate::reducer::event::WorkspaceIoErrorKind::Other,
},
});
let event_str = format!("{failure_event:?}");
let duration_ms = u64::try_from(start_time.elapsed().as_millis()).unwrap_or(u64::MAX);
let new_state = reduce(state.clone(), failure_event);
super::logging::log_effect_execution(
ctx,
event_loop_logger,
&new_state,
effect_str,
&event_str,
&[],
duration_ms,
);
handler.update_state(new_state.clone());
new_state
}