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