Skip to main content

bamboo_server/app_state/
provider_api.rs

1use super::*;
2
3use crate::tools::ToolSurface;
4
5impl AppState {
6    /// Get a clone of the current provider
7    ///
8    /// Returns a thread-safe reference to the current LLM provider.
9    /// This is the preferred way to access the provider for making requests.
10    ///
11    /// # Returns
12    ///
13    /// An Arc reference to the current provider implementation.
14    ///
15    /// # Example
16    ///
17    /// ```rust,no_run
18    /// use bamboo_server::app_state::AppState;
19    /// use std::path::PathBuf;
20    ///
21    /// #[tokio::main]
22    /// async fn main() {
23    ///     let state = AppState::new(PathBuf::from("/path/to/.bamboo"))
24    ///         .await
25    ///         .expect("failed to initialize app state");
26    ///     let provider = state.get_provider().await;
27    ///
28    ///     // Use provider to make LLM requests...
29    /// }
30    /// ```
31    pub async fn get_provider(&self) -> Arc<dyn LLMProvider> {
32        // Important: return the reloadable handle, not a snapshot clone of the current provider.
33        // This ensures config/provider switches take effect without restarting the server.
34        self.provider_handle.clone()
35    }
36
37    /// Get a provider for a specific [`ProviderModelRef`].
38    ///
39    /// Used when `features.provider_model_ref` is enabled to route requests
40    /// to the correct provider based on the model reference.
41    pub fn get_provider_for_model_ref(
42        &self,
43        target: &bamboo_domain::ProviderModelRef,
44    ) -> Result<Arc<dyn LLMProvider>, AppError> {
45        self.provider_router
46            .route(target)
47            .map_err(|e| AppError::BadRequest(e.to_string()))
48    }
49
50    /// Get the appropriate provider for a named provider endpoint (e.g., "openai", "anthropic").
51    ///
52    /// Uses the registry when the `provider_model_ref` feature flag is enabled,
53    /// otherwise falls back to the default provider.
54    pub async fn get_provider_for_endpoint(
55        &self,
56        provider_name: &str,
57    ) -> Result<Arc<dyn LLMProvider>, AppError> {
58        let use_registry = {
59            let config = self.config.read().await;
60            config.features.provider_model_ref
61        };
62
63        if use_registry {
64            self.provider_registry.get(provider_name).ok_or_else(|| {
65                AppError::InternalError(anyhow::anyhow!(
66                    "Provider '{}' not found in registry",
67                    provider_name
68                ))
69            })
70        } else {
71            Ok(self.get_provider().await)
72        }
73    }
74
75    /// Shutdown all MCP servers gracefully
76    ///
77    /// Sends shutdown signals to all running MCP server processes
78    /// and waits for them to terminate cleanly.
79    ///
80    /// This should be called during application shutdown to ensure
81    /// MCP servers are not left running as orphaned processes. Invoked by
82    /// [`crate::server::web_service::WebService::stop`]. #119.
83    pub async fn shutdown(&self) {
84        tracing::info!("Shutting down MCP servers...");
85        // Stop the supervised MCP proxy service (issue #47) so its reconnect
86        // supervisor exits cleanly instead of looping after an intended stop.
87        self.mcp_proxy_shutdown.cancel();
88        self.mcp_manager.shutdown_all().await;
89        tracing::info!("MCP servers shut down complete");
90    }
91
92    /// Get the tool executor for a specific surface variant.
93    ///
94    /// Use [`ToolSurface::Root`] for primary sessions,
95    /// [`ToolSurface::Child`] for child sessions, etc.
96    pub fn tools_for(
97        &self,
98        surface: ToolSurface,
99    ) -> Arc<dyn bamboo_agent_core::tools::ToolExecutor> {
100        self.tool_factory.get(surface)
101    }
102
103    /// Get all tool schemas from the composite tool executor
104    ///
105    /// Returns schemas for both built-in tools and MCP-provided tools.
106    /// These schemas are used to inform the LLM about available tools.
107    ///
108    /// # Returns
109    ///
110    /// Vector of tool schemas in Anthropic's tool definition format.
111    pub fn get_all_tool_schemas(&self) -> Vec<bamboo_agent_core::tools::ToolSchema> {
112        self.tool_factory.get(ToolSurface::Root).list_tools()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    /// Helper: create AppState from a temp dir.
121    async fn make_state() -> (tempfile::TempDir, AppState) {
122        let temp_dir = tempfile::tempdir().expect("temp dir");
123        let state = AppState::new(temp_dir.path().to_path_buf())
124            .await
125            .expect("app state");
126        (temp_dir, state)
127    }
128
129    // ---- get_provider ----
130
131    #[tokio::test]
132    async fn get_provider_returns_a_provider() {
133        let (_temp, state) = make_state().await;
134        let provider = state.get_provider().await;
135        let models = provider.list_models().await;
136        // Default config has no API keys, so the unconfigured provider
137        // returns an auth error — but it should still return a provider handle.
138        assert!(
139            models.is_err(),
140            "Default provider should be UnconfiguredProvider"
141        );
142    }
143
144    // ---- get_provider_for_endpoint ----
145
146    #[tokio::test]
147    async fn endpoint_flag_off_returns_default_provider() {
148        let (_temp, state) = make_state().await;
149        // Flag is OFF by default
150        {
151            let config = state.config.read().await;
152            assert!(!config.features.provider_model_ref);
153        }
154
155        let result = state.get_provider_for_endpoint("openai").await;
156        assert!(
157            result.is_ok(),
158            "Flag OFF should always return default provider"
159        );
160    }
161
162    #[tokio::test]
163    async fn endpoint_flag_on_unknown_provider_returns_error() {
164        let (_temp, state) = make_state().await;
165        {
166            let mut config = state.config.write().await;
167            config.features.provider_model_ref = true;
168        }
169
170        let result = state.get_provider_for_endpoint("nonexistent").await;
171        assert!(result.is_err(), "Flag ON with unknown provider should fail");
172    }
173
174    #[tokio::test]
175    async fn endpoint_flag_on_copilot_returns_provider() {
176        let (_temp, state) = make_state().await;
177        {
178            let mut config = state.config.write().await;
179            config.features.provider_model_ref = true;
180        }
181
182        // Copilot is always available (no API key required)
183        let result = state.get_provider_for_endpoint("copilot").await;
184        assert!(result.is_ok(), "Flag ON with copilot should succeed");
185    }
186
187    // ---- get_provider_for_model_ref ----
188
189    #[tokio::test]
190    async fn model_ref_unknown_provider_returns_error() {
191        let (_temp, state) = make_state().await;
192        let target = bamboo_domain::ProviderModelRef::new("nonexistent", "some-model");
193        let result = state.get_provider_for_model_ref(&target);
194        assert!(result.is_err());
195    }
196
197    #[tokio::test]
198    async fn model_ref_copilot_returns_provider() {
199        let (_temp, state) = make_state().await;
200        let target = bamboo_domain::ProviderModelRef::new("copilot", "gpt-4o");
201        let result = state.get_provider_for_model_ref(&target);
202        assert!(result.is_ok(), "copilot provider should be routable");
203    }
204
205    // ---- tools_for ----
206
207    #[tokio::test]
208    async fn tools_for_root_returns_tool_executor() {
209        let (_temp, state) = make_state().await;
210        let executor = state.tools_for(ToolSurface::Root);
211        let schemas = executor.list_tools();
212        assert!(!schemas.is_empty(), "Root tools should not be empty");
213    }
214
215    // ---- get_all_tool_schemas ----
216
217    #[tokio::test]
218    async fn get_all_tool_schemas_includes_core_tools() {
219        let (_temp, state) = make_state().await;
220        let schemas = state.get_all_tool_schemas();
221        let names: std::collections::HashSet<&str> =
222            schemas.iter().map(|s| s.function.name.as_str()).collect();
223        assert!(names.contains("Task"));
224        assert!(names.contains("SubAgent"));
225    }
226}