rho_coding_agent/app/
bootstrap.rs1use 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;