1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct LlmConfig {
9 pub provider: String,
11 pub model: String,
13 pub api_key_env: String,
15 pub base_url: Option<String>,
17 pub timeout_secs: u64,
19 pub max_tokens: u32,
21 pub temperature: f32,
23}
24
25impl Default for LlmConfig {
26 fn default() -> Self {
27 Self {
28 provider: "openai".to_string(),
29 model: "gpt-4o-mini".to_string(),
30 api_key_env: "OPENAI_API_KEY".to_string(),
31 base_url: None,
32 timeout_secs: 60,
33 max_tokens: 4096,
34 temperature: 0.3,
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct AgentConfig {
42 pub enabled: bool,
44 pub namespace: String,
46 pub inbox_dir: String,
48 pub scan_interval_secs: u64,
50 pub consolidation_interval_mins: u64,
52 pub consolidation_batch_size: usize,
54 pub query_context_limit: usize,
56}
57
58impl Default for AgentConfig {
59 fn default() -> Self {
60 Self {
61 enabled: false,
62 namespace: "nexus-agent".to_string(),
63 inbox_dir: "./inbox".to_string(),
64 scan_interval_secs: 5,
65 consolidation_interval_mins: 30,
66 consolidation_batch_size: 10,
67 query_context_limit: 50,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct CognitionConfig {
75 pub auto_runtime_enabled: bool,
77 pub derive_enabled: bool,
79 pub digest_enabled: bool,
81 pub reflect_enabled: bool,
83 pub activity_distill_enabled: bool,
85 pub dream_on_session_end: bool,
87 pub checkpoint_flush_enabled: bool,
89 pub runtime_idle_timeout_secs: u64,
91 pub max_job_batch: usize,
93 pub lease_ttl_secs: u64,
95 pub representation_max_items: usize,
97 pub digest_short_target_tokens: usize,
99 pub digest_long_target_tokens: usize,
101 pub direct_enrichment_timeout_secs: u64,
103 pub activity_distill_min_events: usize,
105 pub activity_distill_max_events: usize,
107 pub include_raw_by_default: bool,
109 pub session_end_dream_timeout_secs: u64,
111 pub retry_buffer_drain_limit: usize,
113 pub contradiction_belief_revision_enabled: bool,
115 pub contradiction_confidence_penalty: f32,
117 pub memory_decay_enabled: bool,
119 pub memory_decay_age_days: u64,
121 pub memory_decay_access_boost_days: u64,
123 pub adaptive_dream_enabled: bool,
125 pub adaptive_dream_min_interval_secs: u64,
127 pub adaptive_dream_max_interval_secs: u64,
129}
130
131impl Default for CognitionConfig {
132 fn default() -> Self {
133 Self {
134 auto_runtime_enabled: true,
135 derive_enabled: true,
136 digest_enabled: true,
137 reflect_enabled: true,
138 activity_distill_enabled: true,
139 dream_on_session_end: true,
140 checkpoint_flush_enabled: true,
141 runtime_idle_timeout_secs: 900,
142 max_job_batch: 8,
143 lease_ttl_secs: 120,
144 representation_max_items: 24,
145 digest_short_target_tokens: 600,
146 digest_long_target_tokens: 1800,
147 direct_enrichment_timeout_secs: 8,
148 activity_distill_min_events: 8,
149 activity_distill_max_events: 60,
150 include_raw_by_default: false,
151 session_end_dream_timeout_secs: 8,
152 retry_buffer_drain_limit: 8,
153 contradiction_belief_revision_enabled: true,
154 contradiction_confidence_penalty: 0.15,
155 memory_decay_enabled: true,
156 memory_decay_age_days: 90,
157 memory_decay_access_boost_days: 30,
158 adaptive_dream_enabled: true,
159 adaptive_dream_min_interval_secs: 60,
160 adaptive_dream_max_interval_secs: 1800,
161 }
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, Default)]
167pub struct Config {
168 pub database: DatabaseConfig,
170
171 pub server: ServerConfig,
173
174 pub embedding: EmbeddingConfig,
176
177 pub sync: SyncConfig,
179
180 pub llm: LlmConfig,
182
183 pub agent: AgentConfig,
185
186 pub cognition: CognitionConfig,
188}
189
190impl Config {
191 pub fn from_env() -> crate::Result<Self> {
193 let mut config = Self::default();
194
195 if let Ok(path) = std::env::var("NEXUS_DATABASE_PATH") {
196 config.database.path = PathBuf::from(path);
197 }
198
199 if let Ok(host) = std::env::var("NEXUS_HOST") {
200 config.server.host = host;
201 }
202
203 if let Ok(port) = std::env::var("NEXUS_PORT") {
204 config.server.port = port.parse().unwrap_or(8768);
205 }
206
207 if let Ok(enabled) = std::env::var("NEXUS_EMBEDDINGS_ENABLED") {
208 config.embedding.enabled = enabled.parse().unwrap_or(false);
209 }
210
211 if let Ok(backend) = std::env::var("NEXUS_EMBEDDING_BACKEND") {
212 config.embedding.backend = backend;
213 }
214
215 if let Ok(provider) = std::env::var("NEXUS_EMBEDDING_PROVIDER") {
216 config.embedding.provider = provider;
217 }
218
219 if let Ok(model) = std::env::var("NEXUS_EMBEDDING_MODEL") {
220 config.embedding.model = model;
221 }
222
223 if let Ok(key_env) = std::env::var("NEXUS_EMBEDDING_API_KEY_ENV") {
224 config.embedding.api_key_env = key_env;
225 }
226
227 if let Ok(base_url) = std::env::var("NEXUS_EMBEDDING_BASE_URL") {
228 config.embedding.base_url = Some(base_url);
229 }
230
231 if let Ok(dimension) = std::env::var("NEXUS_EMBEDDING_DIMENSION") {
232 config.embedding.dimension = dimension
233 .parse()
234 .unwrap_or(EmbeddingConfig::default().dimension);
235 }
236
237 if let Ok(timeout) = std::env::var("NEXUS_EMBEDDING_TIMEOUT_SECS") {
238 config.embedding.timeout_secs = timeout
239 .parse()
240 .unwrap_or(EmbeddingConfig::default().timeout_secs);
241 }
242
243 if let Ok(model_path) = std::env::var("NEXUS_EMBEDDING_MODEL_PATH") {
244 config.embedding.local_model_path = Some(model_path);
245 }
246
247 if let Ok(tokenizer_path) = std::env::var("NEXUS_TOKENIZER_PATH") {
248 config.embedding.local_tokenizer_path = Some(tokenizer_path);
249 }
250
251 if let Ok(policy) = std::env::var("NEXUS_SYNC_POLICY") {
252 config.sync.policy = policy;
253 }
254
255 if let Ok(provider) = std::env::var("NEXUS_LLM_PROVIDER") {
257 config.llm.provider = provider;
258 }
259 if let Ok(model) = std::env::var("NEXUS_LLM_MODEL") {
260 config.llm.model = model;
261 }
262 if let Ok(key_env) = std::env::var("NEXUS_LLM_API_KEY_ENV") {
263 config.llm.api_key_env = key_env;
264 }
265 if let Ok(base_url) = std::env::var("NEXUS_LLM_BASE_URL") {
266 config.llm.base_url = Some(base_url);
267 }
268
269 if let Ok(enabled) = std::env::var("NEXUS_AGENT_ENABLED") {
271 config.agent.enabled = enabled.parse().unwrap_or(false);
272 }
273 if let Ok(namespace) = std::env::var("NEXUS_AGENT_NAMESPACE") {
274 config.agent.namespace = namespace;
275 }
276 if let Ok(inbox) = std::env::var("NEXUS_AGENT_INBOX_DIR") {
277 config.agent.inbox_dir = inbox;
278 }
279 if let Ok(interval) = std::env::var("NEXUS_AGENT_CONSOLIDATION_INTERVAL_MINS") {
280 config.agent.consolidation_interval_mins = interval
281 .parse()
282 .unwrap_or(AgentConfig::default().consolidation_interval_mins);
283 } else if let Ok(interval) = std::env::var("NEXUS_AGENT_CONSOLIDATION_INTERVAL") {
284 config.agent.consolidation_interval_mins = interval
286 .parse()
287 .unwrap_or(AgentConfig::default().consolidation_interval_mins);
288 }
289 if let Ok(interval) = std::env::var("NEXUS_AGENT_SCAN_INTERVAL_SECS") {
290 config.agent.scan_interval_secs = interval
291 .parse()
292 .unwrap_or(AgentConfig::default().scan_interval_secs);
293 } else if let Ok(interval) = std::env::var("NEXUS_AGENT_SCAN_INTERVAL") {
294 config.agent.scan_interval_secs = interval
296 .parse()
297 .unwrap_or(AgentConfig::default().scan_interval_secs);
298 }
299
300 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_AUTO_RUNTIME_ENABLED") {
301 config.cognition.auto_runtime_enabled = enabled.parse().unwrap_or(true);
302 }
303 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_DERIVE_ENABLED") {
304 config.cognition.derive_enabled = enabled.parse().unwrap_or(true);
305 }
306 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_DIGEST_ENABLED") {
307 config.cognition.digest_enabled = enabled.parse().unwrap_or(true);
308 }
309 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_REFLECT_ENABLED") {
310 config.cognition.reflect_enabled = enabled.parse().unwrap_or(true);
311 }
312 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_ACTIVITY_DISTILL_ENABLED") {
313 config.cognition.activity_distill_enabled = enabled.parse().unwrap_or(true);
314 }
315 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_DREAM_ON_SESSION_END") {
316 config.cognition.dream_on_session_end = enabled.parse().unwrap_or(true);
317 }
318 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_CHECKPOINT_FLUSH_ENABLED") {
319 config.cognition.checkpoint_flush_enabled = enabled.parse().unwrap_or(true);
320 }
321 if let Ok(timeout) = std::env::var("NEXUS_COGNITION_RUNTIME_IDLE_TIMEOUT_SECS") {
322 config.cognition.runtime_idle_timeout_secs = timeout
323 .parse()
324 .unwrap_or(CognitionConfig::default().runtime_idle_timeout_secs);
325 }
326 if let Ok(batch) = std::env::var("NEXUS_COGNITION_MAX_JOB_BATCH") {
327 config.cognition.max_job_batch = batch
328 .parse()
329 .unwrap_or(CognitionConfig::default().max_job_batch);
330 }
331 if let Ok(ttl) = std::env::var("NEXUS_COGNITION_LEASE_TTL_SECS") {
332 config.cognition.lease_ttl_secs = ttl
333 .parse()
334 .unwrap_or(CognitionConfig::default().lease_ttl_secs);
335 }
336 if let Ok(items) = std::env::var("NEXUS_COGNITION_REPRESENTATION_MAX_ITEMS") {
337 config.cognition.representation_max_items = items
338 .parse()
339 .unwrap_or(CognitionConfig::default().representation_max_items);
340 }
341 if let Ok(tokens) = std::env::var("NEXUS_COGNITION_DIGEST_SHORT_TARGET_TOKENS") {
342 config.cognition.digest_short_target_tokens = tokens
343 .parse()
344 .unwrap_or(CognitionConfig::default().digest_short_target_tokens);
345 }
346 if let Ok(tokens) = std::env::var("NEXUS_COGNITION_DIGEST_LONG_TARGET_TOKENS") {
347 config.cognition.digest_long_target_tokens = tokens
348 .parse()
349 .unwrap_or(CognitionConfig::default().digest_long_target_tokens);
350 }
351 if let Ok(timeout) = std::env::var("NEXUS_COGNITION_DIRECT_ENRICHMENT_TIMEOUT_SECS") {
352 config.cognition.direct_enrichment_timeout_secs = timeout
353 .parse()
354 .unwrap_or(CognitionConfig::default().direct_enrichment_timeout_secs);
355 }
356 if let Ok(events) = std::env::var("NEXUS_COGNITION_ACTIVITY_DISTILL_MIN_EVENTS") {
357 config.cognition.activity_distill_min_events = events
358 .parse()
359 .unwrap_or(CognitionConfig::default().activity_distill_min_events);
360 }
361 if let Ok(events) = std::env::var("NEXUS_COGNITION_ACTIVITY_DISTILL_MAX_EVENTS") {
362 config.cognition.activity_distill_max_events = events
363 .parse()
364 .unwrap_or(CognitionConfig::default().activity_distill_max_events);
365 }
366 if let Ok(include_raw) = std::env::var("NEXUS_COGNITION_INCLUDE_RAW_BY_DEFAULT") {
367 config.cognition.include_raw_by_default = include_raw.parse().unwrap_or(false);
368 }
369 if let Ok(timeout) = std::env::var("NEXUS_COGNITION_SESSION_END_DREAM_TIMEOUT_SECS") {
370 config.cognition.session_end_dream_timeout_secs = timeout
371 .parse()
372 .unwrap_or(CognitionConfig::default().session_end_dream_timeout_secs);
373 }
374 if let Ok(limit) = std::env::var("NEXUS_COGNITION_RETRY_BUFFER_DRAIN_LIMIT") {
375 config.cognition.retry_buffer_drain_limit = limit
376 .parse()
377 .unwrap_or(CognitionConfig::default().retry_buffer_drain_limit);
378 }
379 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_CONTRADICTION_BELIEF_REVISION_ENABLED")
380 {
381 config.cognition.contradiction_belief_revision_enabled =
382 enabled.parse().unwrap_or(true);
383 }
384 if let Ok(penalty) = std::env::var("NEXUS_COGNITION_CONTRADICTION_CONFIDENCE_PENALTY") {
385 config.cognition.contradiction_confidence_penalty = penalty
386 .parse()
387 .unwrap_or(CognitionConfig::default().contradiction_confidence_penalty);
388 }
389 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_MEMORY_DECAY_ENABLED") {
390 config.cognition.memory_decay_enabled = enabled.parse().unwrap_or(true);
391 }
392 if let Ok(days) = std::env::var("NEXUS_COGNITION_MEMORY_DECAY_AGE_DAYS") {
393 config.cognition.memory_decay_age_days = days
394 .parse()
395 .unwrap_or(CognitionConfig::default().memory_decay_age_days);
396 }
397 if let Ok(days) = std::env::var("NEXUS_COGNITION_MEMORY_DECAY_ACCESS_BOOST_DAYS") {
398 config.cognition.memory_decay_access_boost_days = days
399 .parse()
400 .unwrap_or(CognitionConfig::default().memory_decay_access_boost_days);
401 }
402 if let Ok(enabled) = std::env::var("NEXUS_COGNITION_ADAPTIVE_DREAM_ENABLED") {
403 config.cognition.adaptive_dream_enabled = enabled.parse().unwrap_or(true);
404 }
405 if let Ok(secs) = std::env::var("NEXUS_COGNITION_ADAPTIVE_DREAM_MIN_INTERVAL_SECS") {
406 config.cognition.adaptive_dream_min_interval_secs = secs
407 .parse()
408 .unwrap_or(CognitionConfig::default().adaptive_dream_min_interval_secs);
409 }
410 if let Ok(secs) = std::env::var("NEXUS_COGNITION_ADAPTIVE_DREAM_MAX_INTERVAL_SECS") {
411 config.cognition.adaptive_dream_max_interval_secs = secs
412 .parse()
413 .unwrap_or(CognitionConfig::default().adaptive_dream_max_interval_secs);
414 }
415
416 Ok(config)
417 }
418
419 pub fn database_url(&self) -> String {
421 format!("sqlite:{}", self.database.path.display())
422 }
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct DatabaseConfig {
428 pub path: PathBuf,
430
431 pub foreign_keys: bool,
433
434 pub pool_size: u32,
436}
437
438impl Default for DatabaseConfig {
439 fn default() -> Self {
440 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
441 let base_path = PathBuf::from(home).join(".nexus");
442
443 Self {
444 path: base_path.join("nexus.db"),
445 foreign_keys: true,
446 pool_size: 5,
447 }
448 }
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct ServerConfig {
454 pub host: String,
456
457 pub port: u16,
459
460 pub web_port: u16,
462
463 pub transport: String,
465}
466
467impl Default for ServerConfig {
468 fn default() -> Self {
469 Self {
470 host: "127.0.0.1".to_string(),
471 port: 8768,
472 web_port: 8768,
473 transport: "stdio".to_string(),
474 }
475 }
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct EmbeddingConfig {
481 pub enabled: bool,
483
484 pub backend: String,
486
487 pub provider: String,
490
491 pub model: String,
493
494 pub api_key_env: String,
496
497 pub base_url: Option<String>,
499
500 pub dimension: usize,
502
503 pub timeout_secs: u64,
505
506 pub local_model_path: Option<String>,
508
509 pub local_tokenizer_path: Option<String>,
511}
512
513impl Default for EmbeddingConfig {
514 fn default() -> Self {
515 Self {
516 enabled: false,
517 backend: "local".to_string(),
518 provider: "local".to_string(),
519 model: "all-MiniLM-L6-v2".to_string(),
520 api_key_env: "OPENAI_API_KEY".to_string(),
521 base_url: None,
522 dimension: 384,
523 timeout_secs: 60,
524 local_model_path: Some("models/all-MiniLM-L6-v2.onnx".to_string()),
525 local_tokenizer_path: Some("models/all-MiniLM-L6-v2-tokenizer".to_string()),
526 }
527 }
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct SyncConfig {
533 pub policy: String,
535
536 pub interval_secs: u64,
538}
539
540impl Default for SyncConfig {
541 fn default() -> Self {
542 Self {
543 policy: "manual".to_string(),
544 interval_secs: 300,
545 }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_default_config() {
555 let config = Config::default();
556 assert!(!config.embedding.enabled);
557 assert_eq!(config.embedding.backend, "local");
558 assert_eq!(config.embedding.provider, "local");
559 assert_eq!(config.embedding.dimension, 384);
560 assert_eq!(config.server.port, 8768);
561 }
562
563 #[test]
564 fn test_database_url() {
565 let config = Config::default();
566 let url = config.database_url();
567 assert!(url.starts_with("sqlite:"));
568 }
569
570 #[test]
571 fn test_cognition_config_defaults() {
572 let config = Config::default();
573 assert!(config.cognition.derive_enabled);
574 assert!(config.cognition.digest_enabled);
575 assert!(config.cognition.reflect_enabled);
576 assert!(config.cognition.activity_distill_enabled);
577 assert_eq!(config.cognition.representation_max_items, 24);
578 assert!(!config.cognition.include_raw_by_default);
579 assert!(config.cognition.contradiction_belief_revision_enabled);
580 assert!((config.cognition.contradiction_confidence_penalty - 0.15).abs() < f32::EPSILON);
581 assert!(config.cognition.memory_decay_enabled);
582 assert_eq!(config.cognition.memory_decay_age_days, 90);
583 assert!(config.cognition.adaptive_dream_enabled);
584 assert_eq!(config.cognition.adaptive_dream_min_interval_secs, 60);
585 assert_eq!(config.cognition.adaptive_dream_max_interval_secs, 1800);
586 }
587
588 #[test]
589 fn test_cognition_config_from_env() {
590 std::env::set_var("NEXUS_COGNITION_DERIVE_ENABLED", "false");
591 std::env::set_var("NEXUS_COGNITION_MAX_JOB_BATCH", "16");
592 std::env::set_var("NEXUS_COGNITION_REPRESENTATION_MAX_ITEMS", "42");
593 std::env::set_var("NEXUS_COGNITION_INCLUDE_RAW_BY_DEFAULT", "true");
594
595 let config = Config::from_env().expect("config from env");
596 assert!(!config.cognition.derive_enabled);
597 assert_eq!(config.cognition.max_job_batch, 16);
598 assert_eq!(config.cognition.representation_max_items, 42);
599 assert!(config.cognition.include_raw_by_default);
600
601 std::env::remove_var("NEXUS_COGNITION_DERIVE_ENABLED");
602 std::env::remove_var("NEXUS_COGNITION_MAX_JOB_BATCH");
603 std::env::remove_var("NEXUS_COGNITION_REPRESENTATION_MAX_ITEMS");
604 std::env::remove_var("NEXUS_COGNITION_INCLUDE_RAW_BY_DEFAULT");
605 }
606
607 #[test]
608 fn test_embedding_config_from_env() {
609 std::env::set_var("NEXUS_EMBEDDINGS_ENABLED", "true");
610 std::env::set_var("NEXUS_EMBEDDING_BACKEND", "openai-compatible");
611 std::env::set_var("NEXUS_EMBEDDING_PROVIDER", "inherit");
612 std::env::set_var("NEXUS_EMBEDDING_MODEL", "text-embedding-004");
613 std::env::set_var("NEXUS_EMBEDDING_API_KEY_ENV", "GEMINI_API_KEY");
614 std::env::set_var(
615 "NEXUS_EMBEDDING_BASE_URL",
616 "https://generativelanguage.googleapis.com/v1beta/openai",
617 );
618 std::env::set_var("NEXUS_EMBEDDING_TIMEOUT_SECS", "45");
619
620 let config = Config::from_env().expect("config from env");
621 assert!(config.embedding.enabled);
622 assert_eq!(config.embedding.backend, "openai-compatible");
623 assert_eq!(config.embedding.provider, "inherit");
624 assert_eq!(config.embedding.model, "text-embedding-004");
625 assert_eq!(config.embedding.api_key_env, "GEMINI_API_KEY");
626 assert_eq!(
627 config.embedding.base_url.as_deref(),
628 Some("https://generativelanguage.googleapis.com/v1beta/openai")
629 );
630 assert_eq!(config.embedding.timeout_secs, 45);
631
632 std::env::remove_var("NEXUS_EMBEDDINGS_ENABLED");
633 std::env::remove_var("NEXUS_EMBEDDING_BACKEND");
634 std::env::remove_var("NEXUS_EMBEDDING_PROVIDER");
635 std::env::remove_var("NEXUS_EMBEDDING_MODEL");
636 std::env::remove_var("NEXUS_EMBEDDING_API_KEY_ENV");
637 std::env::remove_var("NEXUS_EMBEDDING_BASE_URL");
638 std::env::remove_var("NEXUS_EMBEDDING_TIMEOUT_SECS");
639 }
640}