1use std::num::NonZeroUsize;
5
6use serde::{Deserialize, Serialize};
7
8use crate::defaults::{default_skill_paths, default_true};
9use crate::learning::LearningConfig;
10use crate::providers::ProviderName;
11use crate::security::TrustConfig;
12
13fn default_disambiguation_threshold() -> f32 {
14 0.20
15}
16
17fn default_rl_learning_rate() -> f32 {
18 0.01
19}
20
21fn default_rl_weight() -> f32 {
22 0.3
23}
24
25fn default_rl_persist_interval() -> u32 {
26 10
27}
28
29fn default_rl_warmup_updates() -> u32 {
30 50
31}
32
33fn default_min_injection_score() -> f32 {
34 0.20
35}
36
37fn default_cosine_weight() -> f32 {
38 0.7
39}
40
41fn default_hybrid_search() -> bool {
42 true
43}
44
45fn default_max_active_skills() -> NonZeroUsize {
46 NonZeroUsize::new(5).expect("5 is non-zero")
47}
48
49fn default_index_watch() -> bool {
50 false
55}
56
57fn default_index_search_enabled() -> bool {
58 true
59}
60
61fn default_index_max_chunks() -> usize {
62 12
63}
64
65fn default_index_concurrency() -> usize {
66 4
67}
68
69fn default_index_batch_size() -> usize {
70 32
71}
72
73fn default_index_memory_batch_size() -> usize {
74 32
75}
76
77fn default_index_max_file_bytes() -> usize {
78 512 * 1024
79}
80
81fn default_index_embed_concurrency() -> usize {
82 2
83}
84
85fn default_index_score_threshold() -> f32 {
86 0.25
87}
88
89fn default_index_budget_ratio() -> f32 {
90 0.40
91}
92
93fn default_index_repo_map_tokens() -> usize {
94 500
95}
96
97fn default_repo_map_ttl_secs() -> u64 {
98 300
99}
100
101fn default_vault_backend() -> String {
102 "env".into()
103}
104
105fn default_max_daily_cents() -> u32 {
106 0
107}
108
109fn default_otlp_endpoint() -> String {
110 "http://localhost:4317".into()
111}
112
113fn default_pid_file() -> String {
114 "~/.zeph/zeph.pid".into()
115}
116
117fn default_health_interval() -> u64 {
118 30
119}
120
121fn default_max_restart_backoff() -> u64 {
122 60
123}
124
125fn default_scheduler_tick_interval() -> u64 {
126 60
127}
128
129fn default_scheduler_max_tasks() -> usize {
130 100
131}
132
133fn default_scheduler_daemon_tick_secs() -> u64 {
134 60
135}
136
137fn default_scheduler_daemon_shutdown_grace_secs() -> u64 {
138 30
139}
140
141fn default_scheduler_daemon_pid_file() -> String {
142 #[cfg(target_os = "macos")]
144 {
145 dirs::data_local_dir()
146 .map_or_else(
147 || std::path::PathBuf::from("~/.zeph/zeph.pid"),
148 |d| d.join("zeph").join("zeph.pid"),
149 )
150 .to_string_lossy()
151 .into_owned()
152 }
153 #[cfg(not(target_os = "macos"))]
154 {
155 dirs::state_dir()
156 .or_else(dirs::data_local_dir)
157 .map_or_else(
158 || std::path::PathBuf::from("~/.zeph/zeph.pid"),
159 |d| d.join("zeph").join("zeph.pid"),
160 )
161 .to_string_lossy()
162 .into_owned()
163 }
164}
165
166fn default_scheduler_daemon_log_file() -> String {
167 #[cfg(target_os = "macos")]
168 {
169 dirs::cache_dir()
171 .map_or_else(
172 || std::path::PathBuf::from("~/.zeph/zeph.log"),
173 |d| d.join("zeph").join("zeph.log"),
174 )
175 .to_string_lossy()
176 .into_owned()
177 }
178 #[cfg(not(target_os = "macos"))]
179 {
180 dirs::state_dir()
181 .or_else(dirs::data_local_dir)
182 .map_or_else(
183 || std::path::PathBuf::from("~/.zeph/zeph.log"),
184 |d| d.join("zeph").join("zeph.log"),
185 )
186 .to_string_lossy()
187 .into_owned()
188 }
189}
190
191fn default_gateway_bind() -> String {
192 "127.0.0.1".into()
193}
194
195fn default_gateway_port() -> u16 {
196 8090
197}
198
199fn default_gateway_rate_limit() -> u32 {
200 120
201}
202
203fn default_gateway_max_body() -> usize {
204 1_048_576
205}
206
207#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
209#[serde(rename_all = "lowercase")]
210pub enum SkillPromptMode {
211 Full,
212 Compact,
213 #[default]
214 Auto,
215}
216
217#[derive(Debug, Deserialize, Serialize)]
232pub struct SkillsConfig {
233 #[serde(default = "default_skill_paths")]
235 pub paths: Vec<String>,
236 #[serde(default = "default_max_active_skills")]
237 pub max_active_skills: NonZeroUsize,
238 #[serde(default = "default_disambiguation_threshold")]
239 pub disambiguation_threshold: f32,
240 #[serde(default = "default_min_injection_score")]
241 pub min_injection_score: f32,
242 #[serde(default = "default_cosine_weight")]
243 pub cosine_weight: f32,
244 #[serde(default = "default_hybrid_search")]
245 pub hybrid_search: bool,
246 #[serde(default)]
247 pub learning: LearningConfig,
248 #[serde(default)]
249 pub trust: TrustConfig,
250 #[serde(default)]
251 pub prompt_mode: SkillPromptMode,
252 #[serde(default)]
255 pub two_stage_matching: bool,
256 #[serde(default)]
259 pub confusability_threshold: f32,
260
261 #[serde(default)]
264 pub rl_routing_enabled: bool,
265 #[serde(default = "default_rl_learning_rate")]
267 pub rl_learning_rate: f32,
268 #[serde(default = "default_rl_weight")]
270 pub rl_weight: f32,
271 #[serde(default = "default_rl_persist_interval")]
273 pub rl_persist_interval: u32,
274 #[serde(default = "default_rl_warmup_updates")]
276 pub rl_warmup_updates: u32,
277 #[serde(default)]
281 pub rl_embed_dim: Option<usize>,
282
283 #[serde(default)]
286 pub generation_provider: ProviderName,
287 #[serde(default)]
289 pub generation_output_dir: Option<String>,
290 #[serde(default)]
292 pub mining: SkillMiningConfig,
293 #[serde(default)]
295 pub evaluation: SkillEvaluationConfig,
296 #[serde(default)]
298 pub proactive_exploration: ProactiveExplorationConfig,
299}
300
301fn default_skill_quality_threshold() -> f32 {
304 0.60
305}
306
307fn default_weight_correctness() -> f32 {
308 0.50
309}
310
311fn default_weight_reusability() -> f32 {
312 0.25
313}
314
315fn default_weight_specificity() -> f32 {
316 0.25
317}
318
319fn default_eval_fail_open() -> bool {
320 true
321}
322
323fn default_skill_eval_timeout_ms() -> u64 {
324 15_000
325}
326
327#[derive(Debug, Deserialize, Serialize)]
349pub struct SkillEvaluationConfig {
350 #[serde(default)]
352 pub enabled: bool,
353 #[serde(default)]
355 pub provider: ProviderName,
356 #[serde(default = "default_skill_quality_threshold")]
358 pub quality_threshold: f32,
359 #[serde(default = "default_weight_correctness")]
361 pub weight_correctness: f32,
362 #[serde(default = "default_weight_reusability")]
364 pub weight_reusability: f32,
365 #[serde(default = "default_weight_specificity")]
367 pub weight_specificity: f32,
368 #[serde(default = "default_eval_fail_open")]
370 pub fail_open_on_error: bool,
371 #[serde(default = "default_skill_eval_timeout_ms")]
373 pub timeout_ms: u64,
374}
375
376impl Default for SkillEvaluationConfig {
377 fn default() -> Self {
378 Self {
379 enabled: false,
380 provider: ProviderName::default(),
381 quality_threshold: default_skill_quality_threshold(),
382 weight_correctness: default_weight_correctness(),
383 weight_reusability: default_weight_reusability(),
384 weight_specificity: default_weight_specificity(),
385 fail_open_on_error: default_eval_fail_open(),
386 timeout_ms: default_skill_eval_timeout_ms(),
387 }
388 }
389}
390
391fn default_proactive_max_chars() -> usize {
394 8_000
395}
396
397fn default_proactive_timeout_ms() -> u64 {
398 30_000
399}
400
401#[derive(Debug, Deserialize, Serialize)]
418pub struct ProactiveExplorationConfig {
419 #[serde(default)]
421 pub enabled: bool,
422 #[serde(default)]
424 pub provider: ProviderName,
425 #[serde(default)]
427 pub output_dir: Option<String>,
428 #[serde(default = "default_proactive_max_chars")]
430 pub max_chars: usize,
431 #[serde(default = "default_proactive_timeout_ms")]
433 pub timeout_ms: u64,
434 #[serde(default)]
437 pub excluded_domains: Vec<String>,
438}
439
440impl Default for ProactiveExplorationConfig {
441 fn default() -> Self {
442 Self {
443 enabled: false,
444 provider: ProviderName::default(),
445 output_dir: None,
446 max_chars: default_proactive_max_chars(),
447 timeout_ms: default_proactive_timeout_ms(),
448 excluded_domains: Vec::new(),
449 }
450 }
451}
452
453fn default_max_repos_per_query() -> usize {
454 20
455}
456
457fn default_dedup_threshold() -> f32 {
458 0.85
459}
460
461fn default_rate_limit_rpm() -> u32 {
462 25
463}
464
465#[derive(Debug, Default, Deserialize, Serialize)]
467pub struct SkillMiningConfig {
468 #[serde(default)]
470 pub queries: Vec<String>,
471 #[serde(default = "default_max_repos_per_query")]
473 pub max_repos_per_query: usize,
474 #[serde(default = "default_dedup_threshold")]
476 pub dedup_threshold: f32,
477 #[serde(default)]
479 pub output_dir: Option<String>,
480 #[serde(default)]
482 pub generation_provider: ProviderName,
483 #[serde(default)]
485 pub embedding_provider: ProviderName,
486 #[serde(default = "default_rate_limit_rpm")]
488 pub rate_limit_rpm: u32,
489}
490
491#[derive(Debug, Deserialize, Serialize)]
507#[allow(clippy::struct_excessive_bools)] pub struct IndexConfig {
509 #[serde(default)]
511 pub enabled: bool,
512 #[serde(default = "default_index_search_enabled")]
514 pub search_enabled: bool,
515 #[serde(default = "default_index_watch")]
516 pub watch: bool,
517 #[serde(default = "default_index_max_chunks")]
518 pub max_chunks: usize,
519 #[serde(default = "default_index_score_threshold")]
520 pub score_threshold: f32,
521 #[serde(default = "default_index_budget_ratio")]
522 pub budget_ratio: f32,
523 #[serde(default = "default_index_repo_map_tokens")]
524 pub repo_map_tokens: usize,
525 #[serde(default = "default_repo_map_ttl_secs")]
526 pub repo_map_ttl_secs: u64,
527 #[serde(default)]
531 pub mcp_enabled: bool,
532 #[serde(default)]
535 pub workspace_root: Option<std::path::PathBuf>,
536 #[serde(default = "default_index_concurrency")]
538 pub concurrency: usize,
539 #[serde(default = "default_index_batch_size")]
541 pub batch_size: usize,
542 #[serde(default = "default_index_memory_batch_size")]
546 pub memory_batch_size: usize,
547 #[serde(default = "default_index_max_file_bytes")]
551 pub max_file_bytes: usize,
552 #[serde(default)]
557 pub embed_provider: Option<ProviderName>,
558 #[serde(default = "default_index_embed_concurrency")]
561 pub embed_concurrency: usize,
562}
563
564impl Default for IndexConfig {
565 fn default() -> Self {
566 Self {
567 enabled: false,
568 search_enabled: default_index_search_enabled(),
569 watch: default_index_watch(),
570 max_chunks: default_index_max_chunks(),
571 score_threshold: default_index_score_threshold(),
572 budget_ratio: default_index_budget_ratio(),
573 repo_map_tokens: default_index_repo_map_tokens(),
574 repo_map_ttl_secs: default_repo_map_ttl_secs(),
575 mcp_enabled: false,
576 workspace_root: None,
577 concurrency: default_index_concurrency(),
578 batch_size: default_index_batch_size(),
579 memory_batch_size: default_index_memory_batch_size(),
580 max_file_bytes: default_index_max_file_bytes(),
581 embed_provider: None,
582 embed_concurrency: default_index_embed_concurrency(),
583 }
584 }
585}
586
587#[derive(Debug, Deserialize, Serialize)]
598pub struct VaultConfig {
599 #[serde(default = "default_vault_backend")]
601 pub backend: String,
602}
603
604impl Default for VaultConfig {
605 fn default() -> Self {
606 Self {
607 backend: default_vault_backend(),
608 }
609 }
610}
611
612#[derive(Debug, Deserialize, Serialize)]
626pub struct CostConfig {
627 #[serde(default = "default_true")]
629 pub enabled: bool,
630 #[serde(default = "default_max_daily_cents")]
632 pub max_daily_cents: u32,
633}
634
635impl Default for CostConfig {
636 fn default() -> Self {
637 Self {
638 enabled: true,
639 max_daily_cents: default_max_daily_cents(),
640 }
641 }
642}
643
644#[derive(Debug, Clone, Deserialize, Serialize)]
661pub struct GatewayConfig {
662 #[serde(default)]
664 pub enabled: bool,
665 #[serde(default = "default_gateway_bind")]
667 pub bind: String,
668 #[serde(default = "default_gateway_port")]
670 pub port: u16,
671 #[serde(default)]
674 pub auth_token: Option<String>,
675 #[serde(default = "default_gateway_rate_limit")]
677 pub rate_limit: u32,
678 #[serde(default = "default_gateway_max_body")]
680 pub max_body_size: usize,
681}
682
683impl Default for GatewayConfig {
684 fn default() -> Self {
685 Self {
686 enabled: false,
687 bind: default_gateway_bind(),
688 port: default_gateway_port(),
689 auth_token: None,
690 rate_limit: default_gateway_rate_limit(),
691 max_body_size: default_gateway_max_body(),
692 }
693 }
694}
695
696#[derive(Debug, Clone, Deserialize, Serialize)]
710pub struct DaemonConfig {
711 #[serde(default)]
713 pub enabled: bool,
714 #[serde(default = "default_pid_file")]
716 pub pid_file: String,
717 #[serde(default = "default_health_interval")]
719 pub health_interval_secs: u64,
720 #[serde(default = "default_max_restart_backoff")]
722 pub max_restart_backoff_secs: u64,
723}
724
725impl Default for DaemonConfig {
726 fn default() -> Self {
727 Self {
728 enabled: false,
729 pid_file: default_pid_file(),
730 health_interval_secs: default_health_interval(),
731 max_restart_backoff_secs: default_max_restart_backoff(),
732 }
733 }
734}
735
736#[derive(Debug, Clone, Deserialize, Serialize)]
763pub struct SchedulerDaemonConfig {
764 #[serde(default = "default_scheduler_daemon_pid_file")]
766 pub pid_file: String,
767 #[serde(default = "default_scheduler_daemon_log_file")]
769 pub log_file: String,
770 #[serde(default = "crate::defaults::default_true")]
773 pub catch_up: bool,
774 #[serde(default = "default_scheduler_daemon_tick_secs")]
776 pub tick_secs: u64,
777 #[serde(default = "default_scheduler_daemon_shutdown_grace_secs")]
780 pub shutdown_grace_secs: u64,
781}
782
783impl Default for SchedulerDaemonConfig {
784 fn default() -> Self {
785 Self {
786 pid_file: default_scheduler_daemon_pid_file(),
787 log_file: default_scheduler_daemon_log_file(),
788 catch_up: true,
789 tick_secs: default_scheduler_daemon_tick_secs(),
790 shutdown_grace_secs: default_scheduler_daemon_shutdown_grace_secs(),
791 }
792 }
793}
794
795#[derive(Debug, Clone, Deserialize, Serialize)]
815pub struct SchedulerConfig {
816 #[serde(default)]
818 pub enabled: bool,
819 #[serde(default = "default_scheduler_tick_interval")]
821 pub tick_interval_secs: u64,
822 #[serde(default = "default_scheduler_max_tasks")]
824 pub max_tasks: usize,
825 #[serde(default)]
827 pub tasks: Vec<ScheduledTaskConfig>,
828 #[serde(default)]
830 pub daemon: SchedulerDaemonConfig,
831}
832
833impl Default for SchedulerConfig {
834 fn default() -> Self {
835 Self {
836 enabled: true,
837 tick_interval_secs: default_scheduler_tick_interval(),
838 max_tasks: default_scheduler_max_tasks(),
839 tasks: Vec::new(),
840 daemon: SchedulerDaemonConfig::default(),
841 }
842 }
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
849#[serde(rename_all = "snake_case")]
850pub enum ScheduledTaskKind {
851 MemoryCleanup,
852 SkillRefresh,
853 HealthCheck,
854 UpdateCheck,
855 Experiment,
856 Custom(String),
857}
858
859#[derive(Debug, Clone, Deserialize, Serialize)]
863pub struct ScheduledTaskConfig {
864 pub name: String,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub cron: Option<String>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub run_at: Option<String>,
872 pub kind: ScheduledTaskKind,
874 #[serde(default)]
876 pub config: serde_json::Value,
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 #[test]
884 fn index_config_defaults() {
885 let cfg = IndexConfig::default();
886 assert!(!cfg.enabled);
887 assert!(cfg.search_enabled);
888 assert!(!cfg.watch);
889 assert_eq!(cfg.concurrency, 4);
890 assert_eq!(cfg.batch_size, 32);
891 assert!(cfg.workspace_root.is_none());
892 }
893
894 #[test]
895 fn index_config_serde_roundtrip_with_new_fields() {
896 let toml = r#"
897 enabled = true
898 concurrency = 8
899 batch_size = 16
900 workspace_root = "/tmp/myproject"
901 "#;
902 let cfg: IndexConfig = toml::from_str(toml).unwrap();
903 assert!(cfg.enabled);
904 assert_eq!(cfg.concurrency, 8);
905 assert_eq!(cfg.batch_size, 16);
906 assert_eq!(
907 cfg.workspace_root,
908 Some(std::path::PathBuf::from("/tmp/myproject"))
909 );
910 let serialized = toml::to_string(&cfg).unwrap();
912 let cfg2: IndexConfig = toml::from_str(&serialized).unwrap();
913 assert_eq!(cfg2.concurrency, 8);
914 assert_eq!(cfg2.batch_size, 16);
915 }
916
917 #[test]
918 fn index_config_backward_compat_old_toml_without_new_fields() {
919 let toml = "
922 enabled = true
923 max_chunks = 20
924 score_threshold = 0.3
925 ";
926 let cfg: IndexConfig = toml::from_str(toml).unwrap();
927 assert!(cfg.enabled);
928 assert_eq!(cfg.max_chunks, 20);
929 assert!(cfg.workspace_root.is_none());
930 assert_eq!(cfg.concurrency, 4);
931 assert_eq!(cfg.batch_size, 32);
932 }
933
934 #[test]
935 fn index_config_workspace_root_none_by_default() {
936 let cfg: IndexConfig = toml::from_str("enabled = false").unwrap();
937 assert!(cfg.workspace_root.is_none());
938 }
939}
940
941fn default_compression_spectrum_promotion_window() -> usize {
944 200
945}
946
947fn default_compression_spectrum_min_occurrences() -> u32 {
948 3
949}
950
951fn default_compression_spectrum_min_sessions() -> u32 {
952 2
953}
954
955fn default_compression_spectrum_cluster_threshold() -> f32 {
956 0.85
957}
958
959fn default_retrieval_low_budget_ratio() -> f32 {
960 0.20
961}
962
963fn default_retrieval_mid_budget_ratio() -> f32 {
964 0.50
965}
966
967#[derive(Debug, Deserialize, Serialize)]
983pub struct CompressionSpectrumConfig {
984 #[serde(default)]
986 pub enabled: bool,
987 #[serde(default)]
989 pub promotion_output_dir: Option<String>,
990 #[serde(default)]
992 pub promotion_provider: ProviderName,
993 #[serde(default = "default_compression_spectrum_promotion_window")]
996 pub promotion_window: usize,
997 #[serde(default = "default_compression_spectrum_min_occurrences")]
1000 pub min_occurrences: u32,
1001 #[serde(default = "default_compression_spectrum_min_sessions")]
1003 pub min_sessions: u32,
1004 #[serde(default = "default_compression_spectrum_cluster_threshold")]
1006 pub cluster_threshold: f32,
1007 #[serde(default = "default_retrieval_low_budget_ratio")]
1009 pub retrieval_low_budget_ratio: f32,
1010 #[serde(default = "default_retrieval_mid_budget_ratio")]
1012 pub retrieval_mid_budget_ratio: f32,
1013}
1014
1015impl Default for CompressionSpectrumConfig {
1016 fn default() -> Self {
1017 Self {
1018 enabled: false,
1019 promotion_output_dir: None,
1020 promotion_provider: ProviderName::default(),
1021 promotion_window: default_compression_spectrum_promotion_window(),
1022 min_occurrences: default_compression_spectrum_min_occurrences(),
1023 min_sessions: default_compression_spectrum_min_sessions(),
1024 cluster_threshold: default_compression_spectrum_cluster_threshold(),
1025 retrieval_low_budget_ratio: default_retrieval_low_budget_ratio(),
1026 retrieval_mid_budget_ratio: default_retrieval_mid_budget_ratio(),
1027 }
1028 }
1029}
1030
1031fn default_trace_service_name() -> String {
1032 "zeph".into()
1033}
1034
1035#[derive(Debug, Clone, Deserialize, Serialize)]
1042#[serde(default)]
1043pub struct TraceConfig {
1044 #[serde(default = "default_otlp_endpoint")]
1047 pub otlp_endpoint: String,
1048 #[serde(default = "default_trace_service_name")]
1050 pub service_name: String,
1051 #[serde(default = "default_true")]
1053 pub redact: bool,
1054}
1055
1056impl Default for TraceConfig {
1057 fn default() -> Self {
1058 Self {
1059 otlp_endpoint: default_otlp_endpoint(),
1060 service_name: default_trace_service_name(),
1061 redact: true,
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Clone, Deserialize, Serialize)]
1079#[serde(default)]
1080pub struct DebugConfig {
1081 pub enabled: bool,
1083 #[serde(default = "crate::defaults::default_debug_output_dir")]
1085 pub output_dir: std::path::PathBuf,
1086 pub format: crate::dump_format::DumpFormat,
1088 pub traces: TraceConfig,
1090}
1091
1092impl Default for DebugConfig {
1093 fn default() -> Self {
1094 Self {
1095 enabled: false,
1096 output_dir: super::defaults::default_debug_output_dir(),
1097 format: crate::dump_format::DumpFormat::default(),
1098 traces: TraceConfig::default(),
1099 }
1100 }
1101}