malvin 0.2.6

Non-interactive research and coding agent
use std::path::{Path, PathBuf};

use crate::acp::{AgentError, backoff_after_agent_failure, retries_noun};
use crate::bridge_sdk::BridgeSpawnArgs;
use crate::model_id::ModelBackend;

use super::super::sdk_client::{BegunCoderSession, SdkClient};
use super::super::sdk_session::SdkSession;

fn cursor_resume_id(client: &SdkClient) -> Option<String> {
    matches!(client.model.backend, ModelBackend::Cursor)
        .then(|| client.last_agent_id.clone())
        .flatten()
}

fn record_spawn_success(
    client: &mut SdkClient,
    session: SdkSession,
    cwd: PathBuf,
    resume_agent_id: Option<&str>,
) -> bool {
    client.record_backend_success();
    adopt_spawned_session(client, session, cwd);
    let resumed = resume_agent_id.is_some();
    if resumed {
        client.header_lifecycle.mark_satisfied_keeping_header();
    } else {
        client.header_lifecycle.mark_fresh_spawn();
    }
    if !resumed {
        emit_agent_started_log(client);
    }
    resumed
}

async fn handle_spawn_failure(
    client: &mut SdkClient,
    err: AgentError,
    attempt: u32,
    max_attempts: u32,
) -> Result<(String, bool), AgentError> {
    let last_error = note_spawn_failure(client, err);
    if client.record_backend_error(&last_error) {
        return Err(AgentError(
            crate::agent_backend::backend_error_tracker::format_backend_consecutive_error_message(
                client.model.backend.label(),
                &last_error,
                client.max_acp_retries,
            ),
        ));
    }
    let stop = backoff_after_agent_failure(
        client.timing.as_ref(),
        &last_error,
        attempt,
        max_attempts,
    )
    .await?;
    Ok((last_error, stop))
}

pub(super) async fn spawn_with_retries(
    client: &mut SdkClient,
    cwd: PathBuf,
    thinking: Option<&str>,
) -> Result<bool, AgentError> {
    let resume_agent_id = cursor_resume_id(client);
    let mut last_error;
    let backoff_ceiling = u32::MAX;
    let mut attempts_used = 0_u32;
    loop {
        attempts_used = attempts_used.saturating_add(1);
        match spawn_for_backend(
            client.model.backend,
            bridge_spawn_args(client, &cwd, thinking),
            resume_agent_id.as_deref(),
            spawn_service_wire(client).as_deref(),
        )
        .await
        {
            Ok(s) => return Ok(record_spawn_success(client, s, cwd, resume_agent_id.as_deref())),
            Err(e) => {
                let (err_msg, stop) =
                    handle_spawn_failure(client, e, attempts_used, backoff_ceiling).await?;
                last_error = err_msg;
                if stop {
                    break;
                }
            }
        }
    }
    let retries = attempts_used.saturating_sub(1);
    Err(AgentError(format!(
        "{}-sdk-bridge failed to spawn after {retries} {}. Last error:\n{last_error}",
        client.model.backend.label(),
        retries_noun(retries)
    )))
}

pub(super) fn spawn_thinking_wire(client: &SdkClient) -> Option<String> {
    client
        .model
        .thinking_param()
        .filter(|_| {
            matches!(
                client.model.backend,
                ModelBackend::NpmPi | ModelBackend::Pi | ModelBackend::Codex
            )
        })
        .map(str::to_string)
}

fn bridge_spawn_args<'a>(
    client: &'a SdkClient,
    cwd: &'a Path,
    thinking: Option<&'a str>,
) -> BridgeSpawnArgs<'a> {
    BridgeSpawnArgs {
        cwd,
        model: &client.model,
        thinking,
        io: client.io,
        run_dir: client.prompts_log_run_dir.clone(),
        timing: client.timing.clone(),
    }
}

pub(super) fn spawn_service_wire(client: &SdkClient) -> Option<String> {
    client
        .model
        .service_param()
        .filter(|_| matches!(client.model.backend, ModelBackend::Codex))
        .map(str::to_string)
}

async fn spawn_for_backend(
    backend: ModelBackend,
    args: BridgeSpawnArgs<'_>,
    resume_agent_id: Option<&str>,
    service: Option<&str>,
) -> Result<SdkSession, AgentError> {
    match backend {
        ModelBackend::Cursor => crate::cursor_sdk::spawn_bridge(args, resume_agent_id)
            .await
            .map(|session| SdkSession::Cursor(Box::new(session))),
        ModelBackend::NpmPi => crate::npm_pi_sdk::spawn_bridge(args)
            .await
            .map(|session| SdkSession::NpmPi(Box::new(session))),
        ModelBackend::Pi => crate::pi_sdk::spawn_bridge(args).await,
        ModelBackend::Codex => crate::codex_sdk::spawn_bridge(args, service)
            .await
            .map(|session| SdkSession::Codex(Box::new(session))),
    }
}

fn adopt_spawned_session(client: &mut SdkClient, s: SdkSession, cwd: PathBuf) {
    if matches!(client.model.backend, ModelBackend::Cursor) {
        remember_agent_id_from(client, &s);
    }
    client.coder = BegunCoderSession::Live { cwd, session: s };
    crate::herdr::notify_reclaim();
}

fn emit_agent_started_log(client: &SdkClient) {
    crate::output::print_stdout_line(crate::output::WHO_A, &client.model.canonical());
}

fn note_spawn_failure(client: &mut SdkClient, err: AgentError) -> String {
    let mut last_error = err.message;
    if matches!(client.model.backend, ModelBackend::Cursor) && client.last_agent_id.take().is_some()
    {
        last_error = format!("{last_error} (resume failed; will create)");
    }
    last_error
}

pub(super) fn remember_agent_id_from(client: &mut SdkClient, session: &SdkSession) {
    let Some(bridge) = session.as_cursor() else {
        return;
    };
    let id = bridge
        .agent_id
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .clone();
    if let Some(id) = id {
        client.last_agent_id = Some(id);
    }
}