malvin 0.2.4

Non-interactive research and coding agent
use crate::agent_backend::{AgentBackend, build_agent_backend, build_agent_backend_with_tee};
use crate::artifacts::{RunArtifacts, SessionDotfileBackups};
use crate::cli::one_shot_session::{
    finish_one_shot_after_prompt, resolve_one_shot_request_artifacts,
};
use crate::cli::run_emit::{
    RunStartupEmitOpts, emit_command_line, emit_run_logs_line, emit_run_startup_banner,
};
use crate::cli::{AgentStdoutTeeFlags, SharedOpts, WorkflowCliOptions};
use crate::output::agent_stdout_tee_enabled;

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

use do_flow_acp::run_do_acp;
pub use do_flow_prompt::{
    combine_do_acp_prompt_header_and_user, combine_do_prompt_file_and_user,
    combine_do_raw_header_and_user, prepare_do_prompt_store,
};

#[derive(Debug)]
pub struct DoArgs {
    pub request: Option<String>,
}

struct DoRunPrep {
    client: AgentBackend,
    artifacts: RunArtifacts,
    coder: do_flow_prompt::DoCoderRun,
    session_dotfile_backups: SessionDotfileBackups,
}

fn new_do_client(
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<AgentBackend, String> {
    if shared.verbose {
        return build_agent_backend(shared, workflow, shared.acp_stdout_markdown_enabled(), "do");
    }
    let interactive = agent_stdout_tee_enabled();
    let emit_markdown = interactive && shared.acp_stdout_markdown_enabled();
    let tee = if interactive {
        AgentStdoutTeeFlags {
            emit_stdout_markdown: emit_markdown,
            raw_output: false,
            show_thoughts_on_stdout: false,
        }
    } else {
        AgentStdoutTeeFlags {
            emit_stdout_markdown: false,
            raw_output: true,
            show_thoughts_on_stdout: false,
        }
    };
    build_agent_backend_with_tee(shared, workflow, tee)
}

async fn prepare_do_run(
    do_args: &DoArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<DoRunPrep, String> {
    let mut client = new_do_client(shared, workflow)?;
    let (text, artifacts) = resolve_one_shot_request_artifacts(
        do_args.request.as_ref(),
        "--do",
        Some(crate::run_id::RunDirOptions { gc: false }),
    )?;
    if shared.verbose {
        emit_run_startup_banner(
            &artifacts,
            RunStartupEmitOpts::from_shared(shared, true),
            &text,
        )?;
    } else {
        emit_command_line(&artifacts.run_dir, false)?;
    }
    crate::run_id::maybe_gc_after_run_created(&artifacts.work_dir, &artifacts.run_dir);
    client.ensure_authenticated().map_err(|e| e.to_string())?;
    client.prompts_log_run_dir = Some(artifacts.run_dir.clone());

    let (coder, session_dotfile_backups) =
        begin_do_session_overlapping_prompt_prep(&mut client, &artifacts, &text, shared).await?;

    Ok(DoRunPrep {
        client,
        artifacts,
        coder,
        session_dotfile_backups,
    })
}

async fn begin_do_session_overlapping_prompt_prep(
    client: &mut AgentBackend,
    artifacts: &RunArtifacts,
    text: &str,
    shared: &SharedOpts,
) -> Result<(do_flow_prompt::DoCoderRun, SessionDotfileBackups), String> {
    let begin = client.begin_coder_session(&artifacts.work_dir);
    let model = shared.model.canonical();
    let git = shared.git;
    let coder_backup = async {
        let coder = do_flow_prompt::build_do_coder_run(
            artifacts,
            text,
            crate::workflow_context::PromptModelOpts::new(&model, git),
        )?;
        let session_dotfile_backups =
            SessionDotfileBackups::snapshot_after_ensuring_home_config(&artifacts.work_dir)?;
        Ok::<_, String>((coder, session_dotfile_backups))
    };
    let (begin_res, coder_backup_res) = tokio::join!(begin, coder_backup);
    begin_res.map_err(|e| e.to_string())?;
    coder_backup_res
}

pub async fn run_do(
    do_args: DoArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<(), String> {
    let interactive = agent_stdout_tee_enabled();
    let emit_markdown = interactive && shared.acp_stdout_markdown_enabled();
    let dm_only = !shared.verbose;
    crate::output::set_do_dm_stdout_opts(crate::output::DoDmStdoutOpts {
        enabled: dm_only,
        emit_markdown: dm_only && emit_markdown,
    });
    crate::output::set_heartbeat_stdout_suppressed(dm_only);
    let result = run_do_body(do_args, shared, workflow).await;
    crate::output::set_do_dm_stdout_opts(crate::output::DoDmStdoutOpts::default());
    crate::output::set_heartbeat_stdout_suppressed(false);
    result
}

async fn run_do_body(
    do_args: DoArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<(), String> {
    let mut prep = prepare_do_run(&do_args, shared, workflow).await?;
    if shared.verbose {
        emit_run_logs_line(&prep.artifacts)?;
    }
    let acp_res = run_do_acp(&mut prep.client, &prep.artifacts, prep.coder).await;
    finish_one_shot_after_prompt(
        acp_res,
        &prep.artifacts.work_dir,
        &prep.session_dotfile_backups,
        &prep.artifacts.artifact_result_md(),
    )?;
    Ok(())
}

#[cfg(test)]
mod do_snapshot_tests {
    use super::SessionDotfileBackups;
    use crate::malvin_config_path;
    use crate::test_utils::with_isolated_home;

    #[test]
    fn snapshot_do_session_dotfiles_on_empty_workdir() {
        let tmp = tempfile::tempdir().expect("tempdir");
        SessionDotfileBackups::snapshot(tmp.path()).expect("snapshot");
    }

    #[test]
    fn do_prepare_snapshot_ensures_home_config_exists() {
        with_isolated_home(|work| {
            let cfg = malvin_config_path(work);
            assert!(!cfg.exists());
            SessionDotfileBackups::snapshot_after_ensuring_home_config(work).expect("snapshot");
            assert!(
                cfg.is_file(),
                "do session snapshot must ensure ~/.malvin_home/config.toml exists"
            );
        });
    }
}

#[cfg(test)]
mod kiss_static_fn_item_refs {
    use super::do_flow_acp::run_do_coder_prompt;
    use super::{run_do, run_do_acp};

    #[test]
    fn kiss_static_fn_item_refs() {
        let _ = run_do;
        let _ = run_do_acp;
        let _ = run_do_coder_prompt;
    }
}

#[cfg(test)]
#[allow(unused_imports)]
mod kiss_cov_gate_refs {
    use super::*;
    #[test]
    fn kiss_cov_unit_names() {
        let _: Option<DoRunPrep> = None;
        let _ = new_do_client;
        let _ = prepare_do_run;
        let _ = begin_do_session_overlapping_prompt_prep;
    }
}

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