1use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::path::Path;
10use std::str::FromStr;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14pub enum RuntimeMode {
15 #[default]
16 #[serde(rename = "local")]
17 Local,
18 #[serde(rename = "airgapped")]
19 AirGapped,
20 #[serde(rename = "cloud")]
21 Cloud,
22}
23
24impl FromStr for RuntimeMode {
25 type Err = std::convert::Infallible;
26
27 fn from_str(value: &str) -> Result<Self, Self::Err> {
28 Ok(match value.to_ascii_lowercase().as_str() {
29 "airgapped" => RuntimeMode::AirGapped,
30 "cloud" => RuntimeMode::Cloud,
31 _ => RuntimeMode::Local,
32 })
33 }
34}
35
36impl fmt::Display for RuntimeMode {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 RuntimeMode::Local => write!(f, "local"),
40 RuntimeMode::AirGapped => write!(f, "airgapped"),
41 RuntimeMode::Cloud => write!(f, "cloud"),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
48pub struct Providers {
49 pub azure: Option<AzureProvider>,
50 pub anthropic: Option<AnthropicProvider>,
51 pub openai: Option<OpenAIProvider>,
52 pub ollama: Option<OllamaProvider>,
53 pub google: Option<GoogleProvider>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct AzureProvider {
58 pub endpoint: Option<String>,
59 pub api_key: Option<String>,
60 pub deployment_name: Option<String>,
61 #[serde(default = "default_api_version")]
62 pub api_version: String,
63}
64
65fn default_api_version() -> String {
66 "2024-02-15-preview".to_string()
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub struct AnthropicProvider {
71 pub api_key: Option<String>,
72 #[serde(default = "default_anthropic_base_url")]
73 pub base_url: String,
74}
75
76fn default_anthropic_base_url() -> String {
77 "https://api.anthropic.com".to_string()
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct OpenAIProvider {
82 pub api_key: Option<String>,
83 #[serde(default = "default_openai_base_url")]
84 pub base_url: String,
85 pub organization: Option<String>,
86}
87
88fn default_openai_base_url() -> String {
89 "https://api.openai.com/v1".to_string()
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93pub struct OllamaProvider {
94 #[serde(default = "default_ollama_base_url")]
95 pub base_url: String,
96}
97
98fn default_ollama_base_url() -> String {
99 "http://localhost:11434".to_string()
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub struct GoogleProvider {
104 pub api_key: Option<String>,
105 #[serde(default = "default_google_base_url")]
106 pub base_url: String,
107}
108
109fn default_google_base_url() -> String {
110 "https://generativelanguage.googleapis.com/v1".to_string()
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115pub struct Runtime {
116 #[serde(default)]
117 pub mode: RuntimeMode,
118 #[serde(default = "default_max_concurrent")]
119 pub max_concurrent_executions: u32,
120 #[serde(default = "default_timeout")]
121 pub default_timeout: u64,
122 #[serde(default = "default_true")]
123 pub enable_telemetry: bool,
124 #[serde(default = "default_true")]
125 pub allow_network: bool,
126}
127
128fn default_max_concurrent() -> u32 {
129 10
130}
131
132fn default_timeout() -> u64 {
133 30000
134}
135
136fn default_true() -> bool {
137 true
138}
139
140impl Default for Runtime {
141 fn default() -> Self {
142 Self {
143 mode: RuntimeMode::Local,
144 max_concurrent_executions: 10,
145 default_timeout: 30000,
146 enable_telemetry: true,
147 allow_network: true,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
154pub struct Storage {
155 #[serde(default = "default_event_store")]
156 pub event_store: EventStore,
157 #[serde(default = "default_state_store")]
158 pub state_store: StateStore,
159 #[serde(default = "default_filesystem_store")]
160 pub artifact_store: ArtifactStore,
161 #[serde(default = "default_sqlite_vector_store")]
162 pub vector_store: VectorStore,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
166pub struct EventStore {
167 #[serde(default = "default_sqlite")]
168 pub r#type: String,
169 pub path: Option<String>,
170 pub dsn: Option<String>,
171}
172
173fn default_sqlite() -> String {
174 "sqlite".to_string()
175}
176
177impl Default for EventStore {
178 fn default() -> Self {
179 Self {
180 r#type: "jsonl".to_string(),
181 path: Some("events".to_string()),
182 dsn: None,
183 }
184 }
185}
186
187fn default_event_store() -> EventStore {
188 EventStore::default()
189}
190
191impl Default for StateStore {
192 fn default() -> Self {
193 Self {
194 r#type: "jsonl".to_string(),
195 path: Some("state".to_string()),
196 dsn: None,
197 }
198 }
199}
200
201fn default_state_store() -> StateStore {
202 StateStore::default()
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
206pub struct StateStore {
207 #[serde(default = "default_sqlite")]
208 pub r#type: String,
209 pub path: Option<String>,
210 pub dsn: Option<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
214pub struct ArtifactStore {
215 #[serde(default = "default_filesystem")]
216 pub r#type: String,
217 pub path: Option<String>,
218 #[serde(default = "default_zstd")]
219 pub compression: String,
220}
221
222fn default_filesystem() -> String {
223 "filesystem".to_string()
224}
225
226fn default_zstd() -> String {
227 "zstd".to_string()
228}
229
230impl Default for ArtifactStore {
231 fn default() -> Self {
232 Self {
233 r#type: "filesystem".to_string(),
234 path: None,
235 compression: "zstd".to_string(),
236 }
237 }
238}
239
240fn default_filesystem_store() -> ArtifactStore {
241 ArtifactStore::default()
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
245pub struct VectorStore {
246 #[serde(default = "default_sqlite")]
247 pub r#type: String,
248 pub url: Option<String>,
249 pub collection: Option<String>,
250 pub path: Option<String>,
251 pub dsn: Option<String>,
252}
253
254impl Default for VectorStore {
255 fn default() -> Self {
256 Self {
257 r#type: "sqlite".to_string(),
258 url: None,
259 collection: None,
260 path: None,
261 dsn: None,
262 }
263 }
264}
265
266fn default_sqlite_vector_store() -> VectorStore {
267 VectorStore::default()
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
272pub struct Tools {
273 #[serde(default)]
274 pub ingestion: IngestionTools,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
278pub struct IngestionTools {
279 #[serde(default = "default_pdf")]
280 pub pdf: PdfIngestion,
281 #[serde(default = "default_ocr")]
282 pub ocr: OcrIngestion,
283 #[serde(default = "default_embeddings")]
284 pub embeddings: EmbeddingsIngestion,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
288pub struct PdfIngestion {
289 #[serde(default = "default_pdfium")]
290 pub engine: String,
291}
292
293impl Default for PdfIngestion {
294 fn default() -> Self {
295 Self {
296 engine: "pdfium".to_string(),
297 }
298 }
299}
300
301fn default_pdfium() -> String {
302 "pdfium".to_string()
303}
304
305fn default_pdf() -> PdfIngestion {
306 PdfIngestion {
307 engine: "pdfium".to_string(),
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
312pub struct OcrIngestion {
313 #[serde(default = "default_tesseract")]
314 pub engine: String,
315 #[serde(default = "default_languages")]
316 pub languages: Vec<String>,
317}
318
319impl Default for OcrIngestion {
320 fn default() -> Self {
321 Self {
322 engine: "tesseract".to_string(),
323 languages: vec!["eng".to_string()],
324 }
325 }
326}
327
328fn default_tesseract() -> String {
329 "tesseract".to_string()
330}
331
332fn default_languages() -> Vec<String> {
333 vec!["eng".to_string()]
334}
335
336fn default_ocr() -> OcrIngestion {
337 OcrIngestion {
338 engine: "tesseract".to_string(),
339 languages: vec!["eng".to_string()],
340 }
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
344pub struct EmbeddingsIngestion {
345 #[serde(default = "default_fastembed")]
346 pub engine: String,
347 pub model: Option<String>,
348}
349
350impl Default for EmbeddingsIngestion {
351 fn default() -> Self {
352 Self {
353 engine: "fastembed".to_string(),
354 model: None,
355 }
356 }
357}
358
359fn default_fastembed() -> String {
360 "fastembed".to_string()
361}
362
363fn default_embeddings() -> EmbeddingsIngestion {
364 EmbeddingsIngestion {
365 engine: "fastembed".to_string(),
366 model: None,
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
372pub struct Cloud {
373 pub api_url: Option<String>,
374 pub tenant_id: Option<String>,
375 #[serde(default)]
376 pub auto_sync: bool,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
406pub struct ApprovalConfig {
407 #[serde(default)]
409 pub enabled: bool,
410
411 #[serde(default = "default_approval_policy")]
413 pub policy: String,
414
415 pub max_steps: Option<usize>,
417
418 pub require_patterns: Option<Vec<String>>,
421
422 #[serde(default = "default_approval_timeout")]
424 pub timeout_seconds: u64,
425
426 #[serde(default)]
429 pub tool_overrides: Option<std::collections::HashMap<String, String>>,
430}
431
432fn default_approval_policy() -> String {
433 "always_approve".to_string()
434}
435
436fn default_approval_timeout() -> u64 {
437 300 }
439
440impl Default for ApprovalConfig {
441 fn default() -> Self {
442 Self {
443 enabled: false,
444 policy: "always_approve".to_string(),
445 max_steps: None,
446 require_patterns: None,
447 timeout_seconds: 300,
448 tool_overrides: None,
449 }
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
458pub struct MemoryConfig {
459 #[serde(default = "default_memory_backend")]
461 pub backend: String,
462
463 #[serde(default = "default_daily_logs_dir")]
465 pub daily_logs_dir: String,
466
467 #[serde(default = "default_max_daily_entries")]
469 pub max_daily_entries: usize,
470
471 #[serde(default)]
473 pub auto_consolidate: bool,
474
475 pub consolidation_time: Option<String>,
477
478 pub retention_days: Option<u32>,
480
481 #[serde(default = "default_true")]
483 pub include_timestamps: bool,
484}
485
486fn default_memory_backend() -> String {
487 "markdown".to_string()
488}
489
490fn default_daily_logs_dir() -> String {
491 "memory".to_string()
492}
493
494fn default_max_daily_entries() -> usize {
495 100
496}
497
498impl Default for MemoryConfig {
499 fn default() -> Self {
500 Self {
501 backend: "markdown".to_string(),
502 daily_logs_dir: "memory".to_string(),
503 max_daily_entries: 100,
504 auto_consolidate: true,
505 consolidation_time: Some("03:00".to_string()),
506 retention_days: Some(30),
507 include_timestamps: true,
508 }
509 }
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
517pub struct SessionConfig {
518 #[serde(default = "default_max_turns")]
520 pub max_turns: usize,
521
522 #[serde(default = "default_rotation_threshold_pct")]
524 pub rotation_threshold_pct: u8,
525
526 #[serde(default = "default_idle_timeout_secs")]
528 pub idle_timeout_secs: u64,
529
530 #[serde(default = "default_cleanup_interval_secs")]
532 pub cleanup_interval_secs: u64,
533
534 #[serde(default = "default_cleanup_idle_threshold_secs")]
536 pub cleanup_idle_threshold_secs: u64,
537}
538
539fn default_max_turns() -> usize {
540 20
541}
542
543fn default_rotation_threshold_pct() -> u8 {
544 80
545}
546
547fn default_idle_timeout_secs() -> u64 {
548 1800 }
550
551fn default_cleanup_interval_secs() -> u64 {
552 300 }
554
555fn default_cleanup_idle_threshold_secs() -> u64 {
556 3600 }
558
559impl Default for SessionConfig {
560 fn default() -> Self {
561 Self {
562 max_turns: default_max_turns(),
563 rotation_threshold_pct: default_rotation_threshold_pct(),
564 idle_timeout_secs: default_idle_timeout_secs(),
565 cleanup_interval_secs: default_cleanup_interval_secs(),
566 cleanup_idle_threshold_secs: default_cleanup_idle_threshold_secs(),
567 }
568 }
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
576pub struct SessionConfigOverride {
577 pub max_turns: Option<usize>,
578 pub rotation_threshold_pct: Option<u8>,
579 pub idle_timeout_secs: Option<u64>,
580 pub cleanup_interval_secs: Option<u64>,
581 pub cleanup_idle_threshold_secs: Option<u64>,
582}
583
584impl SessionConfigOverride {
585 pub fn apply_to(&self, base: &SessionConfig) -> SessionConfig {
588 SessionConfig {
589 max_turns: self.max_turns.unwrap_or(base.max_turns),
590 rotation_threshold_pct: self
591 .rotation_threshold_pct
592 .unwrap_or(base.rotation_threshold_pct),
593 idle_timeout_secs: self.idle_timeout_secs.unwrap_or(base.idle_timeout_secs),
594 cleanup_interval_secs: self
595 .cleanup_interval_secs
596 .unwrap_or(base.cleanup_interval_secs),
597 cleanup_idle_threshold_secs: self
598 .cleanup_idle_threshold_secs
599 .unwrap_or(base.cleanup_idle_threshold_secs),
600 }
601 }
602}
603
604#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
606pub struct ServerConfig {
607 #[serde(default = "default_server_port")]
609 pub port: u16,
610 #[serde(default = "default_server_host")]
612 pub host: String,
613 #[serde(default = "default_grpc_port")]
615 pub grpc_port: u16,
616 #[serde(default = "default_docs_port")]
618 pub docs_port: u16,
619 #[serde(default)]
622 pub docs_base_url: Option<String>,
623}
624
625fn default_server_port() -> u16 {
626 8080
627}
628
629fn default_server_host() -> String {
630 "0.0.0.0".to_string()
631}
632
633fn default_grpc_port() -> u16 {
634 50051
635}
636
637fn default_docs_port() -> u16 {
638 1111
639}
640
641impl Default for ServerConfig {
642 fn default() -> Self {
643 Self {
644 port: default_server_port(),
645 host: default_server_host(),
646 grpc_port: default_grpc_port(),
647 docs_port: default_docs_port(),
648 docs_base_url: None,
649 }
650 }
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
655pub struct LoggingConfig {
656 #[serde(default = "default_daemon_log")]
658 pub daemon_log: String,
659 #[serde(default = "default_serve_log")]
661 pub serve_log: String,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
669pub struct ObservabilityConfig {
670 #[serde(default = "default_true")]
672 pub trace_llm_calls: bool,
673
674 #[serde(default)]
676 pub log_full_prompts: bool,
677
678 #[serde(default)]
680 pub log_full_responses: bool,
681
682 #[serde(default = "default_true")]
684 pub track_token_usage: bool,
685
686 #[serde(default = "default_true")]
688 pub trace_memory_access: bool,
689
690 #[serde(default)]
692 pub enable_context_snapshots: bool,
693
694 #[serde(default)]
696 pub capture_reasoning_traces: bool,
697
698 #[serde(default = "default_max_content_length")]
700 pub max_content_length: usize,
701}
702
703fn default_max_content_length() -> usize {
704 1000
705}
706
707impl Default for ObservabilityConfig {
708 fn default() -> Self {
709 Self {
710 trace_llm_calls: true,
711 log_full_prompts: false,
712 log_full_responses: false,
713 track_token_usage: true,
714 trace_memory_access: true,
715 enable_context_snapshots: false,
716 capture_reasoning_traces: false,
717 max_content_length: 1000,
718 }
719 }
720}
721
722fn default_daemon_log() -> String {
723 "logs/daemon.log".to_string()
724}
725
726fn default_serve_log() -> String {
727 "logs/serve.log".to_string()
728}
729
730impl Default for LoggingConfig {
731 fn default() -> Self {
732 Self {
733 daemon_log: default_daemon_log(),
734 serve_log: default_serve_log(),
735 }
736 }
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
741#[serde(deny_unknown_fields)]
742pub struct Config {
743 #[serde(default)]
745 pub default_model_id: Option<String>,
746 #[serde(default)]
747 pub providers: Providers,
748 #[serde(default)]
749 pub runtime: Runtime,
750 #[serde(default)]
751 pub storage: Storage,
752 #[serde(default)]
753 pub tools: Tools,
754 pub cloud: Option<Cloud>,
755 #[serde(default)]
757 pub server: ServerConfig,
758 #[serde(default)]
760 pub approval: ApprovalConfig,
761 #[serde(default)]
763 pub memory: MemoryConfig,
764 #[serde(default)]
766 pub session: SessionConfig,
767 #[serde(default)]
769 pub logging: LoggingConfig,
770 #[serde(default)]
772 pub observability: ObservabilityConfig,
773}
774
775impl Config {
776 pub fn load_from_home() -> Result<Self> {
779 let path = crate::home::enact_home().join("config.yaml");
780 Self::load_from_yaml_path(&path)
781 }
782
783 pub fn ensure_default_at(path: &Path) -> Result<()> {
786 if path.exists() {
787 return Ok(());
788 }
789 if let Some(parent) = path.parent() {
790 std::fs::create_dir_all(parent).context("Failed to create config directory")?;
791 }
792 Self::default().save_to_yaml_path(path)
793 }
794
795 pub fn save_to_home(&self) -> Result<()> {
798 crate::home::create_config_backup()?;
799 let path = crate::home::enact_home().join("config.yaml");
800 self.save_to_yaml_path(&path)
801 }
802
803 pub fn load_from_yaml_path(path: &Path) -> Result<Self> {
805 if !path.exists() {
806 return Ok(Self::default());
807 }
808 let s = std::fs::read_to_string(path).context("Failed to read config file")?;
809 let config: Config = serde_yaml::from_str(&s).context("Failed to parse config YAML")?;
810 Ok(config)
811 }
812
813 pub fn save_to_yaml_path(&self, path: &Path) -> Result<()> {
815 if let Some(parent) = path.parent() {
816 std::fs::create_dir_all(parent).context("Failed to create config directory")?;
817 }
818 let s = serde_yaml::to_string(self).context("Failed to serialize config to YAML")?;
819 std::fs::write(path, s).context("Failed to write config file")?;
820 Ok(())
821 }
822}