Skip to main content

zeph_config/
features.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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    true
49}
50
51fn default_index_search_enabled() -> bool {
52    true
53}
54
55fn default_index_max_chunks() -> usize {
56    12
57}
58
59fn default_index_score_threshold() -> f32 {
60    0.25
61}
62
63fn default_index_budget_ratio() -> f32 {
64    0.40
65}
66
67fn default_index_repo_map_tokens() -> usize {
68    500
69}
70
71fn default_repo_map_ttl_secs() -> u64 {
72    300
73}
74
75fn default_vault_backend() -> String {
76    "env".into()
77}
78
79fn default_max_daily_cents() -> u32 {
80    0
81}
82
83fn default_otlp_endpoint() -> String {
84    "http://localhost:4317".into()
85}
86
87fn default_pid_file() -> String {
88    "~/.zeph/zeph.pid".into()
89}
90
91fn default_health_interval() -> u64 {
92    30
93}
94
95fn default_max_restart_backoff() -> u64 {
96    60
97}
98
99fn default_scheduler_tick_interval() -> u64 {
100    60
101}
102
103fn default_scheduler_max_tasks() -> usize {
104    100
105}
106
107fn default_gateway_bind() -> String {
108    "127.0.0.1".into()
109}
110
111fn default_gateway_port() -> u16 {
112    8090
113}
114
115fn default_gateway_rate_limit() -> u32 {
116    120
117}
118
119fn default_gateway_max_body() -> usize {
120    1_048_576
121}
122
123/// Controls how skills are formatted in the system prompt.
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
125#[serde(rename_all = "lowercase")]
126pub enum SkillPromptMode {
127    Full,
128    Compact,
129    #[default]
130    Auto,
131}
132
133#[derive(Debug, Deserialize, Serialize)]
134pub struct SkillsConfig {
135    #[serde(default = "default_skill_paths")]
136    pub paths: Vec<String>,
137    #[serde(default = "default_max_active_skills")]
138    pub max_active_skills: usize,
139    #[serde(default = "default_disambiguation_threshold")]
140    pub disambiguation_threshold: f32,
141    #[serde(default = "default_min_injection_score")]
142    pub min_injection_score: f32,
143    #[serde(default = "default_cosine_weight")]
144    pub cosine_weight: f32,
145    #[serde(default = "default_hybrid_search")]
146    pub hybrid_search: bool,
147    #[serde(default)]
148    pub learning: LearningConfig,
149    #[serde(default)]
150    pub trust: TrustConfig,
151    #[serde(default)]
152    pub prompt_mode: SkillPromptMode,
153    /// Enable two-stage category-first skill matching (requires `category` set in SKILL.md).
154    /// Falls back to flat matching when no multi-skill categories are available.
155    #[serde(default)]
156    pub two_stage_matching: bool,
157    /// Warn when any two skills have cosine similarity ≥ this threshold.
158    /// Set to 0.0 (default) to disable the confusability check entirely.
159    #[serde(default)]
160    pub confusability_threshold: f32,
161
162    // --- SkillOrchestra: RL routing head ---
163    /// Enable RL routing head for skill re-ranking (disabled by default).
164    #[serde(default)]
165    pub rl_routing_enabled: bool,
166    /// Learning rate for REINFORCE weight updates.
167    #[serde(default = "default_rl_learning_rate")]
168    pub rl_learning_rate: f32,
169    /// Blend weight: `final_score = (1-rl_weight)*cosine + rl_weight*rl_score`.
170    #[serde(default = "default_rl_weight")]
171    pub rl_weight: f32,
172    /// Persist weights every N updates (0 = persist every update).
173    #[serde(default = "default_rl_persist_interval")]
174    pub rl_persist_interval: u32,
175    /// Skip RL blending for the first N updates (cold-start warmup).
176    #[serde(default = "default_rl_warmup_updates")]
177    pub rl_warmup_updates: u32,
178    /// Embedding dimension for the RL routing head.
179    /// Must match the output dimension of the configured embedding provider.
180    /// Defaults to `None` → 1536 (`text-embedding-3-small` output dimension).
181    #[serde(default)]
182    pub rl_embed_dim: Option<usize>,
183
184    // --- NL skill generation ---
185    /// Provider name for `/skill create` NL generation. Empty = primary provider.
186    #[serde(default)]
187    pub generation_provider: ProviderName,
188    /// Directory where generated skills are written. Defaults to first entry in `paths`.
189    #[serde(default)]
190    pub generation_output_dir: Option<String>,
191    /// Skill mining configuration.
192    #[serde(default)]
193    pub mining: SkillMiningConfig,
194}
195
196fn default_max_repos_per_query() -> usize {
197    20
198}
199
200fn default_dedup_threshold() -> f32 {
201    0.85
202}
203
204fn default_rate_limit_rpm() -> u32 {
205    25
206}
207
208/// Configuration for the automated skill mining pipeline (`zeph-skills-miner` binary).
209#[derive(Debug, Default, Deserialize, Serialize)]
210pub struct SkillMiningConfig {
211    /// GitHub search queries for repo discovery (e.g. "topic:cli-tool language:rust stars:>100").
212    #[serde(default)]
213    pub queries: Vec<String>,
214    /// Maximum repos to fetch per query (capped at 100 by GitHub API). Default: 20.
215    #[serde(default = "default_max_repos_per_query")]
216    pub max_repos_per_query: usize,
217    /// Cosine similarity threshold for dedup against existing skills. Default: 0.85.
218    #[serde(default = "default_dedup_threshold")]
219    pub dedup_threshold: f32,
220    /// Output directory for mined skills.
221    #[serde(default)]
222    pub output_dir: Option<String>,
223    /// Provider name for skill generation during mining. Empty = primary provider.
224    #[serde(default)]
225    pub generation_provider: ProviderName,
226    /// Provider name for embedding during dedup. Empty = primary provider.
227    #[serde(default)]
228    pub embedding_provider: ProviderName,
229    /// Maximum GitHub search requests per minute. Default: 25.
230    #[serde(default = "default_rate_limit_rpm")]
231    pub rate_limit_rpm: u32,
232}
233
234#[derive(Debug, Deserialize, Serialize)]
235#[allow(clippy::struct_excessive_bools)]
236pub struct IndexConfig {
237    #[serde(default)]
238    pub enabled: bool,
239    #[serde(default = "default_index_search_enabled")]
240    pub search_enabled: bool,
241    #[serde(default = "default_index_watch")]
242    pub watch: bool,
243    #[serde(default = "default_index_max_chunks")]
244    pub max_chunks: usize,
245    #[serde(default = "default_index_score_threshold")]
246    pub score_threshold: f32,
247    #[serde(default = "default_index_budget_ratio")]
248    pub budget_ratio: f32,
249    #[serde(default = "default_index_repo_map_tokens")]
250    pub repo_map_tokens: usize,
251    #[serde(default = "default_repo_map_ttl_secs")]
252    pub repo_map_ttl_secs: u64,
253    /// Enable `IndexMcpServer` tools (`symbol_definition`, `find_text_references`, `call_graph`,
254    /// `module_summary`). When `true`, static repo-map injection is skipped and the LLM
255    /// uses on-demand tool calls instead.
256    #[serde(default)]
257    pub mcp_enabled: bool,
258}
259
260impl Default for IndexConfig {
261    fn default() -> Self {
262        Self {
263            enabled: false,
264            search_enabled: default_index_search_enabled(),
265            watch: default_index_watch(),
266            max_chunks: default_index_max_chunks(),
267            score_threshold: default_index_score_threshold(),
268            budget_ratio: default_index_budget_ratio(),
269            repo_map_tokens: default_index_repo_map_tokens(),
270            repo_map_ttl_secs: default_repo_map_ttl_secs(),
271            mcp_enabled: false,
272        }
273    }
274}
275
276#[derive(Debug, Deserialize, Serialize)]
277pub struct VaultConfig {
278    #[serde(default = "default_vault_backend")]
279    pub backend: String,
280}
281
282impl Default for VaultConfig {
283    fn default() -> Self {
284        Self {
285            backend: default_vault_backend(),
286        }
287    }
288}
289
290#[derive(Debug, Deserialize, Serialize)]
291pub struct CostConfig {
292    #[serde(default = "default_true")]
293    pub enabled: bool,
294    #[serde(default = "default_max_daily_cents")]
295    pub max_daily_cents: u32,
296}
297
298impl Default for CostConfig {
299    fn default() -> Self {
300        Self {
301            enabled: true,
302            max_daily_cents: default_max_daily_cents(),
303        }
304    }
305}
306
307#[derive(Debug, Deserialize, Serialize)]
308pub struct ObservabilityConfig {
309    #[serde(default)]
310    pub exporter: String,
311    #[serde(default = "default_otlp_endpoint")]
312    pub endpoint: String,
313}
314
315impl Default for ObservabilityConfig {
316    fn default() -> Self {
317        Self {
318            exporter: String::new(),
319            endpoint: default_otlp_endpoint(),
320        }
321    }
322}
323
324#[derive(Debug, Clone, Deserialize, Serialize)]
325pub struct GatewayConfig {
326    #[serde(default)]
327    pub enabled: bool,
328    #[serde(default = "default_gateway_bind")]
329    pub bind: String,
330    #[serde(default = "default_gateway_port")]
331    pub port: u16,
332    #[serde(default)]
333    pub auth_token: Option<String>,
334    #[serde(default = "default_gateway_rate_limit")]
335    pub rate_limit: u32,
336    #[serde(default = "default_gateway_max_body")]
337    pub max_body_size: usize,
338}
339
340impl Default for GatewayConfig {
341    fn default() -> Self {
342        Self {
343            enabled: false,
344            bind: default_gateway_bind(),
345            port: default_gateway_port(),
346            auth_token: None,
347            rate_limit: default_gateway_rate_limit(),
348            max_body_size: default_gateway_max_body(),
349        }
350    }
351}
352
353#[derive(Debug, Clone, Deserialize, Serialize)]
354pub struct DaemonConfig {
355    #[serde(default)]
356    pub enabled: bool,
357    #[serde(default = "default_pid_file")]
358    pub pid_file: String,
359    #[serde(default = "default_health_interval")]
360    pub health_interval_secs: u64,
361    #[serde(default = "default_max_restart_backoff")]
362    pub max_restart_backoff_secs: u64,
363}
364
365impl Default for DaemonConfig {
366    fn default() -> Self {
367        Self {
368            enabled: false,
369            pid_file: default_pid_file(),
370            health_interval_secs: default_health_interval(),
371            max_restart_backoff_secs: default_max_restart_backoff(),
372        }
373    }
374}
375
376#[derive(Debug, Clone, Deserialize, Serialize)]
377pub struct SchedulerConfig {
378    #[serde(default)]
379    pub enabled: bool,
380    #[serde(default = "default_scheduler_tick_interval")]
381    pub tick_interval_secs: u64,
382    #[serde(default = "default_scheduler_max_tasks")]
383    pub max_tasks: usize,
384    #[serde(default)]
385    pub tasks: Vec<ScheduledTaskConfig>,
386}
387
388impl Default for SchedulerConfig {
389    fn default() -> Self {
390        Self {
391            enabled: true,
392            tick_interval_secs: default_scheduler_tick_interval(),
393            max_tasks: default_scheduler_max_tasks(),
394            tasks: Vec::new(),
395        }
396    }
397}
398
399/// Task kind for scheduled tasks.
400///
401/// Known variants map to built-in handlers; `Custom` accommodates user-defined task types.
402#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
403#[serde(rename_all = "snake_case")]
404pub enum ScheduledTaskKind {
405    MemoryCleanup,
406    SkillRefresh,
407    HealthCheck,
408    UpdateCheck,
409    Experiment,
410    Custom(String),
411}
412
413#[derive(Debug, Clone, Deserialize, Serialize)]
414pub struct ScheduledTaskConfig {
415    pub name: String,
416    #[serde(default, skip_serializing_if = "Option::is_none")]
417    pub cron: Option<String>,
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    pub run_at: Option<String>,
420    pub kind: ScheduledTaskKind,
421    #[serde(default)]
422    pub config: serde_json::Value,
423}
424
425fn default_trace_service_name() -> String {
426    "zeph".into()
427}
428
429/// Configuration for OTel-compatible trace dumps (`format = "trace"`).
430///
431/// When `format = "trace"`, the `TracingCollector` writes a `trace.json` file in OTLP JSON
432/// format at session end. Legacy numbered dump files are NOT written by default (C-03).
433/// When the `otel` feature is enabled and `otlp_endpoint` is set, spans are also exported
434/// via OTLP gRPC.
435#[derive(Debug, Clone, Deserialize, Serialize)]
436#[serde(default)]
437pub struct TraceConfig {
438    /// OTLP gRPC endpoint (only used when `otel` feature is enabled).
439    /// Defaults to `observability.endpoint` if unset (I-01).
440    #[serde(default = "default_otlp_endpoint")]
441    pub otlp_endpoint: String,
442    /// Service name reported to the `OTel` collector.
443    #[serde(default = "default_trace_service_name")]
444    pub service_name: String,
445    /// Redact sensitive data in span attributes (default: `true`) (C-01).
446    #[serde(default = "default_true")]
447    pub redact: bool,
448}
449
450impl Default for TraceConfig {
451    fn default() -> Self {
452        Self {
453            otlp_endpoint: default_otlp_endpoint(),
454            service_name: default_trace_service_name(),
455            redact: true,
456        }
457    }
458}
459
460#[derive(Debug, Clone, Deserialize, Serialize)]
461#[serde(default)]
462pub struct DebugConfig {
463    /// Enable debug dump on startup (CLI `--debug-dump` takes priority).
464    pub enabled: bool,
465    /// Directory where per-session debug dump subdirectories are created.
466    #[serde(default = "crate::defaults::default_debug_output_dir")]
467    pub output_dir: std::path::PathBuf,
468    /// Output format: `"json"` (default), `"raw"` (API payload), or `"trace"` (OTLP spans).
469    pub format: crate::dump_format::DumpFormat,
470    /// `OTel` trace configuration (only used when `format = "trace"`).
471    pub traces: TraceConfig,
472}
473
474impl Default for DebugConfig {
475    fn default() -> Self {
476        Self {
477            enabled: false,
478            output_dir: super::defaults::default_debug_output_dir(),
479            format: crate::dump_format::DumpFormat::default(),
480            traces: TraceConfig::default(),
481        }
482    }
483}