malvin 0.2.4

Non-interactive research and coding agent
use crate::agent_backend::{AgentBackend, build_agent_backend};
use crate::artifacts::{RunArtifacts, resolve_user_md_request};
use crate::cli::cli_request::require_cli_request;
use crate::cli::run_emit::{RunStartupEmitOpts, emit_run_logs_line, emit_run_startup_banner};
use crate::cli::{SharedOpts, WorkflowCliOptions};
use crate::prompts::PromptStore;
#[path = "router_flow_acp.rs"]
pub(crate) mod router_flow_acp;
#[path = "router_flow_loop.rs"]
pub(crate) mod router_flow_loop;
#[path = "router_flow_no_work.rs"]
pub(crate) mod router_flow_no_work;
pub(crate) mod router_flow_prompt;

pub use router_flow_prompt::{
    combine_router_acp_prompt_header_and_user, combine_router_prompt_file_and_user,
    combine_router_raw_header_and_user, prepare_router_prompt_store,
};

#[derive(Debug)]
pub struct RouterArgs {
    pub request: Option<String>,
    pub max_loops: usize,
    pub max_hypotheses: usize,
}

struct RouterRunPrep {
    client: AgentBackend,
    artifacts: RunArtifacts,
    prompt_store: PromptStore,
}

fn new_router_client(
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<AgentBackend, String> {
    build_agent_backend(
        shared,
        workflow,
        shared.acp_stdout_markdown_enabled(),
        "router",
    )
}

fn finish_router_run_artifacts(
    artifacts: &RunArtifacts,
    shared: &SharedOpts,
    request: &str,
) -> Result<(), String> {
    if shared.gates {
        crate::artifacts::init_quality_gates_log_pending(artifacts).map_err(|e| e.to_string())?;
    }
    crate::cli::error_run_log::set_command_error_run_dir(Some(artifacts.run_dir.clone()));
    emit_run_startup_banner(
        artifacts,
        RunStartupEmitOpts::from_shared(shared, true),
        request,
    )?;
    crate::run_id::maybe_gc_after_run_created(&artifacts.work_dir, &artifacts.run_dir);
    Ok(())
}

async fn prepare_router_run(
    router_args: &RouterArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<RouterRunPrep, String> {
    let client = new_router_client(shared, workflow)?;
    let request = require_cli_request(router_args.request.as_ref(), "")?;
    let (text, work_dir) = resolve_user_md_request(&request)?;
    let artifacts = crate::artifacts::create_run_artifacts_from_text_opts(
        &text,
        Some(work_dir.as_path()),
        crate::run_id::RunDirOptions { gc: false },
    )
    .map_err(|e| e.to_string())?;
    finish_router_run_artifacts(&artifacts, shared, &request)?;
    client.ensure_authenticated().map_err(|e| e.to_string())?;
    let prompt_store = prepare_router_prompt_store()?;
    Ok(RouterRunPrep {
        client,
        artifacts,
        prompt_store,
    })
}

pub async fn run_router(
    router_args: RouterArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<(), String> {
    let request = require_cli_request(router_args.request.as_ref(), "")?;
    if shared.quiet {
        let interactive = crate::output::agent_stdout_tee_enabled();
        let emit_markdown = interactive && shared.acp_stdout_markdown_enabled();
        crate::output::set_do_dm_stdout_opts(crate::output::DoDmStdoutOpts {
            enabled: true,
            emit_markdown,
        });
        crate::output::set_heartbeat_stdout_suppressed(true);
    }
    let result = run_router_body(router_args, shared, workflow, &request).await;
    if shared.quiet {
        crate::output::set_do_dm_stdout_opts(crate::output::DoDmStdoutOpts::default());
        crate::output::set_heartbeat_stdout_suppressed(false);
    }
    result
}

async fn run_router_body(
    router_args: RouterArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
    _request: &str,
) -> Result<(), String> {
    let mut prep = prepare_router_run(&router_args, shared, workflow).await?;
    prep.client.prompts_log_run_dir = Some(prep.artifacts.run_dir.clone());
    prep.client
        .begin_coder_session(&prep.artifacts.work_dir)
        .await
        .map_err(|e| e.to_string())?;
    emit_run_logs_line(&prep.artifacts)?;

    let loop_outcome =
        router_flow_loop::run_router_agent_loops(router_flow_loop::RouterAgentLoopInput {
            client: &mut prep.client,
            artifacts: &prep.artifacts,
            prompt_store: &prep.prompt_store,
            shared,
            max_loops: router_args.max_loops,
            max_hypotheses: router_args.max_hypotheses,
        })
        .await?;

    let r = crate::acp_post_run::merge_acp_restore_check_abort_then_print_timing(
        loop_outcome.last_acp,
        &prep.artifacts,
        &loop_outcome.last_backups,
    );
    if r.is_ok() {
        crate::cli::error_run_log::clear_command_error_run_dir();
    }
    r
}

#[cfg(test)]
mod kiss_static_fn_item_refs {
    use super::{run_router, run_router_body};

    #[test]
    fn kiss_static_fn_item_refs() {
        let _ = run_router;
        let _ = run_router_body;
    }
}

#[cfg(test)]
#[allow(unused_imports)]
mod kiss_cov_gate_refs {
    use super::*;
    #[test]
    fn kiss_cov_unit_names() {
        let _: Option<RouterRunPrep> = None;
        let _ = new_router_client;
        let _ = finish_router_run_artifacts;
        let _ = prepare_router_run;
        let _ = router_flow_no_work::chat_has_malvin_done;
    }
}

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