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::security::TrustConfig;
9
10fn default_disambiguation_threshold() -> f32 {
11    0.20
12}
13
14fn default_min_injection_score() -> f32 {
15    0.20
16}
17
18fn default_cosine_weight() -> f32 {
19    0.7
20}
21
22fn default_hybrid_search() -> bool {
23    true
24}
25
26fn default_max_active_skills() -> usize {
27    5
28}
29
30fn default_index_watch() -> bool {
31    true
32}
33
34fn default_index_search_enabled() -> bool {
35    true
36}
37
38fn default_index_max_chunks() -> usize {
39    12
40}
41
42fn default_index_score_threshold() -> f32 {
43    0.25
44}
45
46fn default_index_budget_ratio() -> f32 {
47    0.40
48}
49
50fn default_index_repo_map_tokens() -> usize {
51    500
52}
53
54fn default_repo_map_ttl_secs() -> u64 {
55    300
56}
57
58fn default_vault_backend() -> String {
59    "env".into()
60}
61
62fn default_max_daily_cents() -> u32 {
63    0
64}
65
66fn default_otlp_endpoint() -> String {
67    "http://localhost:4317".into()
68}
69
70fn default_pid_file() -> String {
71    "~/.zeph/zeph.pid".into()
72}
73
74fn default_health_interval() -> u64 {
75    30
76}
77
78fn default_max_restart_backoff() -> u64 {
79    60
80}
81
82fn default_scheduler_tick_interval() -> u64 {
83    60
84}
85
86fn default_scheduler_max_tasks() -> usize {
87    100
88}
89
90fn default_gateway_bind() -> String {
91    "127.0.0.1".into()
92}
93
94fn default_gateway_port() -> u16 {
95    8090
96}
97
98fn default_gateway_rate_limit() -> u32 {
99    120
100}
101
102fn default_gateway_max_body() -> usize {
103    1_048_576
104}
105
106/// Controls how skills are formatted in the system prompt.
107#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
108#[serde(rename_all = "lowercase")]
109pub enum SkillPromptMode {
110    Full,
111    Compact,
112    #[default]
113    Auto,
114}
115
116#[derive(Debug, Deserialize, Serialize)]
117pub struct SkillsConfig {
118    #[serde(default = "default_skill_paths")]
119    pub paths: Vec<String>,
120    #[serde(default = "default_max_active_skills")]
121    pub max_active_skills: usize,
122    #[serde(default = "default_disambiguation_threshold")]
123    pub disambiguation_threshold: f32,
124    #[serde(default = "default_min_injection_score")]
125    pub min_injection_score: f32,
126    #[serde(default = "default_cosine_weight")]
127    pub cosine_weight: f32,
128    #[serde(default = "default_hybrid_search")]
129    pub hybrid_search: bool,
130    #[serde(default)]
131    pub learning: LearningConfig,
132    #[serde(default)]
133    pub trust: TrustConfig,
134    #[serde(default)]
135    pub prompt_mode: SkillPromptMode,
136    /// Enable two-stage category-first skill matching (requires `category` set in SKILL.md).
137    /// Falls back to flat matching when no multi-skill categories are available.
138    #[serde(default)]
139    pub two_stage_matching: bool,
140    /// Warn when any two skills have cosine similarity ≥ this threshold.
141    /// Set to 0.0 (default) to disable the confusability check entirely.
142    #[serde(default)]
143    pub confusability_threshold: f32,
144}
145
146#[derive(Debug, Deserialize, Serialize)]
147pub struct IndexConfig {
148    #[serde(default)]
149    pub enabled: bool,
150    #[serde(default = "default_index_search_enabled")]
151    pub search_enabled: bool,
152    #[serde(default = "default_index_watch")]
153    pub watch: bool,
154    #[serde(default = "default_index_max_chunks")]
155    pub max_chunks: usize,
156    #[serde(default = "default_index_score_threshold")]
157    pub score_threshold: f32,
158    #[serde(default = "default_index_budget_ratio")]
159    pub budget_ratio: f32,
160    #[serde(default = "default_index_repo_map_tokens")]
161    pub repo_map_tokens: usize,
162    #[serde(default = "default_repo_map_ttl_secs")]
163    pub repo_map_ttl_secs: u64,
164}
165
166impl Default for IndexConfig {
167    fn default() -> Self {
168        Self {
169            enabled: false,
170            search_enabled: default_index_search_enabled(),
171            watch: default_index_watch(),
172            max_chunks: default_index_max_chunks(),
173            score_threshold: default_index_score_threshold(),
174            budget_ratio: default_index_budget_ratio(),
175            repo_map_tokens: default_index_repo_map_tokens(),
176            repo_map_ttl_secs: default_repo_map_ttl_secs(),
177        }
178    }
179}
180
181#[derive(Debug, Deserialize, Serialize)]
182pub struct VaultConfig {
183    #[serde(default = "default_vault_backend")]
184    pub backend: String,
185}
186
187impl Default for VaultConfig {
188    fn default() -> Self {
189        Self {
190            backend: default_vault_backend(),
191        }
192    }
193}
194
195#[derive(Debug, Deserialize, Serialize)]
196pub struct CostConfig {
197    #[serde(default = "default_true")]
198    pub enabled: bool,
199    #[serde(default = "default_max_daily_cents")]
200    pub max_daily_cents: u32,
201}
202
203impl Default for CostConfig {
204    fn default() -> Self {
205        Self {
206            enabled: true,
207            max_daily_cents: default_max_daily_cents(),
208        }
209    }
210}
211
212#[derive(Debug, Deserialize, Serialize)]
213pub struct ObservabilityConfig {
214    #[serde(default)]
215    pub exporter: String,
216    #[serde(default = "default_otlp_endpoint")]
217    pub endpoint: String,
218}
219
220impl Default for ObservabilityConfig {
221    fn default() -> Self {
222        Self {
223            exporter: String::new(),
224            endpoint: default_otlp_endpoint(),
225        }
226    }
227}
228
229#[derive(Debug, Clone, Deserialize, Serialize)]
230pub struct GatewayConfig {
231    #[serde(default)]
232    pub enabled: bool,
233    #[serde(default = "default_gateway_bind")]
234    pub bind: String,
235    #[serde(default = "default_gateway_port")]
236    pub port: u16,
237    #[serde(default)]
238    pub auth_token: Option<String>,
239    #[serde(default = "default_gateway_rate_limit")]
240    pub rate_limit: u32,
241    #[serde(default = "default_gateway_max_body")]
242    pub max_body_size: usize,
243}
244
245impl Default for GatewayConfig {
246    fn default() -> Self {
247        Self {
248            enabled: false,
249            bind: default_gateway_bind(),
250            port: default_gateway_port(),
251            auth_token: None,
252            rate_limit: default_gateway_rate_limit(),
253            max_body_size: default_gateway_max_body(),
254        }
255    }
256}
257
258#[derive(Debug, Clone, Deserialize, Serialize)]
259pub struct DaemonConfig {
260    #[serde(default)]
261    pub enabled: bool,
262    #[serde(default = "default_pid_file")]
263    pub pid_file: String,
264    #[serde(default = "default_health_interval")]
265    pub health_interval_secs: u64,
266    #[serde(default = "default_max_restart_backoff")]
267    pub max_restart_backoff_secs: u64,
268}
269
270impl Default for DaemonConfig {
271    fn default() -> Self {
272        Self {
273            enabled: false,
274            pid_file: default_pid_file(),
275            health_interval_secs: default_health_interval(),
276            max_restart_backoff_secs: default_max_restart_backoff(),
277        }
278    }
279}
280
281#[derive(Debug, Clone, Deserialize, Serialize)]
282pub struct SchedulerConfig {
283    #[serde(default)]
284    pub enabled: bool,
285    #[serde(default = "default_scheduler_tick_interval")]
286    pub tick_interval_secs: u64,
287    #[serde(default = "default_scheduler_max_tasks")]
288    pub max_tasks: usize,
289    #[serde(default)]
290    pub tasks: Vec<ScheduledTaskConfig>,
291}
292
293impl Default for SchedulerConfig {
294    fn default() -> Self {
295        Self {
296            enabled: true,
297            tick_interval_secs: default_scheduler_tick_interval(),
298            max_tasks: default_scheduler_max_tasks(),
299            tasks: Vec::new(),
300        }
301    }
302}
303
304/// Task kind for scheduled tasks.
305///
306/// Known variants map to built-in handlers; `Custom` accommodates user-defined task types.
307#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
308#[serde(rename_all = "snake_case")]
309pub enum ScheduledTaskKind {
310    MemoryCleanup,
311    SkillRefresh,
312    HealthCheck,
313    UpdateCheck,
314    Experiment,
315    Custom(String),
316}
317
318#[derive(Debug, Clone, Deserialize, Serialize)]
319pub struct ScheduledTaskConfig {
320    pub name: String,
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub cron: Option<String>,
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub run_at: Option<String>,
325    pub kind: ScheduledTaskKind,
326    #[serde(default)]
327    pub config: serde_json::Value,
328}
329
330fn default_trace_service_name() -> String {
331    "zeph".into()
332}
333
334/// Configuration for OTel-compatible trace dumps (`format = "trace"`).
335///
336/// When `format = "trace"`, the `TracingCollector` writes a `trace.json` file in OTLP JSON
337/// format at session end. Legacy numbered dump files are NOT written by default (C-03).
338/// When the `otel` feature is enabled and `otlp_endpoint` is set, spans are also exported
339/// via OTLP gRPC.
340#[derive(Debug, Clone, Deserialize, Serialize)]
341#[serde(default)]
342pub struct TraceConfig {
343    /// OTLP gRPC endpoint (only used when `otel` feature is enabled).
344    /// Defaults to `observability.endpoint` if unset (I-01).
345    #[serde(default = "default_otlp_endpoint")]
346    pub otlp_endpoint: String,
347    /// Service name reported to the `OTel` collector.
348    #[serde(default = "default_trace_service_name")]
349    pub service_name: String,
350    /// Redact sensitive data in span attributes (default: `true`) (C-01).
351    #[serde(default = "default_true")]
352    pub redact: bool,
353}
354
355impl Default for TraceConfig {
356    fn default() -> Self {
357        Self {
358            otlp_endpoint: default_otlp_endpoint(),
359            service_name: default_trace_service_name(),
360            redact: true,
361        }
362    }
363}
364
365#[derive(Debug, Clone, Deserialize, Serialize)]
366#[serde(default)]
367pub struct DebugConfig {
368    /// Enable debug dump on startup (CLI `--debug-dump` takes priority).
369    pub enabled: bool,
370    /// Directory where per-session debug dump subdirectories are created.
371    #[serde(default = "crate::defaults::default_debug_output_dir")]
372    pub output_dir: std::path::PathBuf,
373    /// Output format: `"json"` (default), `"raw"` (API payload), or `"trace"` (OTLP spans).
374    pub format: crate::dump_format::DumpFormat,
375    /// `OTel` trace configuration (only used when `format = "trace"`).
376    pub traces: TraceConfig,
377}
378
379impl Default for DebugConfig {
380    fn default() -> Self {
381        Self {
382            enabled: false,
383            output_dir: super::defaults::default_debug_output_dir(),
384            format: crate::dump_format::DumpFormat::default(),
385            traces: TraceConfig::default(),
386        }
387    }
388}