malvin 0.2.9

Non-interactive research and coding agent
use crate::acp::{AgentError, AgentFault, CoderPromptOptions};
use crate::nested_budget_scopes::BudgetScopeLayer;

use super::acp_attempt_loop::{BackoffChoice, RetrySpec, retry_until_ok};
use super::sdk_client::{CoderSessionHeader, SdkClient, begun_cwd, live_session};
use super::sdk_client_prompt::{
    append_prompt_files, emit_prompt_stdout, force_fresh_agent_for_retry,
    teardown_sdk_session_after_transport_error,
};

#[must_use]
pub const fn session_header_is_satisfied(client: &SdkClient) -> bool {
    client.header_lifecycle.is_satisfied()
}

#[must_use]
pub fn pending_session_header(client: &SdkClient) -> Option<(String, String)> {
    client
        .header_lifecycle
        .pending_header()
        .map(|h| (h.prompt.clone(), h.stdout_label.clone()))
}

pub(super) async fn send_bound_session_header(client: &mut SdkClient) -> Result<(), AgentError> {
    if client.header_lifecycle.is_satisfied() {
        return Ok(());
    }
    let Some(header) = client.header_lifecycle.pending_header().cloned() else {
        return Ok(());
    };
    let opts = header_prompt_options(&header.stdout_label);
    emit_prompt_stdout(client, &header.prompt, &header.log_who, &opts);
    append_prompt_files(client, &header.prompt, &header.log_path, &header.log_who)?;
    try_send_header_with_retries(client, &header, &opts).await
}

fn header_prompt_options<'a>(stdout_label: &'a str) -> CoderPromptOptions<'a> {
    CoderPromptOptions {
        llm_phase: Some(crate::run_timing::TimingPhase::Implement),
        stdout_bracket_label: Some(stdout_label),
        fresh_agent_on_retry: true,
        ..Default::default()
    }
}

#[cfg(test)]
#[must_use]
pub(crate) fn header_prompt_options_for_test<'a>() -> CoderPromptOptions<'a> {
    header_prompt_options(crate::prompts::header_prompt_file())
}

async fn try_send_header_with_retries(
    client: &mut SdkClient,
    header: &CoderSessionHeader,
    opts: &CoderPromptOptions<'_>,
) -> Result<(), AgentError> {
    let lead = format!("{} SDK header prompt failed", client.model.backend.label());
    let spec = RetrySpec {
        ceiling: BudgetScopeLayer::AcpAttempt.effective_attempt_ceiling(None, false),
        failure_lead: &lead,
        backoff: BackoffChoice::Consult,
    };
    retry_until_ok!(
        client,
        spec,
        |c| send_header_once(c, &header.prompt, opts).await,
        |c, err| recover_header_send_failure(c, err).await
    )?;
    client.header_lifecycle.mark_satisfied_keeping_header();
    Ok(())
}

async fn send_header_once(
    client: &mut SdkClient,
    prompt: &str,
    opts: &CoderPromptOptions<'_>,
) -> Result<(), AgentError> {
    let started = std::time::Instant::now();
    let result = match live_session(client) {
        Some(session) => session.send_prompt(prompt).await,
        None => Err(AgentError("begin_coder_session was not called".into())),
    };
    if let Some(p) = opts.llm_phase {
        crate::run_timing::record_llm(client.timing.as_ref(), p, started.elapsed());
    }
    result
}

async fn recover_header_send_failure(
    client: &mut SdkClient,
    err: AgentError,
) -> Result<AgentError, AgentError> {
    if err.fault == AgentFault::OutputCap {
        return Err(err);
    }
    teardown_sdk_session_after_transport_error(client, &err).await;
    force_fresh_agent_for_retry(client).await;
    if let Some(cwd) = begun_cwd(client).cloned()
        && let Err(e) = client.begin_coder_session(&cwd).await
        && e.fault == AgentFault::BackendRetryLimit
    {
        return Err(e);
    }
    Ok(err)
}