1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct BudgetConfig {
12 #[serde(default = "default_total_tokens")]
14 pub total_tokens: usize,
15
16 #[serde(default = "default_output_reserve")]
18 pub output_reserve: usize,
19
20 #[serde(default = "default_warning_threshold")]
22 pub warning_threshold: u8,
23
24 #[serde(default = "default_critical_threshold")]
26 pub critical_threshold: u8,
27}
28
29fn default_total_tokens() -> usize {
30 128_000
31}
32fn default_output_reserve() -> usize {
33 4_096
34}
35fn default_warning_threshold() -> u8 {
36 80
37}
38fn default_critical_threshold() -> u8 {
39 95
40}
41
42impl Default for BudgetConfig {
43 fn default() -> Self {
44 Self {
45 total_tokens: 128_000,
46 output_reserve: 4_096,
47 warning_threshold: 80,
48 critical_threshold: 95,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct CondenserConfigSettings {
56 #[serde(default = "default_condenser_target_tokens")]
58 pub target_tokens: usize,
59
60 #[serde(default = "default_condenser_max_tokens")]
62 pub max_tokens: usize,
63
64 #[serde(default = "default_condenser_max_steps")]
66 pub max_steps: usize,
67
68 #[serde(default = "default_condenser_max_decisions")]
70 pub max_decisions: usize,
71}
72
73fn default_condenser_target_tokens() -> usize {
74 1500
75}
76fn default_condenser_max_tokens() -> usize {
77 2000
78}
79fn default_condenser_max_steps() -> usize {
80 10
81}
82fn default_condenser_max_decisions() -> usize {
83 5
84}
85
86impl Default for CondenserConfigSettings {
87 fn default() -> Self {
88 Self {
89 target_tokens: 1500,
90 max_tokens: 2000,
91 max_steps: 10,
92 max_decisions: 5,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct CalibratorConfigSettings {
100 #[serde(default = "default_calibrator_max_tokens")]
102 pub max_tokens: usize,
103
104 #[serde(default = "default_calibrator_response_reserve")]
106 pub response_reserve: usize,
107
108 #[serde(default = "default_calibrator_max_history_messages")]
110 pub max_history_messages: usize,
111
112 #[serde(default = "default_calibrator_max_rag_chunks")]
114 pub max_rag_chunks: usize,
115}
116
117fn default_calibrator_max_tokens() -> usize {
118 8000
119}
120fn default_calibrator_response_reserve() -> usize {
121 2000
122}
123fn default_calibrator_max_history_messages() -> usize {
124 20
125}
126fn default_calibrator_max_rag_chunks() -> usize {
127 5
128}
129
130impl Default for CalibratorConfigSettings {
131 fn default() -> Self {
132 Self {
133 max_tokens: 8000,
134 response_reserve: 2000,
135 max_history_messages: 20,
136 max_rag_chunks: 5,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ContextConfig {
144 #[serde(default = "default_preset")]
146 pub default_preset: String,
147
148 #[serde(default)]
150 pub budget: BudgetConfig,
151
152 #[serde(default)]
154 pub condenser: CondenserConfigSettings,
155
156 #[serde(default)]
158 pub calibrator: CalibratorConfigSettings,
159}
160
161fn default_preset() -> String {
162 "gpt4_128k".to_string()
163}
164
165impl Default for ContextConfig {
166 fn default() -> Self {
167 Self {
168 default_preset: default_preset(),
169 budget: BudgetConfig::default(),
170 condenser: CondenserConfigSettings::default(),
171 calibrator: CalibratorConfigSettings::default(),
172 }
173 }
174}
175
176pub fn load_default_context_config() -> Result<ContextConfig, ConfigError> {
197 match enact_config::resolve_config_file("context.yaml", "ENACT_CONTEXT_CONFIG_PATH") {
198 Some(path) => load_context_config_from_path(&path),
199 None => {
200 tracing::debug!("No context.yaml found, using hardcoded defaults");
201 Ok(ContextConfig::default())
202 }
203 }
204}
205
206pub fn load_context_config_from_path(path: &PathBuf) -> Result<ContextConfig, ConfigError> {
216 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::Io {
217 path: path.clone(),
218 source: e,
219 })?;
220
221 serde_yaml::from_str(&content).map_err(|e| ConfigError::Parse {
222 path: path.clone(),
223 source: e,
224 })
225}
226
227#[derive(Debug, thiserror::Error)]
229pub enum ConfigError {
230 #[error("Failed to read config file {path}: {source}")]
232 Io {
233 path: PathBuf,
234 #[source]
235 source: std::io::Error,
236 },
237
238 #[error("Failed to parse config file {path}: {source}")]
240 Parse {
241 path: PathBuf,
242 #[source]
243 source: serde_yaml::Error,
244 },
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::io::Write;
251 use tempfile::NamedTempFile;
252
253 #[test]
254 fn test_default_config() {
255 let config = ContextConfig::default();
256 assert_eq!(config.default_preset, "gpt4_128k");
257 assert_eq!(config.budget.total_tokens, 128_000);
258 assert_eq!(config.condenser.target_tokens, 1500);
259 assert_eq!(config.calibrator.max_tokens, 8000);
260 }
261
262 #[test]
263 fn test_load_from_yaml() {
264 let yaml_content = r#"
265default_preset: gpt4_32k
266budget:
267 total_tokens: 32000
268 output_reserve: 2048
269 warning_threshold: 75
270 critical_threshold: 90
271condenser:
272 target_tokens: 1000
273 max_tokens: 1500
274 max_steps: 5
275 max_decisions: 3
276calibrator:
277 max_tokens: 4000
278 response_reserve: 1000
279 max_history_messages: 10
280 max_rag_chunks: 3
281"#;
282
283 let mut temp_file = NamedTempFile::new().unwrap();
284 temp_file.write_all(yaml_content.as_bytes()).unwrap();
285 let path = temp_file.path().to_path_buf();
286
287 let config = load_context_config_from_path(&path).unwrap();
288
289 assert_eq!(config.default_preset, "gpt4_32k");
290 assert_eq!(config.budget.total_tokens, 32000);
291 assert_eq!(config.budget.output_reserve, 2048);
292 assert_eq!(config.budget.warning_threshold, 75);
293 assert_eq!(config.condenser.target_tokens, 1000);
294 assert_eq!(config.condenser.max_steps, 5);
295 assert_eq!(config.calibrator.max_tokens, 4000);
296 assert_eq!(config.calibrator.max_rag_chunks, 3);
297 }
298
299 #[test]
300 fn test_partial_yaml_uses_defaults() {
301 let yaml_content = r#"
302default_preset: custom
303budget:
304 total_tokens: 64000
305"#;
306
307 let mut temp_file = NamedTempFile::new().unwrap();
308 temp_file.write_all(yaml_content.as_bytes()).unwrap();
309 let path = temp_file.path().to_path_buf();
310
311 let config = load_context_config_from_path(&path).unwrap();
312
313 assert_eq!(config.default_preset, "custom");
315 assert_eq!(config.budget.total_tokens, 64000);
316
317 assert_eq!(config.budget.output_reserve, 4096);
319 assert_eq!(config.condenser.target_tokens, 1500);
320 assert_eq!(config.calibrator.max_tokens, 8000);
321 }
322
323 #[test]
324 fn test_load_default_returns_defaults_when_no_file() {
325 std::env::remove_var("ENACT_CONTEXT_CONFIG_PATH");
327
328 let result = load_default_context_config();
330 assert!(result.is_ok());
331 }
332}