malvin 0.2.5

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::prompts::PromptStore;
use crate::run_timing::TimingPhase;
use crate::workflow_context::format_prompt_path;

use super::WriteArgs;
use super::prep::{WriteInitialPromptInput, build_write_workflow_initial_prompt, compose_write_b_prompt, write_preflight};

struct WriteRunPrep {
    client: AgentBackend,
    artifacts: RunArtifacts,
    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 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::run_id::activate_run(artifacts.run_dir.clone());
    Ok(artifacts)
}

fn prepare_write_prompt_store() -> Result<PromptStore, String> {
    let store = PromptStore::default_store();
    store
        .ensure_defaults()
        .map_err(|e: crate::prompts::PromptError| e.0)?;
    Ok(store)
}

fn bind_write_initial_prompt(
    client: &mut AgentBackend,
    shared: &SharedOpts,
    artifacts: &RunArtifacts,
    request_text: &str,
) -> Result<(), String> {
    let store = prepare_write_prompt_store()?;
    let workspace_dir = write_workspace_dir_display(artifacts);
    let initial = build_write_workflow_initial_prompt(WriteInitialPromptInput {
        store: &store,
        artifacts,
        model: &shared.model.canonical(),
        git: shared.git,
        request_text,
        workspace_dir: &workspace_dir,
    })?;
    client.bind_session_header_parts(
        initial.body,
        artifacts.log_path(initial.log_who),
        &initial.stdout_label,
        initial.log_who,
    );
    Ok(())
}

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)?;
    bind_write_initial_prompt(&mut client, shared, &artifacts, &request_text)?;
    let store = prepare_write_prompt_store()?;
    let workspace_dir = write_workspace_dir_display(&artifacts);
    let prompt_b = compose_write_b_prompt(&store, &tex_display, &pdf_display, &workspace_dir)?;
    Ok(WriteRunPrep {
        client,
        artifacts,
        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_b: &str,
) -> Result<(), String> {
    let guard = OneShotCoderGuard::begin(client, artifacts, "write").await?;
    let run_res = run_write_coder_prompt(client, artifacts, prompt_b, "write_b").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?;
    emit_run_logs_line(&prep.artifacts)?;
    let acp_res =
        run_write_coder_session(&mut prep.client, &prep.artifacts, &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,
            write_workspace_dir_display,
            new_write_client,
            create_write_artifacts,
            run_write_coder_prompt,
            run_write_coder_session,
            prepare_write_run,
            bind_write_initial_prompt,
            prepare_write_prompt_store,
        );
    }

    #[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 workspace_dir = write_workspace_dir_display(&artifacts);
        let store = PromptStore::default_store();
        let b = compose_write_b_prompt(&store, &tex, &pdf, &workspace_dir).expect("b");
        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 {
            background: false,
            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,
            git: false,
            creative: None,
            no_kpop: 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);
    }
}