Skip to main content

rho_coding_agent/app/
bootstrap.rs

1use std::io::{self, IsTerminal};
2
3use crate::{
4    agent::Agent,
5    cli::{Cli, Command},
6    credentials::OsCredentialStore,
7    herdr::HerdrReporter,
8    model::{build_provider, models_dev::cached_model_metadata, ModelError, UnavailableProvider},
9    tool::{ToolContext, ToolRegistry},
10    tools, update,
11};
12
13use super::{automation, cli_config, config_repository::ConfigRepository, interactive, login};
14
15pub async fn run(cli: Cli) -> anyhow::Result<()> {
16    cli_config::validate(&cli)?;
17    if matches!(cli.command, Some(Command::Update)) {
18        return update::run_update(env!("CARGO_PKG_VERSION")).await;
19    }
20    if let Some(Command::Login {
21        provider,
22        device_auth,
23    }) = &cli.command
24    {
25        return login::run(provider, *device_auth).await;
26    }
27
28    let config_path = cli.config.clone();
29    let config_repository = ConfigRepository::new(config_path.clone());
30    let mut config = config_repository.load()?;
31    let store = OsCredentialStore;
32    cli_config::refresh_model_cache(&cli, &store).await?;
33    if cli_config::apply_overrides(&mut config, &cli)? {
34        config_repository.save(&config)?;
35    }
36
37    validate_terminal_mode(&cli)?;
38    let automation_prompt = automation::prompt_for_command(&cli.command)?;
39    if automation_prompt.is_some()
40        && config.provider == "anthropic"
41        && cached_model_metadata(&config.provider, &config.model).is_none()
42    {
43        let _ =
44            crate::model::models_dev::fetch_model_metadata(&config.provider, &config.model).await;
45    }
46    let pending_update_notice = (cli.command.is_none() && config.check_for_updates)
47        .then(|| tokio::spawn(update::update_notice(env!("CARGO_PKG_VERSION"))));
48
49    let provider_result = build_provider(&config.provider, &config.model, config.reasoning);
50    let missing_auth_error = provider_result
51        .as_ref()
52        .err()
53        .filter(|error| is_interactive_startup_unavailable_error(error))
54        .map(ToString::to_string);
55    let provider = match provider_result {
56        Ok(provider) => provider,
57        Err(error)
58            if automation_prompt.is_none() && is_interactive_startup_unavailable_error(&error) =>
59        {
60            Box::new(UnavailableProvider::new(error))
61        }
62        Err(error) => return Err(error.into()),
63    };
64    let registry = if cli.no_tools {
65        ToolRegistry::new()
66    } else {
67        tools::registry(&config)
68    };
69    let cwd = std::env::current_dir()?;
70    let context = ToolContext {
71        cwd: cwd.clone(),
72        max_output_bytes: config.max_output_bytes,
73    };
74    let herdr = HerdrReporter::from_env();
75    let mut agent = Agent::new(provider, registry, context).with_history(Vec::new());
76    if cli.no_system_prompt {
77        agent = agent.without_system_prompt();
78    }
79    agent.set_compaction_config((&config).into());
80    agent.set_context_window(
81        cached_model_metadata(&config.provider, &config.model)
82            .and_then(|metadata| metadata.display_context_window()),
83    );
84
85    let result = match automation_prompt {
86        Some(prompt) => automation::run(&mut agent, prompt, &herdr).await,
87        None => {
88            interactive::run(
89                &mut agent,
90                interactive::Startup {
91                    cli: &cli,
92                    config,
93                    config_repository,
94                    cwd,
95                    missing_auth_error,
96                    pending_update_notice,
97                    herdr,
98                },
99            )
100            .await
101        }
102    };
103    agent.shutdown().await;
104    result
105}
106
107fn validate_terminal_mode(cli: &Cli) -> anyhow::Result<()> {
108    if cli.command.is_none() && (!io::stdin().is_terminal() || !io::stdout().is_terminal()) {
109        anyhow::bail!(
110            "rho's default mode is the interactive TUI; use `rho run` for non-interactive automation"
111        );
112    }
113    Ok(())
114}
115
116fn is_interactive_startup_unavailable_error(error: &ModelError) -> bool {
117    matches!(
118        error,
119        ModelError::MissingApiKey
120            | ModelError::MissingCodexAuth
121            | ModelError::MissingAnthropicApiKey
122            | ModelError::MissingGithubCopilotAuth
123            | ModelError::Credentials(_)
124            | ModelError::UnsupportedProvider(_)
125    )
126}
127
128#[cfg(test)]
129#[path = "bootstrap_tests.rs"]
130mod tests;