Skip to content

Commit eb2b829

Browse files
committed
refactor(config): canonicalize mode tool config overrides
- replace persisted mode tool state with added_tools/removed_tools overrides - resolve effective mode tools from current defaults at runtime - expose mode config views via enabled_tools/default_tools for frontend consumers - remove legacy known_tools and available_tools handling from config model - rename tool config sync module to mode config canonicalizer - update desktop config APIs and frontend mode settings consumers
1 parent 2ee517a commit eb2b829

File tree

14 files changed

+551
-313
lines changed

14 files changed

+551
-313
lines changed

src/apps/desktop/src/api/config_api.rs

Lines changed: 36 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -243,109 +243,24 @@ pub async fn get_runtime_logging_info(
243243
}
244244

245245
#[tauri::command]
246-
pub async fn get_mode_configs(state: State<'_, AppState>) -> Result<Value, String> {
247-
use bitfun_core::service::config::types::ModeConfig;
248-
use std::collections::HashMap;
249-
250-
let config_service = &state.config_service;
251-
let mut mode_configs: HashMap<String, ModeConfig> = config_service
252-
.get_config(Some("ai.mode_configs"))
253-
.await
254-
.unwrap_or_default();
255-
256-
let all_modes = state.agent_registry.get_modes_info().await;
257-
let mut needs_save = false;
258-
259-
for mode in all_modes {
260-
let mode_id = mode.id;
261-
let default_tools = mode.default_tools;
262-
263-
if !mode_configs.contains_key(&mode_id) {
264-
let new_config = ModeConfig {
265-
mode_id: mode_id.clone(),
266-
available_tools: default_tools.clone(),
267-
enabled: true,
268-
default_tools: default_tools,
269-
};
270-
mode_configs.insert(mode_id.clone(), new_config);
271-
needs_save = true;
272-
} else if let Some(config) = mode_configs.get_mut(&mode_id) {
273-
config.default_tools = default_tools.clone();
274-
}
275-
}
276-
277-
if needs_save {
278-
match to_json_value(&mode_configs, "mode configs") {
279-
Ok(mode_configs_value) => {
280-
if let Err(e) = config_service
281-
.set_config("ai.mode_configs", mode_configs_value)
282-
.await
283-
{
284-
warn!("Failed to save initialized mode configs: {}", e);
285-
}
286-
}
287-
Err(e) => {
288-
warn!("Failed to serialize initialized mode configs: {}", e);
289-
}
290-
}
291-
}
246+
pub async fn get_mode_configs(_state: State<'_, AppState>) -> Result<Value, String> {
247+
let mode_configs =
248+
bitfun_core::service::config::mode_config_canonicalizer::get_mode_config_views()
249+
.await
250+
.map_err(|e| format!("Failed to get mode configs: {}", e))?;
292251

293252
Ok(to_json_value(mode_configs, "mode configs")?)
294253
}
295254

296255
#[tauri::command]
297-
pub async fn get_mode_config(state: State<'_, AppState>, mode_id: String) -> Result<Value, String> {
298-
use bitfun_core::service::config::types::ModeConfig;
299-
300-
let config_service = &state.config_service;
301-
let agent_registry = &state.agent_registry;
302-
let path = format!("ai.mode_configs.{}", mode_id);
303-
let config_result = config_service.get_config::<ModeConfig>(Some(&path)).await;
304-
305-
let config = match config_result {
306-
Ok(existing_config) => {
307-
let mut cfg = existing_config;
308-
if let Some(mode) = agent_registry.get_mode_agent(&mode_id) {
309-
cfg.default_tools = mode.default_tools();
310-
}
311-
cfg
312-
}
313-
Err(_) => {
314-
if let Some(mode) = agent_registry.get_mode_agent(&mode_id) {
315-
let default_tools = mode.default_tools();
316-
let new_config = ModeConfig {
317-
mode_id: mode_id.clone(),
318-
available_tools: default_tools.clone(),
319-
enabled: true,
320-
default_tools: default_tools,
321-
};
322-
match to_json_value(&new_config, "initial mode config") {
323-
Ok(new_config_value) => {
324-
if let Err(e) = config_service.set_config(&path, new_config_value).await {
325-
warn!(
326-
"Failed to save initial mode config: mode_id={}, error={}",
327-
mode_id, e
328-
);
329-
}
330-
}
331-
Err(e) => {
332-
warn!(
333-
"Failed to serialize initial mode config: mode_id={}, error={}",
334-
mode_id, e
335-
);
336-
}
337-
}
338-
new_config
339-
} else {
340-
ModeConfig {
341-
mode_id: mode_id.clone(),
342-
available_tools: vec![],
343-
enabled: true,
344-
default_tools: vec![],
345-
}
346-
}
347-
}
348-
};
256+
pub async fn get_mode_config(
257+
_state: State<'_, AppState>,
258+
mode_id: String,
259+
) -> Result<Value, String> {
260+
let config =
261+
bitfun_core::service::config::mode_config_canonicalizer::get_mode_config_view(&mode_id)
262+
.await
263+
.map_err(|e| format!("Failed to get mode config: {}", e))?;
349264

350265
Ok(to_json_value(config, "mode config")?)
351266
}
@@ -356,10 +271,13 @@ pub async fn set_mode_config(
356271
mode_id: String,
357272
config: Value,
358273
) -> Result<String, String> {
359-
let config_service = &state.config_service;
360-
let path = format!("ai.mode_configs.{}", mode_id);
274+
let _ = state;
361275

362-
match config_service.set_config(&path, config).await {
276+
match bitfun_core::service::config::mode_config_canonicalizer::persist_mode_config_from_value(
277+
&mode_id, config,
278+
)
279+
.await
280+
{
363281
Ok(_) => {
364282
if let Err(e) = bitfun_core::service::config::reload_global_config().await {
365283
warn!(
@@ -387,30 +305,14 @@ pub async fn set_mode_config(
387305

388306
#[tauri::command]
389307
pub async fn reset_mode_config(
390-
state: State<'_, AppState>,
308+
_state: State<'_, AppState>,
391309
mode_id: String,
392310
) -> Result<String, String> {
393-
use bitfun_core::service::config::types::ModeConfig;
394-
395-
let agent_registry = &state.agent_registry;
396-
let default_tools = if let Some(mode) = agent_registry.get_mode_agent(&mode_id) {
397-
mode.default_tools()
398-
} else {
399-
return Err(format!("Mode does not exist: {}", mode_id));
400-
};
401-
402-
let default_config = ModeConfig {
403-
mode_id: mode_id.clone(),
404-
available_tools: default_tools.clone(),
405-
enabled: true,
406-
default_tools: default_tools,
407-
};
408-
409-
let config_service = &state.config_service;
410-
let path = format!("ai.mode_configs.{}", mode_id);
411-
let default_config_value = to_json_value(&default_config, "default mode config")?;
412-
413-
match config_service.set_config(&path, default_config_value).await {
311+
match bitfun_core::service::config::mode_config_canonicalizer::reset_mode_config_to_default(
312+
&mode_id,
313+
)
314+
.await
315+
{
414316
Ok(_) => {
415317
if let Err(e) = bitfun_core::service::config::reload_global_config().await {
416318
warn!(
@@ -521,20 +423,23 @@ pub async fn set_subagent_config(
521423
}
522424

523425
#[tauri::command]
524-
pub async fn sync_tool_configs(_state: State<'_, AppState>) -> Result<Value, String> {
525-
match bitfun_core::service::config::tool_config_sync::sync_tool_configs().await {
426+
pub async fn canonicalize_mode_configs(_state: State<'_, AppState>) -> Result<Value, String> {
427+
match bitfun_core::service::config::mode_config_canonicalizer::canonicalize_mode_configs().await
428+
{
526429
Ok(report) => {
527430
info!(
528-
"Tool configs synced: new_tools={}, deleted_tools={}, updated_modes={}",
529-
report.new_tools.len(),
530-
report.deleted_tools.len(),
431+
"Mode configs canonicalized: removed_modes={}, updated_modes={}",
432+
report.removed_mode_configs.len(),
531433
report.updated_modes.len()
532434
);
533-
Ok(to_json_value(report, "tool config sync report")?)
435+
Ok(to_json_value(
436+
report,
437+
"mode config canonicalization report",
438+
)?)
534439
}
535440
Err(e) => {
536-
error!("Failed to sync tool configs: {}", e);
537-
Err(format!("Failed to sync tool configs: {}", e))
441+
error!("Failed to canonicalize mode configs: {}", e);
442+
Err(format!("Failed to canonicalize mode configs: {}", e))
538443
}
539444
}
540445
}

src/apps/desktop/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ pub async fn run() {
600600
create_cron_job,
601601
update_cron_job,
602602
delete_cron_job,
603-
api::config_api::sync_tool_configs,
603+
api::config_api::canonicalize_mode_configs,
604604
api::terminal_api::terminal_get_shells,
605605
api::terminal_api::terminal_create,
606606
api::terminal_api::terminal_get,

src/crates/core/src/agentic/agents/registry.rs

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ use crate::agentic::agents::custom_subagents::{
77
};
88
use crate::agentic::tools::get_all_registered_tool_names;
99
use crate::service::config::global::GlobalConfigManager;
10+
use crate::service::config::mode_config_canonicalizer::resolve_effective_tools;
1011
use crate::service::config::types::{ModeConfig, SubAgentConfig};
1112
use crate::service::config::GlobalConfig;
1213
use crate::util::errors::{BitFunError, BitFunResult};
1314
use log::{debug, error, warn};
1415
use serde::{Deserialize, Serialize};
15-
use std::collections::HashMap;
16+
use std::collections::{HashMap, HashSet};
1617
use std::path::{Path, PathBuf};
1718
use std::sync::RwLock;
1819
use std::sync::{Arc, OnceLock};
@@ -372,7 +373,7 @@ impl AgentRegistry {
372373

373374
/// get agent tools from config
374375
/// if not set, return default tools
375-
/// tool configuration synchronization is implemented through tool_config_sync, here only read configuration
376+
/// mode config canonicalization is handled separately; this only reads resolved configuration
376377
pub async fn get_agent_tools(
377378
&self,
378379
agent_type: &str,
@@ -385,10 +386,13 @@ impl AgentRegistry {
385386
match entry.category {
386387
AgentCategory::Mode => {
387388
let mode_configs = get_mode_configs().await;
388-
mode_configs
389-
.get(agent_type)
390-
.map(|config| config.available_tools.clone())
391-
.unwrap_or_else(|| entry.agent.default_tools())
389+
let valid_tools: HashSet<String> =
390+
get_all_registered_tool_names().await.into_iter().collect();
391+
resolve_effective_tools(
392+
&entry.agent.default_tools(),
393+
mode_configs.get(agent_type),
394+
&valid_tools,
395+
)
392396
}
393397
AgentCategory::SubAgent | AgentCategory::Hidden => entry.agent.default_tools(),
394398
}
@@ -708,9 +712,9 @@ impl AgentRegistry {
708712
agent_id: &str,
709713
workspace_root: Option<&Path>,
710714
) -> BitFunResult<CustomSubagentDetail> {
711-
let entry = self.find_agent_entry(agent_id, workspace_root).ok_or_else(|| {
712-
BitFunError::agent(format!("Subagent not found: {}", agent_id))
713-
})?;
715+
let entry = self
716+
.find_agent_entry(agent_id, workspace_root)
717+
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
714718
if entry.category != AgentCategory::SubAgent {
715719
return Err(BitFunError::agent(format!(
716720
"Agent '{}' is not a subagent",
@@ -767,9 +771,9 @@ impl AgentRegistry {
767771
if let Some(root) = workspace_root {
768772
self.load_custom_subagents(root).await;
769773
}
770-
let entry = self.find_agent_entry(agent_id, workspace_root).ok_or_else(|| {
771-
BitFunError::agent(format!("Subagent not found: {}", agent_id))
772-
})?;
774+
let entry = self
775+
.find_agent_entry(agent_id, workspace_root)
776+
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
773777
if entry.category != AgentCategory::SubAgent {
774778
return Err(BitFunError::agent(format!(
775779
"Agent '{}' is not a subagent",
@@ -791,16 +795,14 @@ impl AgentRegistry {
791795
agent_id
792796
))
793797
})?;
794-
let tools = tools
795-
.filter(|t| !t.is_empty())
796-
.unwrap_or_else(|| {
797-
vec![
798-
"LS".to_string(),
799-
"Read".to_string(),
800-
"Glob".to_string(),
801-
"Grep".to_string(),
802-
]
803-
});
798+
let tools = tools.filter(|t| !t.is_empty()).unwrap_or_else(|| {
799+
vec![
800+
"LS".to_string(),
801+
"Read".to_string(),
802+
"Glob".to_string(),
803+
"Grep".to_string(),
804+
]
805+
});
804806
let mut new_subagent = CustomSubagent::new(
805807
old.name.clone(),
806808
description,
@@ -830,9 +832,9 @@ impl AgentRegistry {
830832
) -> BitFunResult<()> {
831833
let mut map = self.write_agents();
832834
if map.contains_key(agent_id) {
833-
let old_entry = map.get(agent_id).ok_or_else(|| {
834-
BitFunError::agent(format!("Subagent not found: {}", agent_id))
835-
})?;
835+
let old_entry = map
836+
.get(agent_id)
837+
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
836838
if old_entry.category != AgentCategory::SubAgent {
837839
return Err(BitFunError::agent(format!(
838840
"Agent '{}' is not a subagent",
@@ -869,9 +871,9 @@ impl AgentRegistry {
869871
let entries = pm.get_mut(root).ok_or_else(|| {
870872
BitFunError::agent("Project subagent cache not loaded for this workspace".to_string())
871873
})?;
872-
let old_entry = entries.get(agent_id).ok_or_else(|| {
873-
BitFunError::agent(format!("Subagent not found: {}", agent_id))
874-
})?;
874+
let old_entry = entries
875+
.get(agent_id)
876+
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
875877
if old_entry.category != AgentCategory::SubAgent {
876878
return Err(BitFunError::agent(format!(
877879
"Agent '{}' is not a subagent",

src/crates/core/src/service/config/global.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,18 @@ impl GlobalConfigManager {
8080

8181
info!("Global config service initialized");
8282

83-
match super::tool_config_sync::sync_tool_configs().await {
83+
match super::mode_config_canonicalizer::canonicalize_mode_configs().await {
8484
Ok(report) => {
85-
if !report.new_tools.is_empty() || !report.deleted_tools.is_empty() {
85+
if !report.removed_mode_configs.is_empty() || !report.updated_modes.is_empty() {
8686
info!(
87-
"Tool config sync completed: {} new, {} deleted, {} updated modes",
88-
report.new_tools.len(),
89-
report.deleted_tools.len(),
87+
"Mode config canonicalization completed: removed_modes={}, updated_modes={}",
88+
report.removed_mode_configs.len(),
9089
report.updated_modes.len()
9190
);
9291
}
9392
}
9493
Err(e) => {
95-
warn!("Tool config sync failed: {}", e);
94+
warn!("Mode config canonicalization failed: {}", e);
9695
}
9796
}
9897

@@ -136,6 +135,12 @@ impl GlobalConfigManager {
136135
pub async fn reload() -> BitFunResult<()> {
137136
let service = Self::get_service().await?;
138137
service.reload().await?;
138+
if let Err(error) = super::mode_config_canonicalizer::canonicalize_mode_configs().await {
139+
warn!(
140+
"Mode config canonicalization failed after reload: {}",
141+
error
142+
);
143+
}
139144
Self::broadcast_update(ConfigUpdateEvent::ConfigReloaded).await;
140145
Ok(())
141146
}

src/crates/core/src/service/config/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ pub mod app_language;
66
pub mod factory;
77
pub mod global;
88
pub mod manager;
9+
pub mod mode_config_canonicalizer;
910
pub mod providers;
1011
pub mod service;
11-
pub mod tool_config_sync;
1212
pub mod types;
1313

1414
pub use app_language::{get_app_language_code, short_model_user_language_instruction};
@@ -18,7 +18,9 @@ pub use global::{
1818
subscribe_config_updates, ConfigUpdateEvent, GlobalConfigManager,
1919
};
2020
pub use manager::{ConfigManager, ConfigManagerSettings, ConfigStatistics};
21+
pub use mode_config_canonicalizer::{
22+
canonicalize_mode_configs, ModeConfigCanonicalizationReport, ModeConfigUpdateInfo,
23+
};
2124
pub use providers::ConfigProviderRegistry;
2225
pub use service::{ConfigExport, ConfigHealthStatus, ConfigImportResult, ConfigService};
23-
pub use tool_config_sync::{sync_tool_configs, ModeSyncInfo, SyncReport};
2426
pub use types::*;

0 commit comments

Comments
 (0)