Skip to main content

recursive/tui/
runtime_builder.rs

1use 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        // Part B: config.toml with api_key → Ready
107        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}