Skip to main content

zeroclaw_config/
scattered_types.rs

1//! Config types that were originally defined in their home modules (agent, channels, tools, trust)
2//! but are needed by the config schema. Moved here to break circular dependencies.
3
4use crate::traits::{ChannelConfig, HasPropKind, PropKind};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fmt;
8use zeroclaw_macros::Configurable;
9
10// ── Agent config types ──────────────────────────────────────────
11
12/// How deeply the model should reason for a given message.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
15#[serde(rename_all = "lowercase")]
16pub enum ThinkingLevel {
17    Off,
18    Minimal,
19    Low,
20    #[default]
21    Medium,
22    High,
23    Max,
24}
25
26impl HasPropKind for ThinkingLevel {
27    const PROP_KIND: PropKind = PropKind::Enum;
28}
29
30impl ThinkingLevel {
31    pub fn from_str_insensitive(s: &str) -> Option<Self> {
32        match s.to_lowercase().as_str() {
33            "off" | "none" => Some(Self::Off),
34            "minimal" | "min" => Some(Self::Minimal),
35            "low" => Some(Self::Low),
36            "medium" | "med" | "default" => Some(Self::Medium),
37            "high" => Some(Self::High),
38            "max" | "maximum" => Some(Self::Max),
39            _ => None,
40        }
41    }
42
43    pub fn as_str(&self) -> &'static str {
44        match self {
45            Self::Off => "off",
46            Self::Minimal => "minimal",
47            Self::Low => "low",
48            Self::Medium => "medium",
49            Self::High => "high",
50            Self::Max => "max",
51        }
52    }
53
54    pub fn default_budget_tokens(&self) -> Option<u32> {
55        match self {
56            Self::Off | Self::Minimal | Self::Low | Self::Medium => None,
57            Self::High => Some(10_000),
58            Self::Max => Some(50_000),
59        }
60    }
61}
62
63pub use zeroclaw_api::model_provider::{
64    MAX_BUDGET_TOKENS, MIN_BUDGET_TOKENS, NativeThinkingParams,
65};
66
67/// Configuration for thinking/reasoning level control.
68#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
69#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
70#[prefix = "agent.thinking"]
71pub struct ThinkingConfig {
72    #[serde(default)]
73    pub default_level: ThinkingLevel,
74    /// Opt-in flag for provider-native extended thinking. When `true`, the
75    /// provider sends a dedicated `thinking` parameter with `budget_tokens`
76    /// instead of relying solely on prompt-based reasoning. Defaults to
77    /// `false` so existing High/Max users keep their prior prompt-based
78    /// behavior (cost, latency, transport path) until they explicitly migrate.
79    #[serde(default)]
80    pub native_thinking: bool,
81    #[serde(default)]
82    pub budget_tokens: HashMap<String, u32>,
83}
84
85impl Default for ThinkingConfig {
86    fn default() -> Self {
87        Self {
88            default_level: ThinkingLevel::Medium,
89            native_thinking: false,
90            budget_tokens: HashMap::new(),
91        }
92    }
93}
94
95impl ThinkingConfig {
96    /// Resolve the effective `budget_tokens` for a given level.
97    ///
98    /// Only levels with a built-in default (`High`, `Max`) are eligible for
99    /// native thinking. Config overrides for levels Off–Medium are ignored
100    /// to prevent accidentally forcing `temperature = 1.0` on low levels.
101    pub fn budget_tokens_for(&self, level: ThinkingLevel) -> Option<u32> {
102        // Guard: only levels that have a built-in budget can use native thinking.
103        let default = level.default_budget_tokens()?;
104        Some(
105            self.budget_tokens
106                .get(level.as_str())
107                .copied()
108                .unwrap_or(default),
109        )
110    }
111
112    pub fn warn_unknown_budget_keys(&self) {
113        use ThinkingLevel::{High, Low, Max, Medium, Minimal, Off};
114        const ALL_LEVELS: &[ThinkingLevel] = &[Off, Minimal, Low, Medium, High, Max];
115        for key in self.budget_tokens.keys() {
116            if !ALL_LEVELS.iter().any(|l| l.as_str() == key) {
117                ::zeroclaw_log::record!(
118                    WARN,
119                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
120                        .with_attrs(::serde_json::json!({"key": key})),
121                    "Unknown thinking level in budget_tokens config; \
122                     valid levels are: off, minimal, low, medium, high, max"
123                );
124            }
125        }
126    }
127}
128
129fn default_max_tokens() -> usize {
130    8192
131}
132fn default_keep_recent() -> usize {
133    4
134}
135fn default_collapse() -> bool {
136    true
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
140#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
141#[prefix = "agent.history-pruning"]
142pub struct HistoryPrunerConfig {
143    #[serde(default)]
144    pub enabled: bool,
145    #[serde(default = "default_max_tokens")]
146    pub max_tokens: usize,
147    #[serde(default = "default_keep_recent")]
148    pub keep_recent: usize,
149    #[serde(default = "default_collapse")]
150    pub collapse_tool_results: bool,
151}
152
153impl Default for HistoryPrunerConfig {
154    fn default() -> Self {
155        Self {
156            enabled: false,
157            max_tokens: 8192,
158            keep_recent: 4,
159            collapse_tool_results: true,
160        }
161    }
162}
163
164fn default_cost_optimized_hint() -> String {
165    "cost-optimized".to_string()
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
169#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
170#[prefix = "agent.auto-classify"]
171pub struct AutoClassifyConfig {
172    #[serde(default)]
173    pub simple_hint: Option<String>,
174    #[serde(default)]
175    pub standard_hint: Option<String>,
176    #[serde(default)]
177    pub complex_hint: Option<String>,
178    #[serde(default = "default_cost_optimized_hint")]
179    pub cost_optimized_hint: String,
180}
181
182impl Default for AutoClassifyConfig {
183    fn default() -> Self {
184        Self {
185            simple_hint: None,
186            standard_hint: None,
187            complex_hint: None,
188            cost_optimized_hint: default_cost_optimized_hint(),
189        }
190    }
191}
192
193fn default_min_quality_score() -> f64 {
194    0.5
195}
196fn default_eval_max_retries() -> u32 {
197    1
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
201#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
202#[prefix = "agent.eval"]
203pub struct EvalConfig {
204    #[serde(default)]
205    pub enabled: bool,
206    #[serde(default = "default_min_quality_score")]
207    pub min_quality_score: f64,
208    #[serde(default = "default_eval_max_retries")]
209    pub max_retries: u32,
210}
211
212impl Default for EvalConfig {
213    fn default() -> Self {
214        Self {
215            enabled: false,
216            min_quality_score: default_min_quality_score(),
217            max_retries: default_eval_max_retries(),
218        }
219    }
220}
221
222fn default_cc_enabled() -> bool {
223    true
224}
225fn default_threshold_ratio() -> f64 {
226    0.50
227}
228fn default_protect_first_n() -> usize {
229    3
230}
231fn default_protect_last_n() -> usize {
232    4
233}
234fn default_cc_max_passes() -> u32 {
235    3
236}
237fn default_summary_max_chars() -> usize {
238    4000
239}
240fn default_source_max_chars() -> usize {
241    50_000
242}
243fn default_cc_timeout_secs() -> u64 {
244    60
245}
246fn default_identifier_policy() -> String {
247    "strict".to_string()
248}
249fn default_tool_result_retrim_chars() -> usize {
250    2_000
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
254#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
255#[prefix = "agent.context-compression"]
256pub struct ContextCompressionConfig {
257    #[serde(default = "default_cc_enabled")]
258    pub enabled: bool,
259    #[serde(default = "default_threshold_ratio")]
260    pub threshold_ratio: f64,
261    #[serde(default = "default_protect_first_n")]
262    pub protect_first_n: usize,
263    #[serde(default = "default_protect_last_n")]
264    pub protect_last_n: usize,
265    #[serde(default = "default_cc_max_passes")]
266    pub max_passes: u32,
267    #[serde(default = "default_summary_max_chars")]
268    pub summary_max_chars: usize,
269    #[serde(default = "default_source_max_chars")]
270    pub source_max_chars: usize,
271    #[serde(default = "default_cc_timeout_secs")]
272    pub timeout_secs: u64,
273    #[serde(default)]
274    pub summary_model: Option<String>,
275    #[serde(default = "default_identifier_policy")]
276    pub identifier_policy: String,
277    #[serde(default = "default_tool_result_retrim_chars")]
278    pub tool_result_retrim_chars: usize,
279    #[serde(default)]
280    pub tool_result_trim_exempt: Vec<String>,
281}
282
283impl Default for ContextCompressionConfig {
284    fn default() -> Self {
285        Self {
286            enabled: default_cc_enabled(),
287            threshold_ratio: default_threshold_ratio(),
288            protect_first_n: default_protect_first_n(),
289            protect_last_n: default_protect_last_n(),
290            max_passes: default_cc_max_passes(),
291            summary_max_chars: default_summary_max_chars(),
292            source_max_chars: default_source_max_chars(),
293            timeout_secs: default_cc_timeout_secs(),
294            summary_model: None,
295            identifier_policy: default_identifier_policy(),
296            tool_result_retrim_chars: default_tool_result_retrim_chars(),
297            tool_result_trim_exempt: Vec::new(),
298        }
299    }
300}
301
302fn default_precheck_enabled() -> bool {
303    true
304}
305fn default_precheck_timeout_secs() -> u64 {
306    5
307}
308
309/// Channel reply-intent precheck configuration.
310///
311/// The precheck runs a lightweight `REPLY` / `NO_REPLY` classifier before the
312/// main agent loop so group-chat messages that are not addressed to the
313/// assistant do not trigger a full tool-using turn.
314///
315/// In V3 multi-agent configs this block is configured inside each agent as
316/// `[agents.<alias>.precheck]`. Defaults preserve the current behavior: the
317/// classifier is enabled, model/provider selection follows the agent's
318/// `classifier_provider` ref when configured and otherwise reuses the active
319/// route model, and provider errors or timeouts fail open to REPLY.
320/// `timeout_secs` must be greater than zero.
321#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
322#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
323#[prefix = "agent.precheck"]
324pub struct ChannelPrecheckConfig {
325    /// When false, the precheck is skipped entirely for this agent and every
326    /// accepted channel message triggers the full agent loop. Default: `true`.
327    #[serde(default = "default_precheck_enabled")]
328    pub enabled: bool,
329    /// Hard ceiling (seconds) on the precheck LLM call. On timeout the
330    /// precheck fails open to REPLY. Default: `5`.
331    #[serde(default = "default_precheck_timeout_secs")]
332    pub timeout_secs: u64,
333}
334
335impl Default for ChannelPrecheckConfig {
336    fn default() -> Self {
337        Self {
338            enabled: default_precheck_enabled(),
339            timeout_secs: default_precheck_timeout_secs(),
340        }
341    }
342}
343
344// ── Tools config types ──────────────────────────────────────────
345
346fn default_browser_cli() -> String {
347    "claude".into()
348}
349fn default_browser_task_timeout() -> u64 {
350    120
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
354#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
355#[prefix = "browser-delegate"]
356pub struct BrowserDelegateConfig {
357    #[serde(default)]
358    pub enabled: bool,
359    #[serde(default = "default_browser_cli")]
360    pub cli_binary: String,
361    #[serde(default)]
362    pub chrome_profile_dir: String,
363    #[serde(default)]
364    pub allowed_domains: Vec<String>,
365    #[serde(default)]
366    pub blocked_domains: Vec<String>,
367    #[serde(default = "default_browser_task_timeout")]
368    pub task_timeout_secs: u64,
369}
370
371impl Default for BrowserDelegateConfig {
372    fn default() -> Self {
373        Self {
374            enabled: false,
375            cli_binary: default_browser_cli(),
376            chrome_profile_dir: String::new(),
377            allowed_domains: Vec::new(),
378            blocked_domains: Vec::new(),
379            task_timeout_secs: default_browser_task_timeout(),
380        }
381    }
382}
383
384// ── Trust config types ──────────────────────────────────────────
385
386fn default_initial_score() -> f64 {
387    0.8
388}
389fn default_decay_half_life() -> f64 {
390    30.0
391}
392fn default_regression_threshold() -> f64 {
393    0.5
394}
395fn default_correction_penalty() -> f64 {
396    0.05
397}
398fn default_success_boost() -> f64 {
399    0.01
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
403#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
404#[prefix = "trust"]
405pub struct TrustConfig {
406    #[serde(default = "default_initial_score")]
407    pub initial_score: f64,
408    #[serde(default = "default_decay_half_life")]
409    pub decay_half_life_days: f64,
410    #[serde(default = "default_regression_threshold")]
411    pub regression_threshold: f64,
412    #[serde(default = "default_correction_penalty")]
413    pub correction_penalty: f64,
414    #[serde(default = "default_success_boost")]
415    pub success_boost: f64,
416}
417
418impl Default for TrustConfig {
419    fn default() -> Self {
420        Self {
421            initial_score: default_initial_score(),
422            decay_half_life_days: default_decay_half_life(),
423            regression_threshold: default_regression_threshold(),
424            correction_penalty: default_correction_penalty(),
425            success_boost: default_success_boost(),
426        }
427    }
428}
429
430// ── Channel config types ────────────────────────────────────────
431
432fn default_imap_port() -> u16 {
433    993
434}
435fn default_smtp_port() -> u16 {
436    465
437}
438fn default_imap_folder() -> String {
439    "INBOX".into()
440}
441fn default_idle_timeout() -> u64 {
442    1740
443}
444fn default_poll_interval_secs() -> u64 {
445    60
446}
447fn default_true() -> bool {
448    true
449}
450fn default_subject() -> String {
451    "Re: Message".into()
452}
453fn default_max_attachment_bytes() -> usize {
454    25 * 1024 * 1024
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, zeroclaw_macros::Configurable)]
458#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
459#[prefix = "channels.email"]
460pub struct EmailConfig {
461    /// Whether this channel is active. The runtime only loads channels whose
462    /// `enabled = true`. Default: `false` so an operator who pastes a partial
463    /// `[channels.<type>.<alias>]` block doesn't accidentally bring a channel
464    /// live before the rest of its config is filled in.
465    #[serde(default)]
466    pub enabled: bool,
467    pub imap_host: String,
468    #[serde(default = "default_imap_port")]
469    pub imap_port: u16,
470    #[serde(default = "default_imap_folder")]
471    pub imap_folder: String,
472    pub smtp_host: String,
473    #[serde(default = "default_smtp_port")]
474    pub smtp_port: u16,
475    #[serde(default = "default_true")]
476    pub smtp_tls: bool,
477    #[serde(default)]
478    pub smtp_username: Option<String>,
479    #[secret]
480    #[serde(default)]
481    pub smtp_password: Option<String>,
482    pub username: String,
483    #[secret]
484    pub password: String,
485    pub from_address: String,
486    #[serde(default = "default_idle_timeout")]
487    pub idle_timeout_secs: u64,
488    /// Polling interval used when the IMAP server does not advertise the IDLE
489    /// capability (RFC 2177). Ignored when IDLE is available.
490    #[serde(default = "default_poll_interval_secs")]
491    pub poll_interval_secs: u64,
492    #[serde(default = "default_subject")]
493    pub default_subject: String,
494    #[serde(default = "default_max_attachment_bytes")]
495    pub max_attachment_bytes: usize,
496
497    /// Tools excluded from this channel's tool spec. When set, these tools
498    /// are not exposed to the model when responding via this channel.
499    #[serde(default)]
500    pub excluded_tools: Vec<String>,
501    /// When `true` (default), outbound emails are rendered as HTML via Markdown conversion.
502    /// Set to `false` to send plain-text emails instead.
503    #[serde(default = "default_true")]
504    pub html_body: bool,
505}
506
507impl ChannelConfig for EmailConfig {
508    fn name() -> &'static str {
509        "Email"
510    }
511    fn desc() -> &'static str {
512        "Email over IMAP/SMTP"
513    }
514}
515
516impl Default for EmailConfig {
517    fn default() -> Self {
518        Self {
519            enabled: false,
520            imap_host: String::new(),
521            imap_port: default_imap_port(),
522            imap_folder: default_imap_folder(),
523            smtp_host: String::new(),
524            smtp_port: default_smtp_port(),
525            smtp_tls: true,
526            smtp_username: None,
527            smtp_password: None,
528            username: String::new(),
529            password: String::new(),
530            from_address: String::new(),
531            idle_timeout_secs: default_idle_timeout(),
532            poll_interval_secs: default_poll_interval_secs(),
533            default_subject: default_subject(),
534            max_attachment_bytes: default_max_attachment_bytes(),
535            excluded_tools: Vec::new(),
536            html_body: true,
537        }
538    }
539}
540
541fn default_label_filter() -> Vec<String> {
542    vec!["INBOX".into()]
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, zeroclaw_macros::Configurable)]
546#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
547#[prefix = "channels.gmail"]
548pub struct GmailPushConfig {
549    /// Whether this channel is active. The runtime only loads channels whose
550    /// `enabled = true`. Default: `false` so an operator who pastes a partial
551    /// `[channels.<type>.<alias>]` block doesn't accidentally bring a channel
552    /// live before the rest of its config is filled in.
553    #[serde(default)]
554    pub enabled: bool,
555    pub topic: String,
556    #[serde(default = "default_label_filter")]
557    pub label_filter: Vec<String>,
558    #[serde(default)]
559    #[secret]
560    pub oauth_token: String,
561    #[serde(default)]
562    pub webhook_url: String,
563    #[serde(default)]
564    pub webhook_secret: String,
565
566    /// Tools excluded from this channel's tool spec. When set, these tools
567    /// are not exposed to the model when responding via this channel.
568    #[serde(default)]
569    pub excluded_tools: Vec<String>,
570}
571
572impl ChannelConfig for GmailPushConfig {
573    fn name() -> &'static str {
574        "Gmail Push"
575    }
576    fn desc() -> &'static str {
577        "Gmail Pub/Sub push notifications"
578    }
579}
580
581impl Default for GmailPushConfig {
582    fn default() -> Self {
583        Self {
584            enabled: false,
585            topic: String::new(),
586            label_filter: default_label_filter(),
587            oauth_token: String::new(),
588            webhook_url: String::new(),
589            webhook_secret: String::new(),
590            excluded_tools: Vec::new(),
591        }
592    }
593}
594
595#[derive(Debug, Clone, Default, Serialize, Deserialize, zeroclaw_macros::Configurable)]
596#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
597#[prefix = "channels.clawdtalk"]
598pub struct ClawdTalkConfig {
599    /// Whether this channel is active. The runtime only loads channels whose
600    /// `enabled = true`. Default: `false` so an operator who pastes a partial
601    /// `[channels.<type>.<alias>]` block doesn't accidentally bring a channel
602    /// live before the rest of its config is filled in.
603    #[serde(default)]
604    pub enabled: bool,
605    #[secret]
606    pub api_key: String,
607    pub connection_id: String,
608    pub from_number: String,
609    #[serde(default)]
610    pub allowed_destinations: Vec<String>,
611    #[serde(default)]
612    #[secret]
613    pub webhook_secret: Option<String>,
614
615    /// Tools excluded from this channel's tool spec. When set, these tools
616    /// are not exposed to the model when responding via this channel.
617    #[serde(default)]
618    pub excluded_tools: Vec<String>,
619}
620
621impl ChannelConfig for ClawdTalkConfig {
622    fn name() -> &'static str {
623        "ClawdTalk"
624    }
625    fn desc() -> &'static str {
626        "ClawdTalk Channel"
627    }
628}
629
630/// Which telephony model_provider to use.
631#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
632#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
633#[serde(rename_all = "lowercase")]
634pub enum VoiceProvider {
635    #[default]
636    Twilio,
637    Telnyx,
638    Plivo,
639}
640
641impl HasPropKind for VoiceProvider {
642    const PROP_KIND: PropKind = PropKind::Enum;
643}
644
645impl fmt::Display for VoiceProvider {
646    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647        match self {
648            Self::Twilio => write!(f, "twilio"),
649            Self::Telnyx => write!(f, "telnyx"),
650            Self::Plivo => write!(f, "plivo"),
651        }
652    }
653}
654
655fn default_webhook_port() -> u16 {
656    8090
657}
658fn default_max_call_duration() -> u64 {
659    3600
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
663#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
664#[prefix = "channels.voice-call"]
665pub struct VoiceCallConfig {
666    /// Whether this channel is active. The runtime only loads channels whose
667    /// `enabled = true`. Default: `false` so an operator who pastes a partial
668    /// `[channels.<type>.<alias>]` block doesn't accidentally bring a channel
669    /// live before the rest of its config is filled in.
670    #[serde(default)]
671    pub enabled: bool,
672    #[serde(default)]
673    pub model_provider: VoiceProvider,
674    pub account_id: String,
675    pub auth_token: String,
676    pub from_number: String,
677    #[serde(default = "default_webhook_port")]
678    pub webhook_port: u16,
679    #[serde(default = "default_true")]
680    pub require_outbound_approval: bool,
681    #[serde(default = "default_true")]
682    pub transcription_logging: bool,
683    #[serde(default)]
684    pub tts_voice: Option<String>,
685    #[serde(default = "default_max_call_duration")]
686    pub max_call_duration_secs: u64,
687    #[serde(default)]
688    pub webhook_base_url: Option<String>,
689
690    /// Tools excluded from this channel's tool spec. When set, these tools
691    /// are not exposed to the model when responding via this channel.
692    #[serde(default)]
693    pub excluded_tools: Vec<String>,
694}
695
696impl crate::traits::ChannelConfig for VoiceCallConfig {
697    fn name() -> &'static str {
698        "Voice Call"
699    }
700    fn desc() -> &'static str {
701        "outbound voice call channel"
702    }
703}
704
705impl Default for VoiceCallConfig {
706    fn default() -> Self {
707        Self {
708            enabled: false,
709            model_provider: VoiceProvider::default(),
710            account_id: String::new(),
711            auth_token: String::new(),
712            from_number: String::new(),
713            webhook_port: default_webhook_port(),
714            require_outbound_approval: default_true(),
715            transcription_logging: default_true(),
716            tts_voice: None,
717            max_call_duration_secs: default_max_call_duration(),
718            webhook_base_url: None,
719            excluded_tools: Vec::new(),
720        }
721    }
722}