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        self.provider_registry
39            .reload_from_config(&config, self.app_data_dir.clone())
40            .await?;
41
42        let default_provider_name = self.provider_registry.default_provider_name();
43        tracing::info!(
44            default_provider = %default_provider_name,
45            legacy_provider = %config.provider,
46            has_provider_instances = config.has_provider_instances(),
47            "Reloading provider runtime from current config"
48        );
49
50        let new_provider = self.provider_registry.get_default().unwrap_or_else(|| {
51            let message = if config.has_provider_instances() {
52                format!(
53                    "Default provider instance '{}' is not available or failed to initialize",
54                    default_provider_name
55                )
56            } else {
57                format!(
58                    "Provider '{}' is not available or failed to initialize",
59                    config.provider
60                )
61            };
62            Arc::new(UnconfiguredProvider { message }) as Arc<dyn LLMProvider>
63        });
64
65        let mut provider = self.provider.write().await;
66        *provider = new_provider;
67
68        tracing::info!(
69            default_provider = %default_provider_name,
70            "Provider reloaded successfully"
71        );
72        Ok(())
73    }
74
75    /// Reload the configuration from file
76    ///
77    /// Reads the configuration file again and updates the in-memory
78    /// config. Note: This does NOT automatically reload the provider;
79    /// call `reload_provider()` afterwards if needed.
80    ///
81    /// # Returns
82    ///
83    /// The newly loaded configuration.
84    ///
85    /// # Example
86    ///
87    /// ```rust,no_run
88    /// use bamboo_server::app_state::AppState;
89    /// use std::path::PathBuf;
90    ///
91    /// #[tokio::main]
92    /// async fn main() {
93    ///     let state = AppState::new(PathBuf::from("/path/to/.bamboo"))
94    ///         .await
95    ///         .expect("failed to initialize app state");
96    ///
97    ///     // Reload config from disk
98    ///     let new_config = state.reload_config().await;
99    ///
100    ///     // Optionally reload provider with new config
101    ///     state.reload_provider().await.ok();
102    /// }
103    /// ```
104    pub async fn reload_config(&self) -> Config {
105        let new_config = Config::from_data_dir(Some(self.app_data_dir.clone()));
106        let mut config = self.config.write().await;
107        *config = new_config.clone();
108        new_config
109    }
110
111    /// Persist the current in-memory config to disk (`{app_data_dir}/config.json`).
112    ///
113    /// This is the single "exit" for configuration writes in the server runtime.
114    pub async fn persist_config(&self) -> anyhow::Result<()> {
115        let config = self.config.read().await.clone();
116        let data_dir = self.app_data_dir.clone();
117        tokio::task::spawn_blocking(move || config.save_to_dir(data_dir))
118            .await
119            .map_err(|e| anyhow::anyhow!("Config save task failed: {e}"))??;
120        Ok(())
121    }
122
123    async fn persist_config_snapshot(&self, config: Config) -> anyhow::Result<()> {
124        let data_dir = self.app_data_dir.clone();
125        tokio::task::spawn_blocking(move || config.save_to_dir(data_dir))
126            .await
127            .map_err(|e| anyhow::anyhow!("Config save task failed: {e}"))??;
128        Ok(())
129    }
130
131    /// Unified config update entrypoint.
132    ///
133    /// Invariants:
134    /// - Update in-memory first
135    /// - Persist to disk
136    /// - Apply runtime side-effects last (provider reload, MCP reconcile)
137    pub async fn update_config<F>(
138        &self,
139        update: F,
140        effects: ConfigUpdateEffects,
141    ) -> Result<Config, AppError>
142    where
143        F: FnOnce(&mut Config) -> Result<(), AppError>,
144    {
145        let snapshot = {
146            let mut cfg = self.config.write().await;
147            update(&mut cfg)?;
148            cfg.publish_env_vars();
149            cfg.clone()
150        };
151
152        self.persist_config_snapshot(snapshot.clone())
153            .await
154            .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to save config: {e}")))?;
155
156        self.apply_config_effects(snapshot.clone(), effects).await?;
157        Ok(snapshot)
158    }
159
160    /// Replace the full config (used for JSON merge endpoints).
161    pub async fn replace_config(
162        &self,
163        new_config: Config,
164        effects: ConfigUpdateEffects,
165    ) -> Result<Config, AppError> {
166        {
167            let mut cfg = self.config.write().await;
168            *cfg = new_config.clone();
169            cfg.publish_env_vars();
170        }
171
172        self.persist_config_snapshot(new_config.clone())
173            .await
174            .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to save config: {e}")))?;
175
176        self.apply_config_effects(new_config.clone(), effects)
177            .await?;
178        Ok(new_config)
179    }
180
181    async fn apply_config_effects(
182        &self,
183        new_config: Config,
184        effects: ConfigUpdateEffects,
185    ) -> Result<(), AppError> {
186        if effects.reload_provider {
187            self.reload_provider().await.map_err(|e| {
188                AppError::InternalError(anyhow::anyhow!(
189                    "Failed to reload provider after updating config: {e}"
190                ))
191            })?;
192        }
193
194        if effects.reconcile_mcp {
195            self.mcp_manager
196                .reconcile_from_config(&new_config.mcp)
197                .await;
198        }
199
200        Ok(())
201    }
202}