malvin 0.2.4

Non-interactive research and coding agent
use crate::agent_backend::{
    AgentBackend, agent_backend_attach_run_timing_for_session, agent_backend_ensure_coder_session,
    agent_backend_set_implement_display_name, agent_backend_set_run_timing,
};
use crate::artifacts::{RunArtifacts, SessionDotfileBackups};
use crate::cli::SharedOpts;
use crate::prompts::PromptStore;
use crate::router_flow::router_flow_prompt;
use crate::run_timing::acp_post_run::RunTimingSessionEnd;
use std::path::Path;
use std::sync::{Arc, Mutex};

#[path = "router_flow_acp_support.rs"]
pub(crate) mod router_flow_acp_support;

#[path = "router_flow_coder_prompts.rs"]
mod router_flow_coder_prompts;

pub(crate) use router_flow_acp_support::RouterExitSummarize;

use router_flow_acp_support::{
    router_iteration_log_path, run_router_turns, snapshot_iteration_backups,
};
use router_flow_coder_prompts::run_router_summarize_coder_prompt;

pub(crate) struct RouterAcpIterationOutcome {
    pub acp_result: Result<(), String>,
    pub iteration_backups: SessionDotfileBackups,
    pub done: bool,
    pub session_alive: bool,
    pub timing: Option<Arc<Mutex<crate::run_timing::RunTiming>>>,
}

pub(crate) struct RouterAcpIterationInput<'a> {
    pub client: &'a mut AgentBackend,
    pub artifacts: &'a RunArtifacts,
    pub prompt_store: &'a PromptStore,
    pub shared: &'a SharedOpts,
    pub agent_loop: usize,
    pub session_end: RunTimingSessionEnd,
    pub max_hypotheses: usize,
}

pub(crate) type SessionEndParts<'a> = (
    &'a mut AgentBackend,
    &'a Path,
    &'a Arc<Mutex<crate::run_timing::RunTiming>>,
    RunTimingSessionEnd,
);

pub(crate) async fn begin_coder_session_if_needed(
    client: &mut AgentBackend,
    work_dir: &Path,
) -> Result<(), String> {
    agent_backend_ensure_coder_session(client, work_dir)
        .await
        .map_err(|e| e.to_string())
}

pub(crate) async fn run_router_acp_open_iteration(
    mut input: RouterAcpIterationInput<'_>,
) -> RouterAcpIterationOutcome {
    let work_dir = input.artifacts.work_dir.as_path();
    let log_path = router_iteration_log_path(input.artifacts, input.agent_loop);
    let timing = agent_backend_attach_run_timing_for_session(input.client);
    if let Err(e) = begin_coder_session_if_needed(input.client, work_dir).await {
        agent_backend_set_run_timing(input.client, None);
        return RouterAcpIterationOutcome {
            acp_result: Err(e),
            iteration_backups: snapshot_iteration_backups(work_dir),
            done: false,
            session_alive: false,
            timing: None,
        };
    }
    agent_backend_set_implement_display_name(input.client, "router");
    let session_end = input.session_end;
    let run_dir = input.artifacts.run_dir.clone();
    match run_router_turns(&mut input, log_path.as_path()).await {
        Ok(turns) => RouterAcpIterationOutcome {
            acp_result: Ok(()),
            iteration_backups: turns.iteration_backups,
            done: turns.done,
            session_alive: true,
            timing: Some(timing),
        },
        Err(e) => {
            let parts: SessionEndParts<'_> = (input.client, &run_dir, &timing, session_end);
            RouterAcpIterationOutcome {
                acp_result: abort_router_acp_session(parts, e).await,
                iteration_backups: snapshot_iteration_backups(work_dir),
                done: false,
                session_alive: false,
                timing: None,
            }
        }
    }
}

pub(crate) async fn finalize_router_acp_iteration(
    input: &mut RouterAcpIterationInput<'_>,
    timing: Arc<Mutex<crate::run_timing::RunTiming>>,
    exit_summarize: RouterExitSummarize,
) -> Result<(), String> {
    let log_path = router_iteration_log_path(input.artifacts, input.agent_loop);
    if matches!(exit_summarize, RouterExitSummarize::Run) {
        let model = input.shared.model.canonical();
        let body = router_flow_prompt::build_router_summarize_prompt(
            router_flow_prompt::RouterSummarizePromptInput {
                store: input.prompt_store,
                artifacts: input.artifacts,
                model: &model,
                git: input.shared.git,
            },
        )?;
        run_router_summarize_coder_prompt(input.client, &body, log_path.as_path()).await?;
    }
    let keep_session = input.client.keeps_coder_session_for_process_life();
    let run_dir = input.artifacts.run_dir.clone();
    let parts: SessionEndParts<'_> = (input.client, run_dir.as_path(), &timing, input.session_end);
    match (exit_summarize, keep_session) {
        (RouterExitSummarize::Run, _) | (RouterExitSummarize::Skip, false) => {
            end_router_acp_session(parts, Ok(())).await
        }
        (RouterExitSummarize::Skip, true) => emit_router_acp_timing(parts, Ok(())),
    }
}

pub(crate) fn emit_router_acp_timing(
    parts: SessionEndParts<'_>,
    agent_result: Result<(), String>,
) -> Result<(), String> {
    let (client, run_dir, timing, session_end) = parts;
    crate::acp_post_run::emit_run_timing_after_backend(crate::acp_post_run::RunTimingAfterBackend {
        backend: client,
        run_dir,
        timing,
        agent_result,
        session_end,
    })
}

pub(crate) async fn end_router_acp_session(
    parts: SessionEndParts<'_>,
    run_res: Result<(), String>,
) -> Result<(), String> {
    let end_res = parts.0.end_coder_session().await.map_err(|e| e.to_string());
    let merged =
        crate::acp_post_run::prefer_primary_over_secondary(run_res, end_res, "end coder session");
    emit_router_acp_timing(parts, merged)
}

pub(crate) async fn abort_router_acp_session(
    parts: SessionEndParts<'_>,
    err: String,
) -> Result<(), String> {
    crate::output::print_log_error(&err);
    crate::cli::error_run_log::note_command_error_emitted(&err);
    crate::cli::error_run_log::append_command_error_to_run_log(&err);
    agent_backend_set_run_timing(parts.0, None);
    end_router_acp_session(parts, Err(err)).await
}