malvin 0.2.9

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

use super::backend_error_tracker::{
    format_backend_consecutive_error_message, format_local_backend_retry_cap_message,
};
use super::sdk_client::SdkClient;

pub const LOCAL_BACKEND_GPU_MEMORY_HINT: &str =
    "Hint: the local server may be out of GPU memory; see `malvin --doc`, section Local LLMs.";

const LOCAL_BACKEND_GPU_MEMORY_NEEDLES: &[&str] = &[
    "compute error",
    "unexpected content-type",
    "empty body",
    "empty response",
];

fn is_keyless_local(client: &SdkClient) -> bool {
    crate::pi_sdk::model_needs_local_llm(&client.model)
}

#[must_use]
pub fn error_suggests_local_gpu_memory(error: &str) -> bool {
    let text = error.to_ascii_lowercase();
    LOCAL_BACKEND_GPU_MEMORY_NEEDLES
        .iter()
        .any(|needle| text.contains(needle))
}

#[must_use]
pub(super) fn with_local_backend_hint(client: &SdkClient, message: String) -> String {
    if is_keyless_local(client) && error_suggests_local_gpu_memory(&message) {
        format!("{message}\n{LOCAL_BACKEND_GPU_MEMORY_HINT}")
    } else {
        message
    }
}

pub(super) fn backend_error_stop(client: &mut SdkClient, error: &str) -> Option<AgentError> {
    let label = client.model.backend.label();
    let message = if client.record_backend_error(error) {
        format_backend_consecutive_error_message(label, error, client.max_acp_retries.get())
    } else {
        let errors = is_keyless_local(client)
            .then(|| client.backend_error_tracker.local_retry_cap_errors())
            .flatten()?;
        format_local_backend_retry_cap_message(label, error, errors)
    };
    Some(AgentError {
        message: with_local_backend_hint(client, message),
        fault: AgentFault::BackendRetryLimit,
    })
}