rho-coding-agent 0.22.1

A lightweight agent harness inspired by Pi
Documentation
use std::io::{self, IsTerminal};

use crate::{
    agent::Agent,
    cli::{Cli, Command},
    credentials::OsCredentialStore,
    herdr::HerdrReporter,
    model::{build_provider, models_dev::cached_model_metadata, ModelError, UnavailableProvider},
    tool::{ToolContext, ToolRegistry},
    tools, update,
};

use super::{automation, cli_config, config_repository::ConfigRepository, interactive, login};

pub async fn run(cli: Cli) -> anyhow::Result<()> {
    cli_config::validate(&cli)?;
    if matches!(cli.command, Some(Command::Update)) {
        return update::run_update(env!("CARGO_PKG_VERSION")).await;
    }
    if let Some(Command::Login {
        provider,
        device_auth,
    }) = &cli.command
    {
        return login::run(provider, *device_auth).await;
    }

    let config_path = cli.config.clone();
    let config_repository = ConfigRepository::new(config_path.clone());
    let mut config = config_repository.load()?;
    let store = OsCredentialStore;
    cli_config::refresh_model_cache(&cli, &store).await?;
    if cli_config::apply_overrides(&mut config, &cli)? {
        config_repository.save(&config)?;
    }

    validate_terminal_mode(&cli)?;
    let automation_prompt = automation::prompt_for_command(&cli.command)?;
    let update_notice = if cli.command.is_none() && config.check_for_updates {
        update::update_notice(env!("CARGO_PKG_VERSION")).await
    } else {
        None
    };

    if config.provider == "anthropic"
        && cached_model_metadata(&config.provider, &config.model).is_none()
    {
        let _ =
            crate::model::models_dev::fetch_model_metadata(&config.provider, &config.model).await;
    }

    let provider_result = build_provider(&config.provider, &config.model, config.reasoning);
    let missing_auth_error = provider_result
        .as_ref()
        .err()
        .filter(|error| is_interactive_startup_unavailable_error(error))
        .map(ToString::to_string);
    let provider = match provider_result {
        Ok(provider) => provider,
        Err(error)
            if automation_prompt.is_none() && is_interactive_startup_unavailable_error(&error) =>
        {
            Box::new(UnavailableProvider::new(error))
        }
        Err(error) => return Err(error.into()),
    };
    let registry = if cli.no_tools {
        ToolRegistry::new()
    } else {
        tools::registry(&config)
    };
    let cwd = std::env::current_dir()?;
    let context = ToolContext {
        cwd: cwd.clone(),
        max_output_bytes: config.max_output_bytes,
    };
    let herdr = HerdrReporter::from_env();
    let mut agent = Agent::new(provider, registry, context);
    if cli.no_system_prompt {
        agent = agent.without_system_prompt();
    }
    agent.set_compaction_config((&config).into());
    agent.set_context_window(
        cached_model_metadata(&config.provider, &config.model)
            .and_then(|metadata| metadata.display_context_window()),
    );

    match automation_prompt {
        Some(prompt) => automation::run(&mut agent, prompt, &herdr).await,
        None => {
            interactive::run(
                agent,
                interactive::Startup {
                    cli: &cli,
                    config,
                    config_repository,
                    cwd,
                    missing_auth_error,
                    update_notice,
                    herdr,
                },
            )
            .await
        }
    }
}

fn validate_terminal_mode(cli: &Cli) -> anyhow::Result<()> {
    if cli.command.is_none() && (!io::stdin().is_terminal() || !io::stdout().is_terminal()) {
        anyhow::bail!(
            "rho's default mode is the interactive TUI; use `rho run` for non-interactive automation"
        );
    }
    Ok(())
}

fn is_interactive_startup_unavailable_error(error: &ModelError) -> bool {
    matches!(
        error,
        ModelError::MissingApiKey
            | ModelError::MissingCodexAuth
            | ModelError::MissingAnthropicApiKey
            | ModelError::MissingGithubCopilotAuth
            | ModelError::Credentials(_)
            | ModelError::UnsupportedProvider(_)
    )
}

#[cfg(test)]
#[path = "bootstrap_tests.rs"]
mod tests;