recursive/tui/
runtime_builder.rs1use std::sync::Arc;
2
3use crate::config::Config;
4use crate::{AgentRuntime, AgentRuntimeBuilder, LlmProvider};
5
6pub enum RuntimeBuild {
7 Ready(Option<Box<AgentRuntime>>),
8 Offline { reason: String },
9}
10
11pub fn build_runtime() -> RuntimeBuild {
12 let config = match Config::from_env() {
13 Ok(c) => c,
14 Err(e) => {
15 return RuntimeBuild::Offline {
16 reason: format!("failed to load configuration: {e}"),
17 };
18 }
19 };
20
21 let api_key = match config.api_key.as_deref().filter(|k| !k.is_empty()) {
22 Some(k) => k.to_string(),
23 None => {
24 return RuntimeBuild::Offline {
25 reason: "no LLM provider configured. Set RECURSIVE_API_KEY / \
26 OPENAI_API_KEY, or run `recursive config set \
27 provider.api_key <KEY>` to populate \
28 ~/.recursive/config.toml."
29 .to_string(),
30 };
31 }
32 };
33
34 let provider: Arc<dyn LlmProvider> = Arc::new(
35 crate::llm::OpenAiProvider::new(&config.api_base, api_key, &config.model)
36 .with_temperature(config.temperature),
37 );
38
39 let tools =
40 crate::tools::build_standard_tools(&config.workspace, &[], config.shell_timeout_secs);
41
42 match AgentRuntimeBuilder::new()
43 .llm(provider)
44 .tools(tools)
45 .system_prompt(&config.system_prompt)
46 .max_steps(config.max_steps)
47 .build()
48 {
49 Ok(rt) => RuntimeBuild::Ready(Some(Box::new(rt))),
50 Err(e) => RuntimeBuild::Offline {
51 reason: format!("failed to build agent runtime: {e}"),
52 },
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use std::time::Duration;
60
61 use crate::tui::backend::Backend;
62 use crate::tui::events::UiEvent;
63 use crate::tui::events::UserAction;
64
65 #[tokio::test]
66 async fn offline_mode_and_config_file_resolution() {
67 let empty_home = tempfile::tempdir().expect("tempdir");
68 let _pin = crate::test_util::PinnedHome::new(empty_home.path());
69
70 let prev_recursive = std::env::var("RECURSIVE_API_KEY").ok();
71 let prev_openai = std::env::var("OPENAI_API_KEY").ok();
72 std::env::remove_var("RECURSIVE_API_KEY");
73 std::env::remove_var("OPENAI_API_KEY");
74
75 let mut backend = Backend::spawn();
76 backend
77 .action_tx
78 .send(UserAction::SendMessage("hi".into()))
79 .unwrap();
80
81 let mut got_error = false;
82 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
83 while tokio::time::Instant::now() < deadline {
84 match tokio::time::timeout(Duration::from_millis(500), backend.event_rx.recv()).await {
85 Ok(Some(UiEvent::Error { message })) => {
86 assert!(
87 message.contains("no LLM provider configured"),
88 "expected offline reason, got {message:?}"
89 );
90 assert!(
91 message.contains("recursive config set"),
92 "offline reason should mention CLI config helper, got {message:?}"
93 );
94 got_error = true;
95 break;
96 }
97 Ok(Some(_)) => continue,
98 Ok(None) => break,
99 Err(_) => continue,
100 }
101 }
102 let _ = backend.action_tx.send(UserAction::Shutdown);
103 assert!(got_error, "expected an offline-mode UiEvent::Error");
104 drop(backend);
105
106 let cfg_dir = empty_home.path().join(".recursive");
108 std::fs::create_dir_all(&cfg_dir).expect("mkdir");
109 std::fs::write(
110 cfg_dir.join("config.toml"),
111 r#"[provider]
112api_key = "sk-test-from-config"
113api_base = "https://api.example.invalid"
114model = "test-model-from-config"
115type = "openai"
116"#,
117 )
118 .expect("write config");
119
120 let build = build_runtime();
121 match build {
122 RuntimeBuild::Ready(_) => {}
123 RuntimeBuild::Offline { reason } => {
124 panic!("expected Ready when config.toml has api_key, got Offline: {reason}");
125 }
126 }
127
128 if let Some(v) = prev_recursive {
129 std::env::set_var("RECURSIVE_API_KEY", v);
130 }
131 if let Some(v) = prev_openai {
132 std::env::set_var("OPENAI_API_KEY", v);
133 }
134 }
135}