Skip to main content

convergio_types/
config.rs

1//! Configuration types — pure data structures, no I/O.
2//!
3//! Loading, validation, and hot-reload live elsewhere (server crate).
4//! These structs are shared across crates for type-safe config access.
5
6use serde::{Deserialize, Serialize};
7
8/// Roles a node can assume — controls which extensions load at boot.
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum NodeRole {
12    /// All extensions loaded (default, single-node).
13    #[default]
14    All,
15    /// Orchestrates workers, hosts plans DB, runs platform services.
16    Orchestrator,
17    /// Local AI kernel (Jarvis), Telegram, voice.
18    Kernel,
19    /// Voice I/O only.
20    Voice,
21    /// Receives delegated tasks, runs agents.
22    Worker,
23    /// Hosts night agent workloads (knowledge sync, nightly jobs).
24    NightAgent,
25}
26
27impl NodeRole {
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            Self::All => "all",
31            Self::Orchestrator => "orchestrator",
32            Self::Kernel => "kernel",
33            Self::Voice => "voice",
34            Self::Worker => "worker",
35            Self::NightAgent => "nightagent",
36        }
37    }
38}
39
40impl std::fmt::Display for NodeRole {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.write_str(self.as_str())
43    }
44}
45
46#[derive(Debug, Clone, Deserialize)]
47#[serde(default)]
48pub struct NodeConfig {
49    pub name: String,
50    /// Node role — controls which extensions are loaded at boot.
51    pub role: NodeRole,
52}
53
54impl Default for NodeConfig {
55    fn default() -> Self {
56        Self {
57            name: String::new(),
58            role: NodeRole::All,
59        }
60    }
61}
62
63#[derive(Debug, Clone, Deserialize, Default)]
64#[serde(default)]
65pub struct TailscaleConfig {
66    pub enabled: bool,
67    pub auth_key: String,
68}
69
70#[derive(Debug, Clone, Deserialize)]
71#[serde(default)]
72pub struct MeshConfig {
73    pub transport: String,
74    pub discovery: String,
75    pub peers: Vec<String>,
76    pub tailscale: TailscaleConfig,
77}
78
79impl Default for MeshConfig {
80    fn default() -> Self {
81        Self {
82            transport: "lan".to_string(),
83            discovery: "mdns".to_string(),
84            peers: Vec::new(),
85            tailscale: TailscaleConfig::default(),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Deserialize, PartialEq)]
91#[serde(default)]
92pub struct InferenceFallbackConfig {
93    pub max_attempts: usize,
94    pub t1: Vec<String>,
95    pub t2: Vec<String>,
96    pub t3: Vec<String>,
97    pub t4: Vec<String>,
98}
99
100impl Default for InferenceFallbackConfig {
101    fn default() -> Self {
102        Self {
103            max_attempts: 3,
104            t1: vec!["local".into(), "haiku".into(), "sonnet".into()],
105            t2: vec!["haiku".into(), "local".into(), "sonnet".into()],
106            t3: vec!["sonnet".into(), "opus".into()],
107            t4: vec!["opus".into(), "sonnet".into()],
108        }
109    }
110}
111
112#[derive(Debug, Clone, Deserialize)]
113#[serde(default)]
114pub struct InferenceConfig {
115    pub default_model: String,
116    pub api_key_env: String,
117    pub fallback: InferenceFallbackConfig,
118}
119
120impl Default for InferenceConfig {
121    fn default() -> Self {
122        Self {
123            default_model: "claude-sonnet-4-6".to_string(),
124            api_key_env: "ANTHROPIC_API_KEY".to_string(),
125            fallback: InferenceFallbackConfig::default(),
126        }
127    }
128}
129
130#[derive(Debug, Clone, Deserialize)]
131#[serde(default)]
132pub struct KernelConfig {
133    pub model: String,
134    pub model_path: String,
135    pub escalation_model: String,
136    pub max_tokens: u32,
137}
138
139impl Default for KernelConfig {
140    fn default() -> Self {
141        Self {
142            model: "none".to_string(),
143            model_path: String::new(),
144            escalation_model: String::new(),
145            max_tokens: 2048,
146        }
147    }
148}
149
150#[derive(Debug, Clone, Deserialize, Default)]
151#[serde(default)]
152pub struct TelegramConfig {
153    pub enabled: bool,
154    pub token_keychain: String,
155}
156
157#[derive(Debug, Clone, Deserialize)]
158#[serde(default)]
159pub struct NightConfig {
160    pub night_mode: bool,
161    /// Format: "HH:MM-HH:MM" (e.g. "23:00-07:00")
162    pub night_hours: String,
163    pub night_model: String,
164}
165
166impl Default for NightConfig {
167    fn default() -> Self {
168        Self {
169            night_mode: false,
170            night_hours: "23:00-07:00".to_string(),
171            night_model: "claude-haiku-4-5".to_string(),
172        }
173    }
174}
175
176#[derive(Debug, Clone, Deserialize)]
177#[serde(default)]
178pub struct DaemonConfig {
179    pub port: u16,
180    pub quiet_hours: Option<String>,
181    pub timezone: Option<String>,
182    pub auto_update: bool,
183}
184
185impl Default for DaemonConfig {
186    fn default() -> Self {
187        Self {
188            port: 8420,
189            quiet_hours: None,
190            timezone: None,
191            auto_update: true,
192        }
193    }
194}
195
196/// Top-level config — deserialized from config.toml.
197#[derive(Debug, Clone, Deserialize, Default)]
198#[serde(default)]
199pub struct ConvergioConfig {
200    pub node: NodeConfig,
201    pub daemon: DaemonConfig,
202    pub night: NightConfig,
203    pub mesh: MeshConfig,
204    pub inference: InferenceConfig,
205    pub kernel: KernelConfig,
206    pub telegram: TelegramConfig,
207}