Skip to main content

bamboo_server/app_state/
config_runtime.rs

1use super::*;
2
3impl AppState {
4    /// Reload the provider based on current configuration
5    ///
6    /// Re-reads the configuration and creates a new LLM provider
7    /// instance, allowing runtime switching of providers or models.
8    ///
9    /// # Returns
10    ///
11    /// `Ok(())` if the provider was successfully reloaded.
12    ///
13    /// # Errors
14    ///
15    /// Returns an error if:
16    /// - Configuration cannot be read
17    /// - Provider initialization fails (e.g., invalid API key)
18    ///
19    /// # Example
20    ///
21    /// ```rust,no_run
22    /// use bamboo_server::app_state::AppState;
23    /// use std::path::PathBuf;
24    ///
25    /// #[tokio::main]
26    /// async fn main() {
27    ///     let state = AppState::new(PathBuf::from("/path/to/.bamboo"))
28    ///         .await
29    ///         .expect("failed to initialize app state");
30    ///
31    ///     // User updated config file...
32    ///     state.reload_provider().await.expect("Provider reload failed");
33    /// }
34    /// ```
35    pub async fn reload_provider(&self) -> Result<(), bamboo_infrastructure::LLMError> {
36        let config = self.config.read().await.clone();
37
38        let configured_model = match config.provider.as_str() {
39            "copilot" => config
40                .providers
41                .copilot
42                .as_ref()
43                .and_then(|p| p.model.as_ref()),
44            "openai" => config
45                .providers
46                .openai
47                .as_ref()
48                .and_then(|p| p.model.as_ref()),
49            "anthropic" => config
50                .providers
51                .anthropic
52                .as_ref()
53                .and_then(|p| p.model.as_ref()),
54            "gemini" => config
55                .providers
56                .gemini
57                .as_ref()
58                .and_then(|p| p.model.as_ref()),
59            _ => None,
60        };
61
62        tracing::info!(
63            "Reloading provider: type={}, model={:?}",
64            config.provider,
65            configured_model
66        );
67
68        let new_provider =
69            bamboo_infrastructure::create_provider_with_dir(&config, self.app_data_dir.clone())
70                .await?;
71
72        let mut provider = self.provider.write().await;
73        *provider = new_provider;
74
75        tracing::info!("Provider reloaded successfully to: {}", config.provider);
76        Ok(())
77    }
78
79    /// Reload the configuration from file
80    ///
81    /// Reads the configuration file again and updates the in-memory
82    /// config. Note: This does NOT automatically reload the provider;
83    /// call `reload_provider()` afterwards if needed.
84    ///
85    /// # Returns
86    ///
87    /// The newly loaded configuration.
88    ///
89    /// # Example
90    ///
91    /// ```rust,no_run
92    /// use bamboo_server::app_state::AppState;
93    /// use std::path::PathBuf;
94    ///
95    /// #[tokio::main]
96    /// async fn main() {
97    ///     let state = AppState::new(PathBuf::from("/path/to/.bamboo"))
98    ///         .await
99    ///         .expect("failed to initialize app state");
100    ///
101    ///     // Reload config from disk
102    ///     let new_config = state.reload_config().await;
103    ///
104    ///     // Optionally reload provider with new config
105    ///     state.reload_provider().await.ok();
106    /// }
107    /// ```
108    pub async fn reload_config(&self) -> Config {
109        let new_config = Config::from_data_dir(Some(self.app_data_dir.clone()));
110        let mut config = self.config.write().await;
111        *config = new_config.clone();
112        new_config
113    }
114
115    /// Persist the current in-memory config to disk (`{app_data_dir}/config.json`).
116    ///
117    /// This is the single "exit" for configuration writes in the server runtime.
118    pub async fn persist_config(&self) -> anyhow::Result<()> {
119        let config = self.config.read().await.clone();
120        let data_dir = self.app_data_dir.clone();
121        tokio::task::spawn_blocking(move || config.save_to_dir(data_dir))
122            .await
123            .map_err(|e| anyhow::anyhow!("Config save task failed: {e}"))??;
124        Ok(())
125    }
126
127    async fn persist_config_snapshot(&self, config: Config) -> anyhow::Result<()> {
128        let data_dir = self.app_data_dir.clone();
129        tokio::task::spawn_blocking(move || config.save_to_dir(data_dir))
130            .await
131            .map_err(|e| anyhow::anyhow!("Config save task failed: {e}"))??;
132        Ok(())
133    }
134
135    /// Unified config update entrypoint.
136    ///
137    /// Invariants:
138    /// - Update in-memory first
139    /// - Persist to disk
140    /// - Apply runtime side-effects last (provider reload, MCP reconcile)
141    pub async fn update_config<F>(
142        &self,
143        update: F,
144        effects: ConfigUpdateEffects,
145    ) -> Result<Config, AppError>
146    where
147        F: FnOnce(&mut Config) -> Result<(), AppError>,
148    {
149        let snapshot = {
150            let mut cfg = self.config.write().await;
151            update(&mut cfg)?;
152            cfg.publish_env_vars();
153            cfg.clone()
154        };
155
156        self.persist_config_snapshot(snapshot.clone())
157            .await
158            .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to save config: {e}")))?;
159
160        self.apply_config_effects(snapshot.clone(), effects).await?;
161        Ok(snapshot)
162    }
163
164    /// Replace the full config (used for JSON merge endpoints).
165    pub async fn replace_config(
166        &self,
167        new_config: Config,
168        effects: ConfigUpdateEffects,
169    ) -> Result<Config, AppError> {
170        {
171            let mut cfg = self.config.write().await;
172            *cfg = new_config.clone();
173            cfg.publish_env_vars();
174        }
175
176        self.persist_config_snapshot(new_config.clone())
177            .await
178            .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to save config: {e}")))?;
179
180        self.apply_config_effects(new_config.clone(), effects)
181            .await?;
182        Ok(new_config)
183    }
184
185    async fn apply_config_effects(
186        &self,
187        new_config: Config,
188        effects: ConfigUpdateEffects,
189    ) -> Result<(), AppError> {
190        if effects.reload_provider {
191            self.reload_provider().await.map_err(|e| {
192                AppError::InternalError(anyhow::anyhow!(
193                    "Failed to reload provider after updating config: {e}"
194                ))
195            })?;
196        }
197
198        if effects.reconcile_mcp {
199            self.mcp_manager
200                .reconcile_from_config(&new_config.mcp)
201                .await;
202        }
203
204        Ok(())
205    }
206}