malvin 0.2.4

Non-interactive research and coding agent
use crate::agent_backend::{AgentBackend, build_agent_backend};
use crate::artifacts::{RunArtifacts, SessionDotfileBackups};
use crate::cli::one_shot_session::{
    OneShotCoderGuard, finish_one_shot_after_prompt, finish_one_shot_auth_and_backups,
};
use crate::cli::run_emit::{RunStartupEmitOpts, emit_run_logs_line, emit_run_startup_banner};
use crate::cli::{SharedOpts, WorkflowCliOptions};
use crate::run_timing::TimingPhase;
use crate::workflow_context::format_prompt_path;

use super::WriteArgs;
use super::prep::{compose_write_a_prompt, compose_write_b_prompt, write_preflight};

struct WriteRunPrep {
    client: AgentBackend,
    artifacts: RunArtifacts,
    prompt_a: String,
    prompt_b: String,
    session_dotfile_backups: SessionDotfileBackups,
}

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

fn write_workspace_dir_display(artifacts: &RunArtifacts) -> String {
    format_prompt_path(&artifacts.run_dir, &artifacts.work_dir)
}

fn prepare_write_prompts(
    request_text: &str,
    out_paths: (&str, &str),
    artifacts: &RunArtifacts,
) -> Result<(String, String), String> {
    let workspace_dir = write_workspace_dir_display(artifacts);
    let (tex_display, pdf_display) = out_paths;
    Ok((
        compose_write_a_prompt(request_text, &workspace_dir)?,
        compose_write_b_prompt(tex_display, pdf_display, &workspace_dir)?,
    ))
}

fn create_write_artifacts(
    request_text: &str,
    request_work_dir: &std::path::Path,
) -> Result<RunArtifacts, String> {
    let artifacts = crate::artifacts::create_run_artifacts_from_text_opts(
        request_text,
        Some(request_work_dir),
        crate::run_id::RunDirOptions { gc: false },
    )
    .map_err(|e| e.to_string())?;
    crate::cli::error_run_log::set_command_error_run_dir(Some(artifacts.run_dir.clone()));
    Ok(artifacts)
}

async fn prepare_write_run(
    write_args: &mut WriteArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<WriteRunPrep, String> {
    let (request_text, request_work_dir, outputs) = write_preflight(
        write_args.request.as_ref(),
        &write_args.out_path,
        write_args.out_path_explicit,
    )?;
    let mut client = new_write_client(shared, workflow)?;
    let artifacts = create_write_artifacts(&request_text, &request_work_dir)?;
    let tex_display = format_prompt_path(&outputs.tex_path, &artifacts.work_dir);
    let pdf_display = format_prompt_path(&outputs.pdf_path, &artifacts.work_dir);
    write_args.out_path = tex_display.clone();
    let banner_request = write_args
        .request
        .clone()
        .unwrap_or_else(|| request_text.clone());
    emit_run_startup_banner(
        &artifacts,
        RunStartupEmitOpts::from_shared(shared, true),
        &banner_request,
    )?;
    crate::run_id::maybe_gc_after_run_created(&artifacts.work_dir, &artifacts.run_dir);
    let session_dotfile_backups = finish_one_shot_auth_and_backups(&mut client, &artifacts)?;
    let (prompt_a, prompt_b) =
        prepare_write_prompts(&request_text, (&tex_display, &pdf_display), &artifacts)?;
    Ok(WriteRunPrep {
        client,
        artifacts,
        prompt_a,
        prompt_b,
        session_dotfile_backups,
    })
}

async fn run_write_coder_prompt(
    client: &mut AgentBackend,
    artifacts: &RunArtifacts,
    prompt: &str,
    who: &str,
) -> Result<(), String> {
    client
        .run_coder_prompt(
            prompt,
            &artifacts.log_path(who),
            who,
            crate::acp::CoderPromptOptions {
                llm_phase: Some(TimingPhase::Implement),
                do_trace_split: None,
                stdout_bracket_label: None,
                ..Default::default()
            },
        )
        .await
        .map_err(|e| e.to_string())
}

async fn run_write_coder_session(
    client: &mut AgentBackend,
    artifacts: &RunArtifacts,
    prompt_a: &str,
    prompt_b: &str,
) -> Result<(), String> {
    let guard = OneShotCoderGuard::begin(client, artifacts, "write").await?;
    let run_res = async {
        run_write_coder_prompt(client, artifacts, prompt_a, "write_a").await?;
        run_write_coder_prompt(client, artifacts, prompt_b, "write_b").await
    }
    .await;
    guard.finish(client, run_res).await
}

pub async fn run_write(
    write_args: &mut WriteArgs,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<(), String> {
    let mut prep = prepare_write_run(write_args, shared, workflow).await?;
    prep.client
        .begin_coder_session(&prep.artifacts.work_dir)
        .await
        .map_err(|e| e.to_string())?;
    emit_run_logs_line(&prep.artifacts)?;
    let acp_res = run_write_coder_session(
        &mut prep.client,
        &prep.artifacts,
        &prep.prompt_a,
        &prep.prompt_b,
    )
    .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 tests {
    use super::*;

    #[test]
    fn kiss_cov_run_write_symbol() {
        let _: Option<WriteRunPrep> = None;
        let _ = (
            run_write,
            prepare_write_prompts,
            write_workspace_dir_display,
            new_write_client,
            create_write_artifacts,
            run_write_coder_prompt,
            run_write_coder_session,
            prepare_write_run,
        );
    }

    #[test]
    fn prepare_write_prompts_and_paths_for_subdir_work_dir() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let work = tmp.path().join("docs");
        std::fs::create_dir(&work).expect("mkdir");
        let artifacts = create_write_artifacts("topic", &work).expect("art");
        assert!(!write_workspace_dir_display(&artifacts).is_empty());
        let tex = format_prompt_path(&work.join("write.tex"), &work);
        let pdf = format_prompt_path(&work.join("write.pdf"), &work);
        assert_eq!((tex.as_str(), pdf.as_str()), ("./write.tex", "./write.pdf"));
        let (a, b) = prepare_write_prompts("how gates exit", (&tex, &pdf), &artifacts).expect("p");
        assert!(a.contains("how gates exit") && a.contains("notes.tex") && !a.contains("{{"));
        assert!(b.contains("`./write.tex`") && b.contains("`./write.pdf`"));
        assert!(!b.contains("`docs/write.tex`") && !b.contains("{{"));
    }

    #[test]
    fn write_client_uses_styled_agent_io() {
        let shared = SharedOpts {
            model: crate::model_id::parse_model_id(crate::config::DEFAULT_CLI_MODEL)
                .expect("model"),
            no_force: true,
            no_tenacious: false,
            gates: false,
            quiet: false,
            verbose: false,
            max_acp_retries: crate::config::DEFAULT_MAX_ACP_RETRIES,
            doc: false,
            name: None,
            git: false,
            creative: false,
        };
        let io = new_write_client(&shared, WorkflowCliOptions { force: false })
            .expect("backend")
            .io;
        assert!(!io.raw_output && io.show_thoughts_on_stdout && io.emit_stdout_markdown);
    }
}