use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
static ENV_TRACKER: OnceLock<Mutex<env_source::EnvTracker>> = OnceLock::new();
pub fn get_env_tracker() -> &'static Mutex<env_source::EnvTracker> {
ENV_TRACKER.get_or_init(|| Mutex::new(env_source::EnvTracker::new()))
}
pub mod env_source;
pub mod layers;
pub mod loading;
pub mod mcp;
pub mod migrations;
pub mod providers;
pub mod roles;
pub mod validation;
pub use layers::*;
pub use mcp::*;
pub use providers::*;
pub use roles::*;
pub const CURRENT_CONFIG_VERSION: u32 = 1;
type RoleConfigResult<'a> = (
&'a RoleConfig,
&'a RoleMcpConfig,
Option<&'a Vec<crate::session::layers::LayerConfig>>,
Option<&'a Vec<crate::session::layers::LayerConfig>>,
&'a String,
);
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum LogLevel {
#[serde(rename = "none")]
None,
#[serde(rename = "info")]
Info,
#[serde(rename = "debug")]
Debug,
}
impl LogLevel {
pub fn is_info_enabled(&self) -> bool {
matches!(self, LogLevel::Info | LogLevel::Debug)
}
pub fn is_debug_enabled(&self) -> bool {
matches!(self, LogLevel::Debug)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AskConfig {
pub system: String,
pub temperature: f32,
pub top_p: f32,
pub top_k: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ShellConfig {
pub system: String,
pub temperature: f32,
pub top_p: f32,
pub top_k: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PromptConfig {
pub name: String,
pub prompt: String,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub version: u32,
pub log_level: LogLevel,
pub model: String,
pub max_tokens: u32,
pub custom_instructions_file_name: String,
pub custom_constraints_file_name: String,
pub mcp_response_warning_threshold: usize,
pub mcp_response_tokens_threshold: usize,
pub max_session_tokens_threshold: usize,
pub cache_tokens_threshold: u64,
pub cache_timeout_seconds: u64,
pub enable_markdown_rendering: bool,
pub markdown_theme: String,
pub max_session_spending_threshold: f64,
pub max_request_spending_threshold: f64,
pub use_long_system_cache: bool,
pub max_retries: u32,
pub retry_timeout: u32,
#[serde(default)]
pub agents: Vec<crate::session::layers::LayerConfig>,
pub roles: Vec<crate::config::roles::Role>,
#[serde(skip)]
pub role_map: HashMap<String, crate::config::roles::Role>,
#[serde(skip_serializing_if = "McpConfig::is_default_for_serialization")]
pub mcp: McpConfig,
pub commands: Option<Vec<crate::session::layers::LayerConfig>>,
pub layers: Option<Vec<crate::session::layers::LayerConfig>>,
pub ask: AskConfig,
pub shell: ShellConfig,
pub prompts: Vec<PromptConfig>,
pub system: Option<String>,
#[serde(skip)]
config_path: Option<PathBuf>,
}
impl McpConfig {
pub fn is_default_for_serialization(&self) -> bool {
self.servers.is_empty() && self.allowed_tools.is_empty()
}
pub fn get_all_servers(&self) -> Vec<McpServerConfig> {
let mut result = Vec::new();
for server_config in &self.servers {
let server = server_config.clone();
result.push(server);
}
result
}
pub fn with_servers(
servers: std::collections::HashMap<String, McpServerConfig>,
allowed_tools: Option<Vec<String>>,
) -> Self {
let servers_vec: Vec<McpServerConfig> = servers
.into_iter()
.map(|(name, server)| {
match server {
McpServerConfig::Builtin {
timeout_seconds,
tools,
..
} => McpServerConfig::Builtin {
name,
timeout_seconds,
tools,
},
McpServerConfig::Http {
connection,
timeout_seconds,
tools,
..
} => McpServerConfig::Http {
name,
connection,
timeout_seconds,
tools,
},
McpServerConfig::Stdin {
command,
args,
timeout_seconds,
tools,
..
} => McpServerConfig::Stdin {
name,
command,
args,
timeout_seconds,
tools,
},
}
})
.collect();
Self {
servers: servers_vec,
allowed_tools: allowed_tools.unwrap_or_default(),
}
}
}
impl Config {
pub fn get_effective_model(&self) -> String {
self.model.clone()
}
pub fn get_effective_max_tokens(&self) -> u32 {
self.max_tokens
}
pub fn get_server_config(&self, server_name: &str) -> Option<McpServerConfig> {
self.mcp
.servers
.iter()
.find(|s| s.name() == server_name)
.cloned()
}
pub fn get_enabled_layers_for_role(
&self,
role: &str,
) -> Vec<crate::session::layers::LayerConfig> {
self.get_enabled_layers(role)
}
pub fn get_enabled_servers_for_role(
&self,
role_mcp_config: &RoleMcpConfig,
) -> Vec<McpServerConfig> {
role_mcp_config.get_enabled_servers(&self.mcp.servers)
}
pub fn get_log_level(&self) -> LogLevel {
self.log_level.clone()
}
pub fn get_enable_layers(&self, role: &str) -> bool {
let (role_config, _, _, _, _) = self.get_role_config(role);
role_config.enable_layers
}
pub fn get_model(&self, _role: &str) -> String {
self.get_effective_model()
}
pub fn get_max_tokens(&self, _role: &str) -> u32 {
self.get_effective_max_tokens()
}
pub fn get_role_config(&self, role: &str) -> RoleConfigResult<'_> {
if let Some(role_config) = self.role_map.get(role) {
(
&role_config.config,
&role_config.mcp,
self.layers.as_ref(),
self.commands.as_ref(),
&role_config.config.system,
)
} else {
panic!("CRITICAL CONFIG ERROR: Role '{role}' not found in config. All roles must be explicitly defined in config template.");
}
}
pub fn get_merged_config_for_role(&self, mode: &str) -> Config {
let (_role_config, role_mcp_config, _role_layers_config, commands, system_prompt) =
self.get_role_config(mode);
let mut merged = self.clone();
let enabled_servers = self.get_enabled_servers_for_role(role_mcp_config);
crate::log_debug!(
"TRACE: Role '{}' server_refs: {:?}",
mode,
role_mcp_config.server_refs
);
crate::log_debug!(
"TRACE: Found {} enabled servers for role",
enabled_servers.len()
);
for server in &enabled_servers {
crate::log_debug!("TRACE: Adding server '{}' to merged config", server.name());
}
merged.mcp = McpConfig {
servers: enabled_servers, allowed_tools: role_mcp_config.allowed_tools.clone(),
};
merged.commands = commands.cloned();
merged.system = Some(system_prompt.clone());
merged
}
pub fn get_role_config_struct(&self, role: &str) -> &RoleConfig {
let (role_config, _, _, _, _) = self.get_role_config(role);
role_config
}
pub fn get_layer_refs(&self, role: &str) -> &Vec<String> {
if let Some(role_config) = self.role_map.get(role) {
&role_config.layer_refs
} else {
static EMPTY_VEC: Vec<String> = Vec::new();
&EMPTY_VEC
}
}
pub fn get_enabled_layers(&self, role: &str) -> Vec<crate::session::layers::LayerConfig> {
let layer_refs = self.get_layer_refs(role);
if layer_refs.is_empty() {
return Vec::new();
}
let all_layers = if let Some(layers) = &self.layers {
layers
} else {
panic!("CRITICAL CONFIG ERROR: No layers defined in config but role '{role}' references layers: {layer_refs:?}. All layers must be explicitly defined in config.");
};
let mut result = Vec::new();
for layer_name in layer_refs {
let layer_config = all_layers
.iter()
.find(|layer| layer.name == *layer_name)
.cloned();
if let Some(mut layer) = layer_config {
layer.name = layer_name.clone();
result.push(layer);
} else {
panic!("CRITICAL CONFIG ERROR: Layer '{layer_name}' referenced by role '{role}' but not found in config layers registry. All referenced layers must be explicitly defined in config.");
}
}
result
}
pub fn build_role_map(&mut self) {
self.role_map.clear();
for role in &self.roles {
self.role_map.insert(role.name.clone(), role.clone());
}
}
}
thread_local! {
static CURRENT_CONFIG: RefCell<Option<Config>> = const { RefCell::new(None) };
}
pub fn set_thread_config(config: &Config) {
CURRENT_CONFIG.with(|c| {
*c.borrow_mut() = Some(config.clone());
});
}
pub fn with_thread_config<F, R>(f: F) -> Option<R>
where
F: FnOnce(&Config) -> R,
{
CURRENT_CONFIG.with(|c| (*c.borrow()).as_ref().map(f))
}
#[macro_export]
macro_rules! log_info {
($fmt:expr) => {
if let Some(should_log) = $crate::config::with_thread_config(|config| config.get_log_level().is_info_enabled()) {
if should_log {
use colored::Colorize;
println!("{}", $fmt.cyan());
}
}
};
($fmt:expr, $($arg:expr),*) => {
if let Some(should_log) = $crate::config::with_thread_config(|config| config.get_log_level().is_info_enabled()) {
if should_log {
use colored::Colorize;
println!("{}", format!($fmt, $($arg),*).cyan());
}
}
};
}
#[macro_export]
macro_rules! log_debug {
($fmt:expr) => {
if let Some(should_log) = $crate::config::with_thread_config(|config| config.get_log_level().is_debug_enabled()) {
if should_log {
use colored::Colorize;
println!("{}", $fmt.bright_blue());
}
}
};
($fmt:expr, $($arg:expr),*) => {
if let Some(should_log) = $crate::config::with_thread_config(|config| config.get_log_level().is_debug_enabled()) {
if should_log {
use colored::Colorize;
println!("{}", format!($fmt, $($arg),*).bright_blue());
}
}
};
}
#[macro_export]
macro_rules! log_error {
($fmt:expr) => {{
use colored::Colorize;
eprintln!("{}", $fmt.bright_red());
}};
($fmt:expr, $($arg:expr),*) => {{
use colored::Colorize;
eprintln!("{}", format!($fmt, $($arg),*).bright_red());
}};
}
#[macro_export]
macro_rules! log_conditional {
(debug: $debug_msg:expr, info: $info_msg:expr, none: $none_msg:expr) => {
if let Some(level) = $crate::config::with_thread_config(|config| config.get_log_level()) {
match level {
$crate::config::LogLevel::Debug => println!("{}", $debug_msg),
$crate::config::LogLevel::Info => println!("{}", $info_msg),
$crate::config::LogLevel::None => println!("{}", $none_msg),
}
} else {
println!("{}", $none_msg);
}
};
(debug: $debug_msg:expr, default: $default_msg:expr) => {
if let Some(should_debug) =
$crate::config::with_thread_config(|config| config.get_log_level().is_debug_enabled())
{
if should_debug {
println!("{}", $debug_msg);
} else {
println!("{}", $default_msg);
}
} else {
println!("{}", $default_msg);
}
};
(info: $info_msg:expr, default: $default_msg:expr) => {
if let Some(should_info) =
$crate::config::with_thread_config(|config| config.get_log_level().is_info_enabled())
{
if should_info {
println!("{}", $info_msg);
} else {
println!("{}", $default_msg);
}
} else {
println!("{}", $default_msg);
}
};
}