1use serde::{Deserialize, Serialize};
5
6use crate::defaults::{default_skill_paths, default_true};
7use crate::learning::LearningConfig;
8use crate::providers::ProviderName;
9use crate::security::TrustConfig;
10
11fn default_disambiguation_threshold() -> f32 {
12 0.20
13}
14
15fn default_rl_learning_rate() -> f32 {
16 0.01
17}
18
19fn default_rl_weight() -> f32 {
20 0.3
21}
22
23fn default_rl_persist_interval() -> u32 {
24 10
25}
26
27fn default_rl_warmup_updates() -> u32 {
28 50
29}
30
31fn default_min_injection_score() -> f32 {
32 0.20
33}
34
35fn default_cosine_weight() -> f32 {
36 0.7
37}
38
39fn default_hybrid_search() -> bool {
40 true
41}
42
43fn default_max_active_skills() -> usize {
44 5
45}
46
47fn default_index_watch() -> bool {
48 false
53}
54
55fn default_index_search_enabled() -> bool {
56 true
57}
58
59fn default_index_max_chunks() -> usize {
60 12
61}
62
63fn default_index_concurrency() -> usize {
64 4
65}
66
67fn default_index_batch_size() -> usize {
68 32
69}
70
71fn default_index_memory_batch_size() -> usize {
72 32
73}
74
75fn default_index_max_file_bytes() -> usize {
76 512 * 1024
77}
78
79fn default_index_embed_concurrency() -> usize {
80 2
81}
82
83fn default_index_score_threshold() -> f32 {
84 0.25
85}
86
87fn default_index_budget_ratio() -> f32 {
88 0.40
89}
90
91fn default_index_repo_map_tokens() -> usize {
92 500
93}
94
95fn default_repo_map_ttl_secs() -> u64 {
96 300
97}
98
99fn default_vault_backend() -> String {
100 "env".into()
101}
102
103fn default_max_daily_cents() -> u32 {
104 0
105}
106
107fn default_otlp_endpoint() -> String {
108 "http://localhost:4317".into()
109}
110
111fn default_pid_file() -> String {
112 "~/.zeph/zeph.pid".into()
113}
114
115fn default_health_interval() -> u64 {
116 30
117}
118
119fn default_max_restart_backoff() -> u64 {
120 60
121}
122
123fn default_scheduler_tick_interval() -> u64 {
124 60
125}
126
127fn default_scheduler_max_tasks() -> usize {
128 100
129}
130
131fn default_scheduler_daemon_tick_secs() -> u64 {
132 60
133}
134
135fn default_scheduler_daemon_shutdown_grace_secs() -> u64 {
136 30
137}
138
139fn default_scheduler_daemon_pid_file() -> String {
140 #[cfg(target_os = "macos")]
142 {
143 dirs::data_local_dir()
144 .map_or_else(
145 || std::path::PathBuf::from("~/.zeph/zeph.pid"),
146 |d| d.join("zeph").join("zeph.pid"),
147 )
148 .to_string_lossy()
149 .into_owned()
150 }
151 #[cfg(not(target_os = "macos"))]
152 {
153 dirs::state_dir()
154 .or_else(dirs::data_local_dir)
155 .map_or_else(
156 || std::path::PathBuf::from("~/.zeph/zeph.pid"),
157 |d| d.join("zeph").join("zeph.pid"),
158 )
159 .to_string_lossy()
160 .into_owned()
161 }
162}
163
164fn default_scheduler_daemon_log_file() -> String {
165 #[cfg(target_os = "macos")]
166 {
167 dirs::cache_dir()
169 .map_or_else(
170 || std::path::PathBuf::from("~/.zeph/zeph.log"),
171 |d| d.join("zeph").join("zeph.log"),
172 )
173 .to_string_lossy()
174 .into_owned()
175 }
176 #[cfg(not(target_os = "macos"))]
177 {
178 dirs::state_dir()
179 .or_else(dirs::data_local_dir)
180 .map_or_else(
181 || std::path::PathBuf::from("~/.zeph/zeph.log"),
182 |d| d.join("zeph").join("zeph.log"),
183 )
184 .to_string_lossy()
185 .into_owned()
186 }
187}
188
189fn default_gateway_bind() -> String {
190 "127.0.0.1".into()
191}
192
193fn default_gateway_port() -> u16 {
194 8090
195}
196
197fn default_gateway_rate_limit() -> u32 {
198 120
199}
200
201fn default_gateway_max_body() -> usize {
202 1_048_576
203}
204
205#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
207#[serde(rename_all = "lowercase")]
208pub enum SkillPromptMode {
209 Full,
210 Compact,
211 #[default]
212 Auto,
213}
214
215#[derive(Debug, Deserialize, Serialize)]
230pub struct SkillsConfig {
231 #[serde(default = "default_skill_paths")]
233 pub paths: Vec<String>,
234 #[serde(default = "default_max_active_skills")]
235 pub max_active_skills: usize,
236 #[serde(default = "default_disambiguation_threshold")]
237 pub disambiguation_threshold: f32,
238 #[serde(default = "default_min_injection_score")]
239 pub min_injection_score: f32,
240 #[serde(default = "default_cosine_weight")]
241 pub cosine_weight: f32,
242 #[serde(default = "default_hybrid_search")]
243 pub hybrid_search: bool,
244 #[serde(default)]
245 pub learning: LearningConfig,
246 #[serde(default)]
247 pub trust: TrustConfig,
248 #[serde(default)]
249 pub prompt_mode: SkillPromptMode,
250 #[serde(default)]
253 pub two_stage_matching: bool,
254 #[serde(default)]
257 pub confusability_threshold: f32,
258
259 #[serde(default)]
262 pub rl_routing_enabled: bool,
263 #[serde(default = "default_rl_learning_rate")]
265 pub rl_learning_rate: f32,
266 #[serde(default = "default_rl_weight")]
268 pub rl_weight: f32,
269 #[serde(default = "default_rl_persist_interval")]
271 pub rl_persist_interval: u32,
272 #[serde(default = "default_rl_warmup_updates")]
274 pub rl_warmup_updates: u32,
275 #[serde(default)]
279 pub rl_embed_dim: Option<usize>,
280
281 #[serde(default)]
284 pub generation_provider: ProviderName,
285 #[serde(default)]
287 pub generation_output_dir: Option<String>,
288 #[serde(default)]
290 pub mining: SkillMiningConfig,
291 #[serde(default)]
293 pub evaluation: SkillEvaluationConfig,
294 #[serde(default)]
296 pub proactive_exploration: ProactiveExplorationConfig,
297}
298
299fn default_skill_quality_threshold() -> f32 {
302 0.60
303}
304
305fn default_weight_correctness() -> f32 {
306 0.50
307}
308
309fn default_weight_reusability() -> f32 {
310 0.25
311}
312
313fn default_weight_specificity() -> f32 {
314 0.25
315}
316
317fn default_eval_fail_open() -> bool {
318 true
319}
320
321fn default_skill_eval_timeout_ms() -> u64 {
322 15_000
323}
324
325#[derive(Debug, Deserialize, Serialize)]
347pub struct SkillEvaluationConfig {
348 #[serde(default)]
350 pub enabled: bool,
351 #[serde(default)]
353 pub provider: ProviderName,
354 #[serde(default = "default_skill_quality_threshold")]
356 pub quality_threshold: f32,
357 #[serde(default = "default_weight_correctness")]
359 pub weight_correctness: f32,
360 #[serde(default = "default_weight_reusability")]
362 pub weight_reusability: f32,
363 #[serde(default = "default_weight_specificity")]
365 pub weight_specificity: f32,
366 #[serde(default = "default_eval_fail_open")]
368 pub fail_open_on_error: bool,
369 #[serde(default = "default_skill_eval_timeout_ms")]
371 pub timeout_ms: u64,
372}
373
374impl Default for SkillEvaluationConfig {
375 fn default() -> Self {
376 Self {
377 enabled: false,
378 provider: ProviderName::default(),
379 quality_threshold: default_skill_quality_threshold(),
380 weight_correctness: default_weight_correctness(),
381 weight_reusability: default_weight_reusability(),
382 weight_specificity: default_weight_specificity(),
383 fail_open_on_error: default_eval_fail_open(),
384 timeout_ms: default_skill_eval_timeout_ms(),
385 }
386 }
387}
388
389fn default_proactive_max_chars() -> usize {
392 8_000
393}
394
395fn default_proactive_timeout_ms() -> u64 {
396 30_000
397}
398
399#[derive(Debug, Deserialize, Serialize)]
416pub struct ProactiveExplorationConfig {
417 #[serde(default)]
419 pub enabled: bool,
420 #[serde(default)]
422 pub provider: ProviderName,
423 #[serde(default)]
425 pub output_dir: Option<String>,
426 #[serde(default = "default_proactive_max_chars")]
428 pub max_chars: usize,
429 #[serde(default = "default_proactive_timeout_ms")]
431 pub timeout_ms: u64,
432 #[serde(default)]
435 pub excluded_domains: Vec<String>,
436}
437
438impl Default for ProactiveExplorationConfig {
439 fn default() -> Self {
440 Self {
441 enabled: false,
442 provider: ProviderName::default(),
443 output_dir: None,
444 max_chars: default_proactive_max_chars(),
445 timeout_ms: default_proactive_timeout_ms(),
446 excluded_domains: Vec::new(),
447 }
448 }
449}
450
451fn default_max_repos_per_query() -> usize {
452 20
453}
454
455fn default_dedup_threshold() -> f32 {
456 0.85
457}
458
459fn default_rate_limit_rpm() -> u32 {
460 25
461}
462
463#[derive(Debug, Default, Deserialize, Serialize)]
465pub struct SkillMiningConfig {
466 #[serde(default)]
468 pub queries: Vec<String>,
469 #[serde(default = "default_max_repos_per_query")]
471 pub max_repos_per_query: usize,
472 #[serde(default = "default_dedup_threshold")]
474 pub dedup_threshold: f32,
475 #[serde(default)]
477 pub output_dir: Option<String>,
478 #[serde(default)]
480 pub generation_provider: ProviderName,
481 #[serde(default)]
483 pub embedding_provider: ProviderName,
484 #[serde(default = "default_rate_limit_rpm")]
486 pub rate_limit_rpm: u32,
487}
488
489#[derive(Debug, Deserialize, Serialize)]
505#[allow(clippy::struct_excessive_bools)]
506pub struct IndexConfig {
507 #[serde(default)]
509 pub enabled: bool,
510 #[serde(default = "default_index_search_enabled")]
512 pub search_enabled: bool,
513 #[serde(default = "default_index_watch")]
514 pub watch: bool,
515 #[serde(default = "default_index_max_chunks")]
516 pub max_chunks: usize,
517 #[serde(default = "default_index_score_threshold")]
518 pub score_threshold: f32,
519 #[serde(default = "default_index_budget_ratio")]
520 pub budget_ratio: f32,
521 #[serde(default = "default_index_repo_map_tokens")]
522 pub repo_map_tokens: usize,
523 #[serde(default = "default_repo_map_ttl_secs")]
524 pub repo_map_ttl_secs: u64,
525 #[serde(default)]
529 pub mcp_enabled: bool,
530 #[serde(default)]
533 pub workspace_root: Option<std::path::PathBuf>,
534 #[serde(default = "default_index_concurrency")]
536 pub concurrency: usize,
537 #[serde(default = "default_index_batch_size")]
539 pub batch_size: usize,
540 #[serde(default = "default_index_memory_batch_size")]
544 pub memory_batch_size: usize,
545 #[serde(default = "default_index_max_file_bytes")]
549 pub max_file_bytes: usize,
550 #[serde(default)]
555 pub embed_provider: Option<String>,
556 #[serde(default = "default_index_embed_concurrency")]
559 pub embed_concurrency: usize,
560}
561
562impl Default for IndexConfig {
563 fn default() -> Self {
564 Self {
565 enabled: false,
566 search_enabled: default_index_search_enabled(),
567 watch: default_index_watch(),
568 max_chunks: default_index_max_chunks(),
569 score_threshold: default_index_score_threshold(),
570 budget_ratio: default_index_budget_ratio(),
571 repo_map_tokens: default_index_repo_map_tokens(),
572 repo_map_ttl_secs: default_repo_map_ttl_secs(),
573 mcp_enabled: false,
574 workspace_root: None,
575 concurrency: default_index_concurrency(),
576 batch_size: default_index_batch_size(),
577 memory_batch_size: default_index_memory_batch_size(),
578 max_file_bytes: default_index_max_file_bytes(),
579 embed_provider: None,
580 embed_concurrency: default_index_embed_concurrency(),
581 }
582 }
583}
584
585#[derive(Debug, Deserialize, Serialize)]
596pub struct VaultConfig {
597 #[serde(default = "default_vault_backend")]
599 pub backend: String,
600}
601
602impl Default for VaultConfig {
603 fn default() -> Self {
604 Self {
605 backend: default_vault_backend(),
606 }
607 }
608}
609
610#[derive(Debug, Deserialize, Serialize)]
624pub struct CostConfig {
625 #[serde(default = "default_true")]
627 pub enabled: bool,
628 #[serde(default = "default_max_daily_cents")]
630 pub max_daily_cents: u32,
631}
632
633impl Default for CostConfig {
634 fn default() -> Self {
635 Self {
636 enabled: true,
637 max_daily_cents: default_max_daily_cents(),
638 }
639 }
640}
641
642#[derive(Debug, Clone, Deserialize, Serialize)]
659pub struct GatewayConfig {
660 #[serde(default)]
662 pub enabled: bool,
663 #[serde(default = "default_gateway_bind")]
665 pub bind: String,
666 #[serde(default = "default_gateway_port")]
668 pub port: u16,
669 #[serde(default)]
672 pub auth_token: Option<String>,
673 #[serde(default = "default_gateway_rate_limit")]
675 pub rate_limit: u32,
676 #[serde(default = "default_gateway_max_body")]
678 pub max_body_size: usize,
679}
680
681impl Default for GatewayConfig {
682 fn default() -> Self {
683 Self {
684 enabled: false,
685 bind: default_gateway_bind(),
686 port: default_gateway_port(),
687 auth_token: None,
688 rate_limit: default_gateway_rate_limit(),
689 max_body_size: default_gateway_max_body(),
690 }
691 }
692}
693
694#[derive(Debug, Clone, Deserialize, Serialize)]
708pub struct DaemonConfig {
709 #[serde(default)]
711 pub enabled: bool,
712 #[serde(default = "default_pid_file")]
714 pub pid_file: String,
715 #[serde(default = "default_health_interval")]
717 pub health_interval_secs: u64,
718 #[serde(default = "default_max_restart_backoff")]
720 pub max_restart_backoff_secs: u64,
721}
722
723impl Default for DaemonConfig {
724 fn default() -> Self {
725 Self {
726 enabled: false,
727 pid_file: default_pid_file(),
728 health_interval_secs: default_health_interval(),
729 max_restart_backoff_secs: default_max_restart_backoff(),
730 }
731 }
732}
733
734#[derive(Debug, Clone, Deserialize, Serialize)]
761pub struct SchedulerDaemonConfig {
762 #[serde(default = "default_scheduler_daemon_pid_file")]
764 pub pid_file: String,
765 #[serde(default = "default_scheduler_daemon_log_file")]
767 pub log_file: String,
768 #[serde(default = "crate::defaults::default_true")]
771 pub catch_up: bool,
772 #[serde(default = "default_scheduler_daemon_tick_secs")]
774 pub tick_secs: u64,
775 #[serde(default = "default_scheduler_daemon_shutdown_grace_secs")]
778 pub shutdown_grace_secs: u64,
779}
780
781impl Default for SchedulerDaemonConfig {
782 fn default() -> Self {
783 Self {
784 pid_file: default_scheduler_daemon_pid_file(),
785 log_file: default_scheduler_daemon_log_file(),
786 catch_up: true,
787 tick_secs: default_scheduler_daemon_tick_secs(),
788 shutdown_grace_secs: default_scheduler_daemon_shutdown_grace_secs(),
789 }
790 }
791}
792
793#[derive(Debug, Clone, Deserialize, Serialize)]
813pub struct SchedulerConfig {
814 #[serde(default)]
816 pub enabled: bool,
817 #[serde(default = "default_scheduler_tick_interval")]
819 pub tick_interval_secs: u64,
820 #[serde(default = "default_scheduler_max_tasks")]
822 pub max_tasks: usize,
823 #[serde(default)]
825 pub tasks: Vec<ScheduledTaskConfig>,
826 #[serde(default)]
828 pub daemon: SchedulerDaemonConfig,
829}
830
831impl Default for SchedulerConfig {
832 fn default() -> Self {
833 Self {
834 enabled: true,
835 tick_interval_secs: default_scheduler_tick_interval(),
836 max_tasks: default_scheduler_max_tasks(),
837 tasks: Vec::new(),
838 daemon: SchedulerDaemonConfig::default(),
839 }
840 }
841}
842
843#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
847#[serde(rename_all = "snake_case")]
848pub enum ScheduledTaskKind {
849 MemoryCleanup,
850 SkillRefresh,
851 HealthCheck,
852 UpdateCheck,
853 Experiment,
854 Custom(String),
855}
856
857#[derive(Debug, Clone, Deserialize, Serialize)]
861pub struct ScheduledTaskConfig {
862 pub name: String,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub cron: Option<String>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub run_at: Option<String>,
870 pub kind: ScheduledTaskKind,
872 #[serde(default)]
874 pub config: serde_json::Value,
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880
881 #[test]
882 fn index_config_defaults() {
883 let cfg = IndexConfig::default();
884 assert!(!cfg.enabled);
885 assert!(cfg.search_enabled);
886 assert!(!cfg.watch);
887 assert_eq!(cfg.concurrency, 4);
888 assert_eq!(cfg.batch_size, 32);
889 assert!(cfg.workspace_root.is_none());
890 }
891
892 #[test]
893 fn index_config_serde_roundtrip_with_new_fields() {
894 let toml = r#"
895 enabled = true
896 concurrency = 8
897 batch_size = 16
898 workspace_root = "/tmp/myproject"
899 "#;
900 let cfg: IndexConfig = toml::from_str(toml).unwrap();
901 assert!(cfg.enabled);
902 assert_eq!(cfg.concurrency, 8);
903 assert_eq!(cfg.batch_size, 16);
904 assert_eq!(
905 cfg.workspace_root,
906 Some(std::path::PathBuf::from("/tmp/myproject"))
907 );
908 let serialized = toml::to_string(&cfg).unwrap();
910 let cfg2: IndexConfig = toml::from_str(&serialized).unwrap();
911 assert_eq!(cfg2.concurrency, 8);
912 assert_eq!(cfg2.batch_size, 16);
913 }
914
915 #[test]
916 fn index_config_backward_compat_old_toml_without_new_fields() {
917 let toml = "
920 enabled = true
921 max_chunks = 20
922 score_threshold = 0.3
923 ";
924 let cfg: IndexConfig = toml::from_str(toml).unwrap();
925 assert!(cfg.enabled);
926 assert_eq!(cfg.max_chunks, 20);
927 assert!(cfg.workspace_root.is_none());
928 assert_eq!(cfg.concurrency, 4);
929 assert_eq!(cfg.batch_size, 32);
930 }
931
932 #[test]
933 fn index_config_workspace_root_none_by_default() {
934 let cfg: IndexConfig = toml::from_str("enabled = false").unwrap();
935 assert!(cfg.workspace_root.is_none());
936 }
937}
938
939fn default_compression_spectrum_promotion_window() -> usize {
942 200
943}
944
945fn default_compression_spectrum_min_occurrences() -> u32 {
946 3
947}
948
949fn default_compression_spectrum_min_sessions() -> u32 {
950 2
951}
952
953fn default_compression_spectrum_cluster_threshold() -> f32 {
954 0.85
955}
956
957fn default_retrieval_low_budget_ratio() -> f32 {
958 0.20
959}
960
961fn default_retrieval_mid_budget_ratio() -> f32 {
962 0.50
963}
964
965#[derive(Debug, Deserialize, Serialize)]
981pub struct CompressionSpectrumConfig {
982 #[serde(default)]
984 pub enabled: bool,
985 #[serde(default)]
987 pub promotion_output_dir: Option<String>,
988 #[serde(default)]
990 pub promotion_provider: ProviderName,
991 #[serde(default = "default_compression_spectrum_promotion_window")]
994 pub promotion_window: usize,
995 #[serde(default = "default_compression_spectrum_min_occurrences")]
998 pub min_occurrences: u32,
999 #[serde(default = "default_compression_spectrum_min_sessions")]
1001 pub min_sessions: u32,
1002 #[serde(default = "default_compression_spectrum_cluster_threshold")]
1004 pub cluster_threshold: f32,
1005 #[serde(default = "default_retrieval_low_budget_ratio")]
1007 pub retrieval_low_budget_ratio: f32,
1008 #[serde(default = "default_retrieval_mid_budget_ratio")]
1010 pub retrieval_mid_budget_ratio: f32,
1011}
1012
1013impl Default for CompressionSpectrumConfig {
1014 fn default() -> Self {
1015 Self {
1016 enabled: false,
1017 promotion_output_dir: None,
1018 promotion_provider: ProviderName::default(),
1019 promotion_window: default_compression_spectrum_promotion_window(),
1020 min_occurrences: default_compression_spectrum_min_occurrences(),
1021 min_sessions: default_compression_spectrum_min_sessions(),
1022 cluster_threshold: default_compression_spectrum_cluster_threshold(),
1023 retrieval_low_budget_ratio: default_retrieval_low_budget_ratio(),
1024 retrieval_mid_budget_ratio: default_retrieval_mid_budget_ratio(),
1025 }
1026 }
1027}
1028
1029fn default_trace_service_name() -> String {
1030 "zeph".into()
1031}
1032
1033#[derive(Debug, Clone, Deserialize, Serialize)]
1040#[serde(default)]
1041pub struct TraceConfig {
1042 #[serde(default = "default_otlp_endpoint")]
1045 pub otlp_endpoint: String,
1046 #[serde(default = "default_trace_service_name")]
1048 pub service_name: String,
1049 #[serde(default = "default_true")]
1051 pub redact: bool,
1052}
1053
1054impl Default for TraceConfig {
1055 fn default() -> Self {
1056 Self {
1057 otlp_endpoint: default_otlp_endpoint(),
1058 service_name: default_trace_service_name(),
1059 redact: true,
1060 }
1061 }
1062}
1063
1064#[derive(Debug, Clone, Deserialize, Serialize)]
1077#[serde(default)]
1078pub struct DebugConfig {
1079 pub enabled: bool,
1081 #[serde(default = "crate::defaults::default_debug_output_dir")]
1083 pub output_dir: std::path::PathBuf,
1084 pub format: crate::dump_format::DumpFormat,
1086 pub traces: TraceConfig,
1088}
1089
1090impl Default for DebugConfig {
1091 fn default() -> Self {
1092 Self {
1093 enabled: false,
1094 output_dir: super::defaults::default_debug_output_dir(),
1095 format: crate::dump_format::DumpFormat::default(),
1096 traces: TraceConfig::default(),
1097 }
1098 }
1099}