1use serde::{Deserialize, Serialize};
5
6fn default_min_failures() -> u32 {
7 3
8}
9
10fn default_improve_threshold() -> f64 {
11 0.7
12}
13
14fn default_rollback_threshold() -> f64 {
15 0.5
16}
17
18fn default_min_evaluations() -> u32 {
19 5
20}
21
22fn default_max_versions() -> u32 {
23 10
24}
25
26fn default_cooldown_minutes() -> u64 {
27 60
28}
29
30fn default_correction_detection() -> bool {
31 true
32}
33
34fn default_correction_confidence_threshold() -> f32 {
35 0.6
36}
37
38fn default_judge_adaptive_low() -> f32 {
39 0.5
40}
41
42fn default_judge_adaptive_high() -> f32 {
43 0.8
44}
45
46fn default_correction_recall_limit() -> u32 {
47 3
48}
49
50fn default_correction_min_similarity() -> f32 {
51 0.75
52}
53
54fn default_auto_promote_min_uses() -> u32 {
55 50
56}
57
58fn default_auto_promote_threshold() -> f64 {
59 0.95
60}
61
62fn default_auto_demote_min_uses() -> u32 {
63 30
64}
65
66fn default_auto_demote_threshold() -> f64 {
67 0.40
68}
69
70fn default_min_sessions_before_promote() -> u32 {
71 2
72}
73
74fn default_min_sessions_before_demote() -> u32 {
75 1
76}
77
78fn default_max_auto_sections() -> u32 {
79 3
80}
81
82fn default_arise_min_tool_calls() -> u32 {
83 2
84}
85
86fn default_stem_min_occurrences() -> u32 {
87 3
88}
89
90fn default_stem_min_success_rate() -> f64 {
91 0.8
92}
93
94fn default_stem_retention_days() -> u32 {
95 90
96}
97
98fn default_stem_pattern_window_days() -> u32 {
99 30
100}
101
102fn default_erl_max_heuristics_per_skill() -> u32 {
103 3
104}
105
106fn default_erl_dedup_threshold() -> f32 {
107 0.9
108}
109
110fn default_erl_min_confidence() -> f64 {
111 0.5
112}
113
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
116#[serde(rename_all = "lowercase")]
117pub enum DetectorMode {
118 #[default]
120 Regex,
121 Judge,
129 Model,
137}
138
139#[allow(clippy::struct_excessive_bools)]
140#[derive(Debug, Clone, Deserialize, Serialize)]
141pub struct LearningConfig {
142 #[serde(default)]
143 pub enabled: bool,
144 #[serde(default)]
145 pub auto_activate: bool,
146 #[serde(default = "default_min_failures")]
147 pub min_failures: u32,
148 #[serde(default = "default_improve_threshold")]
149 pub improve_threshold: f64,
150 #[serde(default = "default_rollback_threshold")]
151 pub rollback_threshold: f64,
152 #[serde(default = "default_min_evaluations")]
153 pub min_evaluations: u32,
154 #[serde(default = "default_max_versions")]
155 pub max_versions: u32,
156 #[serde(default = "default_cooldown_minutes")]
157 pub cooldown_minutes: u64,
158 #[serde(default = "default_correction_detection")]
159 pub correction_detection: bool,
160 #[serde(default = "default_correction_confidence_threshold")]
161 pub correction_confidence_threshold: f32,
162 #[serde(default)]
164 pub detector_mode: DetectorMode,
165 #[serde(default)]
167 pub judge_model: String,
168 #[serde(default)]
173 pub feedback_provider: String,
174 #[serde(default = "default_judge_adaptive_low")]
176 pub judge_adaptive_low: f32,
177 #[serde(default = "default_judge_adaptive_high")]
179 pub judge_adaptive_high: f32,
180 #[serde(default = "default_correction_recall_limit")]
181 pub correction_recall_limit: u32,
182 #[serde(default = "default_correction_min_similarity")]
183 pub correction_min_similarity: f32,
184 #[serde(default = "default_auto_promote_min_uses")]
185 pub auto_promote_min_uses: u32,
186 #[serde(default = "default_auto_promote_threshold")]
187 pub auto_promote_threshold: f64,
188 #[serde(default = "default_auto_demote_min_uses")]
189 pub auto_demote_min_uses: u32,
190 #[serde(default = "default_auto_demote_threshold")]
191 pub auto_demote_threshold: f64,
192 #[serde(default)]
197 pub cross_session_rollout: bool,
198 #[serde(default = "default_min_sessions_before_promote")]
201 pub min_sessions_before_promote: u32,
202 #[serde(default = "default_min_sessions_before_demote")]
208 pub min_sessions_before_demote: u32,
209 #[serde(default = "default_max_auto_sections")]
213 pub max_auto_sections: u32,
214 #[serde(default)]
218 pub domain_success_gate: bool,
219
220 #[serde(default)]
223 pub arise_enabled: bool,
224 #[serde(default = "default_arise_min_tool_calls")]
226 pub arise_min_tool_calls: u32,
227 #[serde(default)]
230 pub arise_trace_provider: String,
231
232 #[serde(default)]
235 pub stem_enabled: bool,
236 #[serde(default = "default_stem_min_occurrences")]
238 pub stem_min_occurrences: u32,
239 #[serde(default = "default_stem_min_success_rate")]
241 pub stem_min_success_rate: f64,
242 #[serde(default)]
245 pub stem_provider: String,
246 #[serde(default = "default_stem_retention_days")]
248 pub stem_retention_days: u32,
249 #[serde(default = "default_stem_pattern_window_days")]
251 pub stem_pattern_window_days: u32,
252
253 #[serde(default)]
256 pub erl_enabled: bool,
257 #[serde(default)]
260 pub erl_extract_provider: String,
261 #[serde(default = "default_erl_max_heuristics_per_skill")]
263 pub erl_max_heuristics_per_skill: u32,
264 #[serde(default = "default_erl_dedup_threshold")]
267 pub erl_dedup_threshold: f32,
268 #[serde(default = "default_erl_min_confidence")]
270 pub erl_min_confidence: f64,
271}
272
273impl Default for LearningConfig {
274 fn default() -> Self {
275 Self {
276 enabled: false,
277 auto_activate: false,
278 min_failures: default_min_failures(),
279 improve_threshold: default_improve_threshold(),
280 rollback_threshold: default_rollback_threshold(),
281 min_evaluations: default_min_evaluations(),
282 max_versions: default_max_versions(),
283 cooldown_minutes: default_cooldown_minutes(),
284 correction_detection: default_correction_detection(),
285 correction_confidence_threshold: default_correction_confidence_threshold(),
286 detector_mode: DetectorMode::default(),
287 judge_model: String::new(),
288 feedback_provider: String::new(),
289 judge_adaptive_low: default_judge_adaptive_low(),
290 judge_adaptive_high: default_judge_adaptive_high(),
291 correction_recall_limit: default_correction_recall_limit(),
292 correction_min_similarity: default_correction_min_similarity(),
293 auto_promote_min_uses: default_auto_promote_min_uses(),
294 auto_promote_threshold: default_auto_promote_threshold(),
295 auto_demote_min_uses: default_auto_demote_min_uses(),
296 auto_demote_threshold: default_auto_demote_threshold(),
297 cross_session_rollout: false,
298 min_sessions_before_promote: default_min_sessions_before_promote(),
299 min_sessions_before_demote: default_min_sessions_before_demote(),
300 max_auto_sections: default_max_auto_sections(),
301 domain_success_gate: false,
302 arise_enabled: false,
303 arise_min_tool_calls: default_arise_min_tool_calls(),
304 arise_trace_provider: String::new(),
305 stem_enabled: false,
306 stem_min_occurrences: default_stem_min_occurrences(),
307 stem_min_success_rate: default_stem_min_success_rate(),
308 stem_provider: String::new(),
309 stem_retention_days: default_stem_retention_days(),
310 stem_pattern_window_days: default_stem_pattern_window_days(),
311 erl_enabled: false,
312 erl_extract_provider: String::new(),
313 erl_max_heuristics_per_skill: default_erl_max_heuristics_per_skill(),
314 erl_dedup_threshold: default_erl_dedup_threshold(),
315 erl_min_confidence: default_erl_min_confidence(),
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn detector_mode_default_is_regex() {
326 assert_eq!(DetectorMode::default(), DetectorMode::Regex);
327 }
328
329 #[test]
330 fn detector_mode_serde_roundtrip() {
331 for (mode, expected_str) in [
332 (DetectorMode::Regex, "\"regex\""),
333 (DetectorMode::Judge, "\"judge\""),
334 (DetectorMode::Model, "\"model\""),
335 ] {
336 let serialized = serde_json::to_string(&mode).unwrap();
337 assert_eq!(serialized, expected_str, "serialize {mode:?}");
338 let deserialized: DetectorMode = serde_json::from_str(&serialized).unwrap();
339 assert_eq!(deserialized, mode, "deserialize {mode:?}");
340 }
341 }
342
343 #[test]
344 fn learning_config_default_detector_mode_is_regex() {
345 let cfg = LearningConfig::default();
346 assert_eq!(cfg.detector_mode, DetectorMode::Regex);
347 }
348
349 #[test]
350 fn learning_config_default_feedback_provider_is_empty() {
351 let cfg = LearningConfig::default();
352 assert!(cfg.feedback_provider.is_empty());
353 }
354
355 #[test]
356 fn learning_config_deserialize_model_mode() {
357 let toml = r#"detector_mode = "model"
358feedback_provider = "fast""#;
359 let cfg: LearningConfig = toml::from_str(toml).unwrap();
360 assert_eq!(cfg.detector_mode, DetectorMode::Model);
361 assert_eq!(cfg.feedback_provider, "fast");
362 }
363
364 #[test]
365 fn learning_config_deserialize_empty_feedback_provider() {
366 let toml = r#"detector_mode = "model""#;
367 let cfg: LearningConfig = toml::from_str(toml).unwrap();
368 assert_eq!(cfg.detector_mode, DetectorMode::Model);
369 assert!(
370 cfg.feedback_provider.is_empty(),
371 "empty feedback_provider must default to empty string (fallback to primary)"
372 );
373 }
374
375 #[test]
376 fn learning_config_deserialize_empty_section_uses_defaults() {
377 let cfg: LearningConfig = toml::from_str("").unwrap();
378 assert!(!cfg.enabled);
379 assert_eq!(cfg.min_failures, 3);
380 assert_eq!(cfg.detector_mode, DetectorMode::Regex);
381 assert!(cfg.feedback_provider.is_empty());
382 }
383
384 #[test]
385 fn learning_config_defaults_for_new_fields() {
386 let cfg = LearningConfig::default();
387 assert!(!cfg.cross_session_rollout);
388 assert_eq!(cfg.min_sessions_before_promote, 2);
389 assert_eq!(cfg.max_auto_sections, 3);
390 assert!(!cfg.domain_success_gate);
391 }
392
393 #[test]
394 fn learning_config_min_sessions_before_demote_default() {
395 let cfg = LearningConfig::default();
396 assert_eq!(cfg.min_sessions_before_demote, 1);
397 }
398
399 #[test]
400 fn arise_stem_erl_defaults() {
401 let cfg = LearningConfig::default();
402 assert!(!cfg.arise_enabled);
403 assert_eq!(cfg.arise_min_tool_calls, 2);
404 assert!(cfg.arise_trace_provider.is_empty());
405 assert!(!cfg.stem_enabled);
406 assert_eq!(cfg.stem_min_occurrences, 3);
407 assert!((cfg.stem_min_success_rate - 0.8).abs() < f64::EPSILON);
408 assert!(cfg.stem_provider.is_empty());
409 assert_eq!(cfg.stem_retention_days, 90);
410 assert_eq!(cfg.stem_pattern_window_days, 30);
411 assert!(!cfg.erl_enabled);
412 assert!(cfg.erl_extract_provider.is_empty());
413 assert_eq!(cfg.erl_max_heuristics_per_skill, 3);
414 assert!((cfg.erl_dedup_threshold - 0.9).abs() < f32::EPSILON);
415 assert!((cfg.erl_min_confidence - 0.5).abs() < f64::EPSILON);
416 }
417
418 #[test]
419 fn arise_stem_erl_serde_roundtrip() {
420 let toml = r#"
421arise_enabled = true
422arise_min_tool_calls = 3
423arise_trace_provider = "fast"
424stem_enabled = true
425stem_min_occurrences = 5
426stem_min_success_rate = 0.9
427stem_provider = "mid"
428stem_retention_days = 60
429stem_pattern_window_days = 14
430erl_enabled = true
431erl_extract_provider = "fast"
432erl_max_heuristics_per_skill = 5
433erl_dedup_threshold = 0.85
434erl_min_confidence = 0.6
435"#;
436 let cfg: LearningConfig = toml::from_str(toml).unwrap();
437 assert!(cfg.arise_enabled);
438 assert_eq!(cfg.arise_min_tool_calls, 3);
439 assert_eq!(cfg.arise_trace_provider, "fast");
440 assert!(cfg.stem_enabled);
441 assert_eq!(cfg.stem_min_occurrences, 5);
442 assert!((cfg.stem_min_success_rate - 0.9).abs() < f64::EPSILON);
443 assert_eq!(cfg.stem_provider, "mid");
444 assert_eq!(cfg.stem_retention_days, 60);
445 assert_eq!(cfg.stem_pattern_window_days, 14);
446 assert!(cfg.erl_enabled);
447 assert_eq!(cfg.erl_extract_provider, "fast");
448 assert_eq!(cfg.erl_max_heuristics_per_skill, 5);
449 assert!((cfg.erl_dedup_threshold - 0.85_f32).abs() < f32::EPSILON);
450 assert!((cfg.erl_min_confidence - 0.6).abs() < f64::EPSILON);
451 }
452
453 #[test]
454 fn arise_stem_erl_empty_section_uses_defaults() {
455 let cfg: LearningConfig = toml::from_str("").unwrap();
456 assert!(!cfg.arise_enabled);
457 assert!(!cfg.stem_enabled);
458 assert!(!cfg.erl_enabled);
459 }
460
461 #[test]
462 fn learning_config_new_fields_serde_roundtrip() {
463 let toml = r"
464cross_session_rollout = true
465min_sessions_before_promote = 5
466min_sessions_before_demote = 2
467max_auto_sections = 4
468domain_success_gate = true
469";
470 let cfg: LearningConfig = toml::from_str(toml).unwrap();
471 assert!(cfg.cross_session_rollout);
472 assert_eq!(cfg.min_sessions_before_promote, 5);
473 assert_eq!(cfg.min_sessions_before_demote, 2);
474 assert_eq!(cfg.max_auto_sections, 4);
475 assert!(cfg.domain_success_gate);
476 }
477}