malvin 0.2.4

Non-interactive research and coding agent
use super::router_flow_acp::router_flow_acp_support::empty_iteration_backups;
use super::router_flow_acp::{
    RouterAcpIterationInput, RouterAcpIterationOutcome, finalize_router_acp_iteration,
    run_router_acp_open_iteration,
};
use crate::agent_backend::AgentBackend;
use crate::artifacts::{RunArtifacts, SessionDotfileBackups, merge_and_sanitize_for_gate_restore};
use crate::cli::SharedOpts;
use crate::cli::format_workspace_gate_failure;
use crate::cli::workflow_router_shared::effective_max_loops;
use crate::prompts::PromptStore;
use crate::run_timing::acp_post_run::RunTimingSessionEnd;
use std::path::Path;

#[path = "router_flow_loop_decide.rs"]
mod router_flow_loop_decide;
pub(crate) use router_flow_loop_decide::{
    RouterLoopDecision, RouterLoopExitInput, decide_router_loop_exit, router_exit_summarize_for,
};

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

pub(crate) struct RouterAgentLoopOutcome {
    pub last_acp: Result<(), String>,
    pub last_backups: SessionDotfileBackups,
}

struct RouterLoopStepResult {
    last_acp: Result<(), String>,
    last_backups: SessionDotfileBackups,
    decision: Option<RouterLoopDecision>,
}

pub(crate) fn restore_router_iteration_dotfiles(
    work_dir: &Path,
    iteration_anchor: &SessionDotfileBackups,
) -> Result<SessionDotfileBackups, String> {
    let progress =
        SessionDotfileBackups::snapshot(work_dir).unwrap_or_else(|_| iteration_anchor.clone());
    let merged = merge_and_sanitize_for_gate_restore(iteration_anchor, &progress, work_dir);
    merged.restore(work_dir)?;
    Ok(merged)
}

pub(crate) async fn run_router_agent_loops(
    mut input: RouterAgentLoopInput<'_>,
) -> Result<RouterAgentLoopOutcome, String> {
    let max_loops = effective_max_loops(input.max_loops);
    let mut last_acp = Ok(());
    let mut last_backups = empty_iteration_backups();
    for agent_loop in 1..=max_loops {
        let step = run_one_router_loop_step(&mut input, agent_loop, max_loops).await?;
        last_acp = step.last_acp;
        last_backups = step.last_backups;
        if last_acp.is_err() {
            break;
        }
        match step.decision {
            None | Some(RouterLoopDecision::Exit) => break,
            Some(RouterLoopDecision::Continue) => {}
            Some(RouterLoopDecision::ExitGatesFailed(detail)) => {
                return Err(format_workspace_gate_failure("malvin", &detail));
            }
        }
    }
    Ok(RouterAgentLoopOutcome {
        last_acp,
        last_backups,
    })
}

async fn run_one_router_loop_step(
    input: &mut RouterAgentLoopInput<'_>,
    agent_loop: usize,
    max_loops: usize,
) -> Result<RouterLoopStepResult, String> {
    let session_end = if agent_loop == max_loops {
        RunTimingSessionEnd::Finalize
    } else {
        RunTimingSessionEnd::AccumulateRun
    };
    let open = run_router_acp_open_iteration(RouterAcpIterationInput {
        client: input.client,
        artifacts: input.artifacts,
        prompt_store: input.prompt_store,
        shared: input.shared,
        agent_loop,
        session_end,
        max_hypotheses: input.max_hypotheses,
    })
    .await;
    finish_router_loop_step(input, (agent_loop, max_loops, session_end), open).await
}

async fn finish_router_loop_step(
    input: &mut RouterAgentLoopInput<'_>,
    ids: (usize, usize, RunTimingSessionEnd),
    open: RouterAcpIterationOutcome,
) -> Result<RouterLoopStepResult, String> {
    let (agent_loop, max_loops, session_end) = ids;
    let work_dir = input.artifacts.work_dir.as_path();
    let last_backups = restore_router_iteration_dotfiles(work_dir, &open.iteration_backups)?;
    if !open.session_alive {
        return Ok(RouterLoopStepResult {
            last_acp: open.acp_result,
            last_backups,
            decision: None,
        });
    }
    let decision = decide_router_loop_exit(RouterLoopExitInput {
        artifacts: input.artifacts,
        backups: &last_backups,
        done: open.done,
        gates: input.shared.gates,
        agent_loop,
        max_loops,
    });
    let timing = open.timing.expect("alive session carries timing");
    let last_acp = finalize_router_acp_iteration(
        &mut RouterAcpIterationInput {
            client: input.client,
            artifacts: input.artifacts,
            prompt_store: input.prompt_store,
            shared: input.shared,
            agent_loop,
            session_end,
            max_hypotheses: input.max_hypotheses,
        },
        timing,
        router_exit_summarize_for(&decision),
    )
    .await;
    Ok(RouterLoopStepResult {
        last_acp,
        last_backups,
        decision: Some(decision),
    })
}

#[cfg(test)]
#[path = "router_flow_loop_tests.rs"]
mod router_flow_loop_tests;