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