use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetConfig {
#[serde(default = "default_total_tokens")]
pub total_tokens: usize,
#[serde(default = "default_output_reserve")]
pub output_reserve: usize,
#[serde(default = "default_warning_threshold")]
pub warning_threshold: u8,
#[serde(default = "default_critical_threshold")]
pub critical_threshold: u8,
}
fn default_total_tokens() -> usize {
128_000
}
fn default_output_reserve() -> usize {
4_096
}
fn default_warning_threshold() -> u8 {
80
}
fn default_critical_threshold() -> u8 {
95
}
impl Default for BudgetConfig {
fn default() -> Self {
Self {
total_tokens: 128_000,
output_reserve: 4_096,
warning_threshold: 80,
critical_threshold: 95,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CondenserConfigSettings {
#[serde(default = "default_condenser_target_tokens")]
pub target_tokens: usize,
#[serde(default = "default_condenser_max_tokens")]
pub max_tokens: usize,
#[serde(default = "default_condenser_max_steps")]
pub max_steps: usize,
#[serde(default = "default_condenser_max_decisions")]
pub max_decisions: usize,
}
fn default_condenser_target_tokens() -> usize {
1500
}
fn default_condenser_max_tokens() -> usize {
2000
}
fn default_condenser_max_steps() -> usize {
10
}
fn default_condenser_max_decisions() -> usize {
5
}
impl Default for CondenserConfigSettings {
fn default() -> Self {
Self {
target_tokens: 1500,
max_tokens: 2000,
max_steps: 10,
max_decisions: 5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalibratorConfigSettings {
#[serde(default = "default_calibrator_max_tokens")]
pub max_tokens: usize,
#[serde(default = "default_calibrator_response_reserve")]
pub response_reserve: usize,
#[serde(default = "default_calibrator_max_history_messages")]
pub max_history_messages: usize,
#[serde(default = "default_calibrator_max_rag_chunks")]
pub max_rag_chunks: usize,
}
fn default_calibrator_max_tokens() -> usize {
8000
}
fn default_calibrator_response_reserve() -> usize {
2000
}
fn default_calibrator_max_history_messages() -> usize {
20
}
fn default_calibrator_max_rag_chunks() -> usize {
5
}
impl Default for CalibratorConfigSettings {
fn default() -> Self {
Self {
max_tokens: 8000,
response_reserve: 2000,
max_history_messages: 20,
max_rag_chunks: 5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextConfig {
#[serde(default = "default_preset")]
pub default_preset: String,
#[serde(default)]
pub budget: BudgetConfig,
#[serde(default)]
pub condenser: CondenserConfigSettings,
#[serde(default)]
pub calibrator: CalibratorConfigSettings,
}
fn default_preset() -> String {
"gpt4_128k".to_string()
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
default_preset: default_preset(),
budget: BudgetConfig::default(),
condenser: CondenserConfigSettings::default(),
calibrator: CalibratorConfigSettings::default(),
}
}
}
pub fn load_default_context_config() -> Result<ContextConfig, ConfigError> {
match enact_config::resolve_config_file("context.yaml", "ENACT_CONTEXT_CONFIG_PATH") {
Some(path) => load_context_config_from_path(&path),
None => {
tracing::debug!("No context.yaml found, using hardcoded defaults");
Ok(ContextConfig::default())
}
}
}
pub fn load_context_config_from_path(path: &PathBuf) -> Result<ContextConfig, ConfigError> {
let content = std::fs::read_to_string(path).map_err(|e| ConfigError::Io {
path: path.clone(),
source: e,
})?;
serde_yaml::from_str(&content).map_err(|e| ConfigError::Parse {
path: path.clone(),
source: e,
})
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to read config file {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to parse config file {path}: {source}")]
Parse {
path: PathBuf,
#[source]
source: serde_yaml::Error,
},
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_default_config() {
let config = ContextConfig::default();
assert_eq!(config.default_preset, "gpt4_128k");
assert_eq!(config.budget.total_tokens, 128_000);
assert_eq!(config.condenser.target_tokens, 1500);
assert_eq!(config.calibrator.max_tokens, 8000);
}
#[test]
fn test_load_from_yaml() {
let yaml_content = r#"
default_preset: gpt4_32k
budget:
total_tokens: 32000
output_reserve: 2048
warning_threshold: 75
critical_threshold: 90
condenser:
target_tokens: 1000
max_tokens: 1500
max_steps: 5
max_decisions: 3
calibrator:
max_tokens: 4000
response_reserve: 1000
max_history_messages: 10
max_rag_chunks: 3
"#;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(yaml_content.as_bytes()).unwrap();
let path = temp_file.path().to_path_buf();
let config = load_context_config_from_path(&path).unwrap();
assert_eq!(config.default_preset, "gpt4_32k");
assert_eq!(config.budget.total_tokens, 32000);
assert_eq!(config.budget.output_reserve, 2048);
assert_eq!(config.budget.warning_threshold, 75);
assert_eq!(config.condenser.target_tokens, 1000);
assert_eq!(config.condenser.max_steps, 5);
assert_eq!(config.calibrator.max_tokens, 4000);
assert_eq!(config.calibrator.max_rag_chunks, 3);
}
#[test]
fn test_partial_yaml_uses_defaults() {
let yaml_content = r#"
default_preset: custom
budget:
total_tokens: 64000
"#;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(yaml_content.as_bytes()).unwrap();
let path = temp_file.path().to_path_buf();
let config = load_context_config_from_path(&path).unwrap();
assert_eq!(config.default_preset, "custom");
assert_eq!(config.budget.total_tokens, 64000);
assert_eq!(config.budget.output_reserve, 4096);
assert_eq!(config.condenser.target_tokens, 1500);
assert_eq!(config.calibrator.max_tokens, 8000);
}
#[test]
fn test_load_default_returns_defaults_when_no_file() {
std::env::remove_var("ENACT_CONTEXT_CONFIG_PATH");
let result = load_default_context_config();
assert!(result.is_ok());
}
}