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 agents;
pub mod env_source;
pub mod hooks;
pub mod layers;
pub mod loading;
pub mod mcp;
pub mod migrations;
pub mod providers;
pub mod roles;
pub mod validation;
pub mod registry;
pub mod pipelines;
pub mod workflows;
pub use hooks::*;
pub use layers::*;
pub use mcp::*;
pub use pipelines::*;
pub use providers::*;
pub use registry::*;
pub use roles::*;
pub use workflows::*;
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)
}
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::None => "off",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PressureLevel {
pub threshold: usize,
pub target_ratio: f64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CompressionDecisionConfig {
pub model: String,
pub max_tokens: u32,
pub temperature: f32,
pub top_p: f32,
pub top_k: u32,
pub max_retries: u32,
pub retry_timeout: u64,
pub ignore_cost: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CompressionHintConfig {
pub hints_enabled: bool,
pub hints_pressure_threshold: f64,
pub hints_min_interval: usize,
pub pressure_levels: Vec<PressureLevel>,
pub decision: CompressionDecisionConfig,
#[serde(default = "default_knowledge_retention")]
pub knowledge_retention: usize,
}
fn default_knowledge_retention() -> usize {
10
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SkillsConfig {
pub auto_activation: bool,
pub auto_validation: bool,
pub activation_timeout: u64,
pub validation_timeout: u64,
pub max_retries: 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 default: 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,
pub request_timeout_seconds: u32,
#[serde(default)]
pub agents: Vec<crate::config::agents::AgentConfig>,
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>>,
#[serde(default)]
pub workflows: Vec<WorkflowDefinition>,
#[serde(default)]
pub pipelines: Vec<PipelineDefinition>,
pub prompts: Vec<PromptConfig>,
pub compression: CompressionHintConfig,
pub learning: crate::learning::LearningConfig,
pub system: Option<String>,
#[serde(skip)]
pub runtime_output_mode: Option<String>,
#[serde(skip)]
pub working_directory: Option<PathBuf>,
pub sandbox: bool,
#[serde(default)]
pub capabilities: HashMap<String, String>,
#[serde(default)]
pub taps: HashMap<String, String>,
pub skills: SkillsConfig,
#[serde(default)]
pub hooks: Vec<HookConfig>,
#[serde(default)]
pub registry: crate::config::registry::RegistryConfig,
#[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,
auto_bind,
..
} => McpServerConfig::Builtin {
name,
timeout_seconds,
tools,
auto_bind,
},
McpServerConfig::Http {
name: _,
url,
timeout_seconds,
tools,
auto_bind,
} => McpServerConfig::Http {
name,
url,
timeout_seconds,
tools,
auto_bind,
},
McpServerConfig::Stdin {
command,
args,
timeout_seconds,
tools,
auto_bind,
..
} => McpServerConfig::Stdin {
name,
command,
args,
timeout_seconds,
tools,
auto_bind,
},
}
})
.collect();
Self {
servers: servers_vec,
allowed_tools: allowed_tools.unwrap_or_default(),
}
}
}
impl Config {
pub fn get_hook_by_name(&self, name: &str) -> Option<&HookConfig> {
self.hooks.iter().find(|h| h.name == name)
}
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_servers_for_role(
&self,
role_mcp_config: &RoleMcpConfig,
role_name: Option<&str>,
) -> Vec<McpServerConfig> {
role_mcp_config.get_enabled_servers(&self.mcp.servers, role_name)
}
pub fn get_log_level(&self) -> LogLevel {
self.log_level.clone()
}
pub fn output_mode(&self) -> crate::session::output::OutputMode {
crate::session::output::OutputMode::from_runtime_mode(
self.runtime_output_mode.as_deref().unwrap_or("plain"),
)
}
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 has_role(&self, role: &str) -> bool {
self.role_map.contains_key(role)
}
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, Some(mode));
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_working_directory(&self) -> PathBuf {
self.working_directory
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
}
pub fn set_working_directory(&mut self, path: PathBuf) {
self.working_directory = Some(path);
}
pub fn get_role_config_struct(&self, role: &str) -> &RoleConfig {
let (role_config, _, _, _, _) = self.get_role_config(role);
role_config
}
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) };
}
static CURRENT_ROLE: std::sync::RwLock<Option<String>> = std::sync::RwLock::new(None);
pub fn set_thread_config(config: &Config) {
if let Some(session_id) = crate::session::context::current_session_id() {
crate::session::context::set_session_config(&session_id, config);
return;
}
CURRENT_CONFIG.with(|c| {
*c.borrow_mut() = Some(config.clone());
});
}
pub fn set_thread_role(role: &str) {
if let Some(session_id) = crate::session::context::current_session_id() {
crate::session::context::set_session_role(&session_id, role);
return;
}
*CURRENT_ROLE.write().unwrap() = Some(role.to_string());
}
pub fn get_thread_role() -> Option<String> {
if let Some(session_id) = crate::session::context::current_session_id() {
return crate::session::context::get_session_role(&session_id);
}
CURRENT_ROLE.read().unwrap().clone()
}
pub fn with_thread_config<F, R>(f: F) -> Option<R>
where
F: FnOnce(&Config) -> R,
{
if let Some(session_id) = crate::session::context::current_session_id() {
return crate::session::context::get_session_config(&session_id)
.as_ref()
.map(f);
}
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 {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::info!("{}", $fmt);
} else if $crate::config::with_thread_config(|config| {
!config.output_mode().should_suppress_cli_output()
}).unwrap_or(true) {
use colored::Colorize;
$crate::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 {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::info!($fmt, $($arg),*);
} else if $crate::config::with_thread_config(|config| {
!config.output_mode().should_suppress_cli_output()
}).unwrap_or(true) {
use colored::Colorize;
$crate::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 {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::debug!("{}", $fmt);
} else if $crate::config::with_thread_config(|config| {
!config.output_mode().should_suppress_cli_output()
}).unwrap_or(true) {
use colored::Colorize;
$crate::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 {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::debug!($fmt, $($arg),*);
} else if $crate::config::with_thread_config(|config| {
!config.output_mode().should_suppress_cli_output()
}).unwrap_or(true) {
use colored::Colorize;
$crate::println!("{}", format!($fmt, $($arg),*).bright_blue());
}
}
}
};
}
#[macro_export]
macro_rules! log_error {
($fmt:expr) => {{
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::error!("{}", $fmt);
if $crate::logging::tracing_setup::is_structured_output_mode() {
if let Some(sink) = $crate::logging::AcpErrorSink::get_global() {
let _ = sink.log_error_simple($fmt);
}
}
} else {
use colored::Colorize;
$crate::eprintln!("{}", $fmt.bright_red());
}
}};
($fmt:expr, $($arg:expr),*) => {{
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::error!($fmt, $($arg),*);
if $crate::logging::tracing_setup::is_structured_output_mode() {
if let Some(sink) = $crate::logging::AcpErrorSink::get_global() {
let _ = sink.log_error_simple(&format!($fmt, $($arg),*));
}
}
} else {
use colored::Colorize;
$crate::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 => {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::debug!("{}", $debug_msg);
} else {
$crate::println!("{}", $debug_msg);
}
}
$crate::config::LogLevel::Info => {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::info!("{}", $info_msg);
} else {
$crate::println!("{}", $info_msg);
}
}
$crate::config::LogLevel::None => {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::info!("{}", $none_msg);
} else {
$crate::println!("{}", $none_msg);
}
}
}
} else {
$crate::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 {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::debug!("{}", $debug_msg);
} else {
$crate::println!("{}", $debug_msg);
}
} else {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::info!("{}", $default_msg);
} else {
$crate::println!("{}", $default_msg);
}
}
} else {
$crate::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 {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::info!("{}", $info_msg);
} else {
$crate::println!("{}", $info_msg);
}
} else {
if $crate::logging::tracing_setup::is_tracing_initialized() {
tracing::info!("{}", $default_msg);
} else {
$crate::println!("{}", $default_msg);
}
}
} else {
$crate::println!("{}", $default_msg);
}
};
}