1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
11#[serde(rename_all = "lowercase")]
12pub enum DevDisciplineIndex {
13 #[default]
15 Auto,
16 Always,
18 Never,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(default)]
24pub struct SkillsConfig {
25 pub max_skills_in_prompt: usize,
26 pub max_total_tokens: usize,
27 pub priority_order: Vec<String>,
28 pub adaptive: Option<AdaptiveSkillsConfig>,
29
30 #[serde(default = "default_require_human_curation")]
34 pub require_human_curation_before_stable: bool,
35
36 #[serde(default)]
40 pub lifecycle: SkillLifecycleConfig,
41
42 #[serde(default = "default_auto_upgrade")]
47 pub auto_upgrade: bool,
48
49 #[serde(default)]
51 pub dev_discipline_index: DevDisciplineIndex,
52}
53
54fn default_require_human_curation() -> bool {
55 true
56}
57
58fn default_auto_upgrade() -> bool {
59 true
60}
61
62impl Default for SkillsConfig {
63 fn default() -> Self {
64 Self {
65 max_skills_in_prompt: 5,
66 max_total_tokens: 2000,
67 priority_order: vec!["agent".into(), "global".into()],
68 adaptive: Some(AdaptiveSkillsConfig::default()),
69 require_human_curation_before_stable: default_require_human_curation(),
70 lifecycle: SkillLifecycleConfig::default(),
71 auto_upgrade: default_auto_upgrade(),
72 dev_discipline_index: DevDisciplineIndex::default(),
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(default)]
84pub struct SkillLifecycleConfig {
85 pub note_rule_half_life_factor: f64,
89 pub note_fact_half_life_factor: f64,
92 pub promote_draft_uses: u64,
94 pub promote_emerging_uses: u64,
95 pub promote_emerging_success_rate: f64,
96 pub promote_emerging_age_days: i64,
97 pub promote_stable_uses: u64,
98 pub promote_stable_success_rate: f64,
99 pub promote_stable_age_days: i64,
100
101 pub demote_emerging_uses: u64,
103 pub demote_emerging_success_rate: f64,
104 pub demote_stable_uses: u64,
105 pub demote_stable_success_rate: f64,
106 pub deprecated_success_rate: f64,
107 pub deprecated_no_success_days: i64,
108
109 pub auto_archive_confidence: f64,
111 pub auto_archive_age_days: i64,
112
113 pub broken_workflow_streak: u32,
118
119 pub archive_destroy_grace_days: i64,
124}
125
126impl Default for SkillLifecycleConfig {
127 fn default() -> Self {
128 Self {
129 promote_draft_uses: 3,
130 promote_emerging_uses: 10,
131 promote_emerging_success_rate: 0.6,
132 promote_emerging_age_days: 7,
133 promote_stable_uses: 30,
134 promote_stable_success_rate: 0.8,
135 promote_stable_age_days: 30,
136 demote_emerging_uses: 8,
137 demote_emerging_success_rate: 0.55,
138 demote_stable_uses: 25,
139 demote_stable_success_rate: 0.75,
140 deprecated_success_rate: 0.3,
141 deprecated_no_success_days: 90,
142 auto_archive_confidence: 0.10,
143 auto_archive_age_days: 180,
144 broken_workflow_streak: 3,
145 archive_destroy_grace_days: 30,
146 note_rule_half_life_factor: crate::skill::lifecycle::NOTE_RULE_HALF_LIFE_FACTOR,
147 note_fact_half_life_factor: crate::skill::lifecycle::NOTE_FACT_HALF_LIFE_FACTOR,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(default)]
154pub struct AdaptiveSkillsConfig {
155 pub context_fill_decay: f64,
156 pub min_remaining_context_ratio: f64,
157 pub recent_fire_boost_turns: usize,
158 pub model_max_context_tokens: u64,
162}
163
164impl Default for AdaptiveSkillsConfig {
165 fn default() -> Self {
166 Self {
167 context_fill_decay: 1.5,
168 min_remaining_context_ratio: 0.20,
169 recent_fire_boost_turns: 5,
170 model_max_context_tokens: 200_000,
171 }
172 }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct SleepCycleConfig {
179 #[serde(default)]
181 pub enabled: bool,
182
183 #[serde(default = "default_idle_threshold_minutes")]
185 pub idle_threshold_minutes: u64,
186
187 #[serde(default = "default_agent_idle_minutes")]
189 pub agent_idle_minutes: u64,
190}
191
192fn default_idle_threshold_minutes() -> u64 {
193 15
194}
195
196fn default_agent_idle_minutes() -> u64 {
197 5
198}
199
200impl Default for SleepCycleConfig {
201 fn default() -> Self {
202 Self {
203 enabled: false,
204 idle_threshold_minutes: default_idle_threshold_minutes(),
205 agent_idle_minutes: default_agent_idle_minutes(),
206 }
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct NudgeConfig {
214 #[serde(default = "default_nudge_enabled")]
216 pub enabled: bool,
217 #[serde(default = "default_nudge_daily_cap")]
218 pub daily_cap: u32,
219 #[serde(default = "default_nudge_snooze_days")]
220 pub snooze_days: u32,
221 #[serde(default = "default_nudge_threshold")]
222 pub threshold: usize,
223}
224
225fn default_nudge_enabled() -> bool {
226 true
227}
228fn default_nudge_daily_cap() -> u32 {
229 3
230}
231fn default_nudge_snooze_days() -> u32 {
232 7
233}
234fn default_nudge_threshold() -> usize {
235 3
236}
237
238impl Default for NudgeConfig {
239 fn default() -> Self {
240 Self {
241 enabled: true,
242 daily_cap: default_nudge_daily_cap(),
243 snooze_days: default_nudge_snooze_days(),
244 threshold: default_nudge_threshold(),
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct SessionCfg {
254 #[serde(default = "default_capture_mode")]
256 pub capture: String,
257 #[serde(default = "default_retention_days")]
259 pub retention_days: u32,
260}
261
262impl Default for SessionCfg {
263 fn default() -> Self {
264 Self {
265 capture: default_capture_mode(),
266 retention_days: default_retention_days(),
267 }
268 }
269}
270
271fn default_capture_mode() -> String {
272 "ambient".to_string()
273}
274fn default_retention_days() -> u32 {
275 14
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct HarvestCfg {
281 #[serde(default = "default_harvest_enabled")]
283 pub auto_gate: bool,
284 #[serde(default = "default_harvest_llm")]
286 pub llm: String,
287 #[serde(default = "default_min_events")]
289 pub min_events: usize,
290 #[serde(default = "default_min_user_turns")]
291 pub min_user_turns: usize,
292 #[serde(default = "default_min_duration_secs")]
293 pub min_duration_secs: i64,
294 #[serde(default = "default_idle_minutes")]
296 pub idle_minutes: i64,
297 #[serde(default = "default_max_steps")]
300 pub max_steps: usize,
301 #[serde(default = "default_max_duration_secs")]
302 pub max_duration_secs: i64,
303 #[serde(default = "default_max_llm_calls_per_day")]
305 pub max_llm_calls_per_day: u32,
306 #[serde(default = "default_max_extract_input_tokens")]
307 pub max_extract_input_tokens: usize,
308 #[serde(default = "default_harvest_enabled")]
310 pub session_start_hint: bool,
311 #[serde(default = "default_similarity_merge_threshold")]
314 pub similarity_merge_threshold: f32,
315 #[serde(default = "default_min_occurrences")]
319 pub min_occurrences: usize,
320}
321
322impl Default for HarvestCfg {
323 fn default() -> Self {
324 serde_yaml::from_str("{}").expect("HarvestCfg defaults")
325 }
326}
327
328fn default_harvest_enabled() -> bool {
329 true
330}
331fn default_harvest_llm() -> String {
332 "local-first".to_string()
333}
334fn default_min_events() -> usize {
335 5
336}
337fn default_min_user_turns() -> usize {
338 2
339}
340fn default_min_duration_secs() -> i64 {
341 120
342}
343fn default_idle_minutes() -> i64 {
344 30
345}
346fn default_max_steps() -> usize {
350 20
351}
352fn default_max_duration_secs() -> i64 {
355 1800
356}
357fn default_max_llm_calls_per_day() -> u32 {
358 10
359}
360fn default_max_extract_input_tokens() -> usize {
361 12000
362}
363fn default_similarity_merge_threshold() -> f32 {
364 0.6
365}
366fn default_min_occurrences() -> usize {
369 2
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(default)]
376pub struct CrossAgentConfig {
377 #[serde(default = "default_half_life_days")]
378 pub fitness_half_life_days: u32,
379 #[serde(default = "default_fitness_floor")]
380 pub fitness_floor: f64,
381}
382
383fn default_half_life_days() -> u32 {
384 7
385}
386fn default_fitness_floor() -> f64 {
387 0.1
388}
389
390impl Default for CrossAgentConfig {
391 fn default() -> Self {
392 Self {
393 fitness_half_life_days: default_half_life_days(),
394 fitness_floor: default_fitness_floor(),
395 }
396 }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
402#[serde(default)]
403pub struct SkillLlmConfig {
404 #[serde(default = "default_per_call_token_cap")]
406 pub per_call_token_cap: u32,
407
408 #[serde(default = "default_per_day_usd_cap")]
410 pub per_day_usd_cap: f64,
411
412 #[serde(default = "default_cache_ttl_days")]
414 pub cache_ttl_days: u32,
415
416 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub model_ref: Option<String>,
419}
420
421fn default_per_call_token_cap() -> u32 {
422 1500
423}
424fn default_per_day_usd_cap() -> f64 {
425 0.50
426}
427fn default_cache_ttl_days() -> u32 {
428 30
429}
430
431impl Default for SkillLlmConfig {
432 fn default() -> Self {
433 Self {
434 per_call_token_cap: default_per_call_token_cap(),
435 per_day_usd_cap: default_per_day_usd_cap(),
436 cache_ttl_days: default_cache_ttl_days(),
437 model_ref: None,
438 }
439 }
440}
441
442#[cfg(test)]
443mod skills_config_tests {
444 use super::*;
445
446 #[test]
447 fn empty_yaml_hydrates_defaults() {
448 let cfg: Config = serde_yaml_ng::from_str("{}").unwrap();
449 assert_eq!(cfg.skills.max_skills_in_prompt, 5);
450 assert_eq!(cfg.skills.max_total_tokens, 2000);
451 assert!(cfg.skills.adaptive.is_some());
452 }
453
454 #[test]
455 fn load_or_default_missing_file_returns_default() {
456 let cfg = Config::load_or_default(std::path::Path::new("/nonexistent/config.yaml"));
457 assert_eq!(cfg.skills.max_skills_in_prompt, 5);
458 }
459
460 #[test]
461 fn dev_discipline_index_defaults_auto_and_parses() {
462 use crate::config::DevDisciplineIndex;
463 let cfg: Config = serde_yaml_ng::from_str("").unwrap_or_default();
464 assert_eq!(cfg.skills.dev_discipline_index, DevDisciplineIndex::Auto);
465 let cfg: Config =
466 serde_yaml_ng::from_str("skills:\n dev_discipline_index: never\n").unwrap();
467 assert_eq!(cfg.skills.dev_discipline_index, DevDisciplineIndex::Never);
468 let cfg: Config =
469 serde_yaml_ng::from_str("skills:\n dev_discipline_index: always\n").unwrap();
470 assert_eq!(cfg.skills.dev_discipline_index, DevDisciplineIndex::Always);
471 }
472}