dirge-agent 0.11.1

Minimalistic coding agent written in Rust, optimized for memory footprint and performance
//! `AgentEvent::Error` handler extracted from `run_interactive`.
//!
//! Flushes any coalesced trailing tokens, writes the error line, persists
//! the partial turn (so it's searchable), fires the plugin `on-error`
//! hook, tears the runner down, and drops queued interjections — replaying
//! them after an error (e.g. context-length) would just re-trigger it.
//! Behavior is identical to the inline code; pure refactor.

use std::time::Instant;

use compact_str::CompactString;
use tokio::sync::mpsc;

use crate::event::AgentEvent;
use crate::ui::agent_io::{persist_turn_to_db, render_agent_stream};
use crate::ui::avatar;
use crate::ui::colors::{c_agent, c_error};
use crate::ui::events::sanitize_output;
use crate::ui::run_handlers::RunCtx;
use crate::ui::tool_display::close_tool_chamber_if_open;

#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
#[cfg(feature = "plugin")]
use std::sync::{Arc, Mutex};

#[allow(clippy::too_many_arguments)]
pub(crate) async fn handle_error(
    ctx: &mut RunCtx<'_>,
    error: CompactString,
    was_reasoning: &mut bool,
    is_running: &mut bool,
    last_token_render: &mut Option<Instant>,
    agent_rx: &mut Option<mpsc::Receiver<AgentEvent>>,
    agent_abort: &mut Option<tokio::task::JoinHandle<()>>,
    agent_interject: &mut Option<mpsc::Sender<()>>,
    agent_cancel: &mut Option<mpsc::Sender<()>>,
    interjection_queue: &std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
    #[cfg(feature = "plugin")] plugin_manager: Option<&Arc<Mutex<PluginManager>>>,
) -> anyhow::Result<()> {
    *was_reasoning = false;
    ctx.renderer.set_avatar_state(avatar::AvatarState::Error);
    #[cfg(feature = "experimental-ui-terminal-tab")]
    ctx.renderer.set_last_tool_name("");
    close_tool_chamber_if_open(ctx.renderer, ctx.last_tool_name, ctx.tool_chamber_open)?;
    // dirge-ufe0: flush any trailing token the render coalescer skipped
    // (the Error event queued behind the final tokens leaves them
    // caught-up-but-unpainted) before the error line is written, so the
    // streamed text stays on-screen above the error (also DB-persisted).
    if !ctx.response_buf.is_empty() {
        render_agent_stream(
            ctx.response_buf,
            ctx.response_start_line,
            c_agent(),
            ctx.renderer,
        )?;
        *last_token_render = None;
    }
    let safe = sanitize_output(&error);
    ctx.renderer
        .write_line(&format!("error: {}", safe), c_error())?;

    // Persist the partial turn (whatever streamed before the error) so it's
    // searchable and the session records what went wrong.
    persist_turn_to_db(
        ctx.session,
        ctx.last_user_prompt,
        ctx.response_buf,
        ctx.tool_calls_buf,
    );

    #[cfg(feature = "plugin")]
    if let Some(pm) = plugin_manager {
        let mut mgr = pm.lock().unwrap_or_else(|err| err.into_inner());
        if let Err(dispatch_err) = mgr.dispatch(
            "on-error",
            &format!(
                "@{{:error \"{}\"}}",
                crate::plugin::escape_janet_string(&error)
            ),
        ) {
            ctx.renderer.write_line(
                &format!("[plugin] on-error error: {dispatch_err}"),
                c_error(),
            )?;
        }
    }

    *is_running = false;
    if let Some(tx) = agent_cancel.take() {
        let _ = tx.try_send(());
    }
    if let Some(h) = agent_abort.take() {
        h.abort();
    }
    *agent_rx = None;
    *agent_interject = None;
    *ctx.agent_line_started = false;
    ctx.response_buf.clear();
    *ctx.response_start_line = None;
    ctx.reasoning_buf.clear();
    *ctx.reasoning_start_line = None;

    // Drop queued interjections — they were typed expecting the running
    // turn to succeed; replaying them blindly after an error would just
    // re-trigger it.
    let dropped = interjection_queue.lock().unwrap().len();
    interjection_queue.lock().unwrap().clear();
    if dropped > 0 {
        ctx.renderer.write_line(
            &format!(
                "{} queued message{} dropped due to error",
                dropped,
                if dropped == 1 { "" } else { "s" }
            ),
            c_error(),
        )?;
    }
    Ok(())
}