looprs-cli 0.5.1

looprs binary: interactive REPL and scriptable entrypoint
Documentation
use looprs::Agent;
use looprs::ModelId;
use looprs::ProviderConfig;
use looprs::RuntimeSettings;
use looprs::app_config::AppConfig;
use looprs::providers::{ProviderOverrides, create_provider_with_overrides};
use miette::miette;

const MISSING_LOCAL_MODEL: &str = "No local model configured";

pub struct BootstrappedRuntime {
    pub app_config: AppConfig,
    pub provider_config: ProviderConfig,
    pub provider_name: String,
    pub model: String,
    pub agent: Agent,
}

pub async fn bootstrap_runtime(
    model_override: Option<ModelId>,
) -> anyhow::Result<BootstrappedRuntime> {
    let app_config = AppConfig::load().unwrap_or_default();

    let provider = create_provider_with_overrides(ProviderOverrides {
        model: model_override,
    })
    .await?;

    let provider_name = provider.name().to_string();
    let model = provider.model().as_str().to_string();

    let provider_config = ProviderConfig::load().unwrap_or_default();
    let max_tokens_override = provider_config.merged_settings(&provider_name).max_tokens;
    let runtime = RuntimeSettings {
        defaults: app_config.defaults.clone(),
        max_tokens_override,
        fs_mode: app_config.agents.fs_mode,
    };
    let session_logger = looprs::adapters::default_session_store();
    let agent = Agent::new_with_runtime(
        provider,
        runtime,
        app_config.file_ref_policy(),
        session_logger,
        Box::new(looprs::adapters::UiOutput),
    )?;

    Ok(BootstrappedRuntime {
        app_config,
        provider_config,
        provider_name,
        model,
        agent,
    })
}

pub fn provider_bootstrap_report(error: &anyhow::Error) -> Option<miette::Report> {
    let provider_error = error.downcast_ref::<looprs::ProviderError>()?;

    match provider_error {
        looprs::ProviderError::Config(message) if message.contains(MISSING_LOCAL_MODEL) => {
            Some(miette!(
                code = "looprs::provider::missing_local_model",
                help = "Configure an Ollama model before starting looprs:\n  - Set OLLAMA_MODEL=llama3.2:latest or MODEL=llama3.2:latest\n  - Pass --model llama3.2:latest for one run\n  - Add { \"provider\": \"local\", \"local\": { \"model\": \"llama3.2:latest\" } } to .looprs/provider.json\n\nRun `ollama list` to see installed local models.",
                "No local model configured for Ollama"
            ))
        }
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn missing_local_model_report_explains_model_options() {
        let err = anyhow::Error::new(looprs::ProviderError::Config(
            "No local model configured. Set MODEL or OLLAMA_MODEL, or configure .looprs/provider.json"
                .to_string(),
        ));

        let report = provider_bootstrap_report(&err).expect("missing local model report");
        let rendered = format!("{report:?}");

        assert!(rendered.contains("No local model configured for Ollama"));
        assert!(rendered.contains("OLLAMA_MODEL"));
        assert!(rendered.contains("--model"));
        assert!(rendered.contains(".looprs/provider.json"));
    }

    #[test]
    fn unrelated_provider_error_does_not_get_special_report() {
        let err = anyhow::Error::new(looprs::ProviderError::MissingApiKey(
            "anthropic".to_string(),
        ));

        assert!(provider_bootstrap_report(&err).is_none());
    }
}

#[cfg(test)]
mod live_llm_tests {
    use super::*;
    use crate::runtime::events::gui_turn_metadata;
    use crate::runtime::session::run_single_turn;

    fn live_tests_enabled() -> bool {
        std::env::var("LOOPRS_RUN_LIVE_LLM_TESTS")
            .ok()
            .is_some_and(|v| v == "1" || v == "true")
    }

    fn has_any_api_key() -> bool {
        std::env::var("ANTHROPIC_API_KEY").ok().is_some()
            || std::env::var("OPENAI_API_KEY").ok().is_some()
    }

    #[tokio::test]
    #[ignore]
    async fn live_llm_single_turn_smoke() {
        if !live_tests_enabled() || !has_any_api_key() {
            return;
        }

        let mut rt = bootstrap_runtime(None)
            .await
            .expect("bootstrap_runtime failed");

        rt.agent.clear_history();
        let response = run_single_turn(
            &mut rt.agent,
            "Reply with the single word OK.",
            gui_turn_metadata(),
        )
        .await
        .expect("run_single_turn failed");

        assert!(response.to_uppercase().contains("OK"));
    }
}