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