use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum RuntimeMode {
#[default]
#[serde(rename = "local")]
Local,
#[serde(rename = "airgapped")]
AirGapped,
#[serde(rename = "cloud")]
Cloud,
}
impl FromStr for RuntimeMode {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(match value.to_ascii_lowercase().as_str() {
"airgapped" => RuntimeMode::AirGapped,
"cloud" => RuntimeMode::Cloud,
_ => RuntimeMode::Local,
})
}
}
impl fmt::Display for RuntimeMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RuntimeMode::Local => write!(f, "local"),
RuntimeMode::AirGapped => write!(f, "airgapped"),
RuntimeMode::Cloud => write!(f, "cloud"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Providers {
pub azure: Option<AzureProvider>,
pub anthropic: Option<AnthropicProvider>,
pub openai: Option<OpenAIProvider>,
pub ollama: Option<OllamaProvider>,
pub google: Option<GoogleProvider>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AzureProvider {
pub endpoint: Option<String>,
pub api_key: Option<String>,
pub deployment_name: Option<String>,
#[serde(default = "default_api_version")]
pub api_version: String,
}
fn default_api_version() -> String {
"2024-02-15-preview".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AnthropicProvider {
pub api_key: Option<String>,
#[serde(default = "default_anthropic_base_url")]
pub base_url: String,
}
fn default_anthropic_base_url() -> String {
"https://api.anthropic.com".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpenAIProvider {
pub api_key: Option<String>,
#[serde(default = "default_openai_base_url")]
pub base_url: String,
pub organization: Option<String>,
}
fn default_openai_base_url() -> String {
"https://api.openai.com/v1".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OllamaProvider {
#[serde(default = "default_ollama_base_url")]
pub base_url: String,
}
fn default_ollama_base_url() -> String {
"http://localhost:11434".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GoogleProvider {
pub api_key: Option<String>,
#[serde(default = "default_google_base_url")]
pub base_url: String,
}
fn default_google_base_url() -> String {
"https://generativelanguage.googleapis.com/v1".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Runtime {
#[serde(default)]
pub mode: RuntimeMode,
#[serde(default = "default_max_concurrent")]
pub max_concurrent_executions: u32,
#[serde(default = "default_timeout")]
pub default_timeout: u64,
#[serde(default = "default_true")]
pub enable_telemetry: bool,
#[serde(default = "default_true")]
pub allow_network: bool,
}
fn default_max_concurrent() -> u32 {
10
}
fn default_timeout() -> u64 {
30000
}
fn default_true() -> bool {
true
}
impl Default for Runtime {
fn default() -> Self {
Self {
mode: RuntimeMode::Local,
max_concurrent_executions: 10,
default_timeout: 30000,
enable_telemetry: true,
allow_network: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Storage {
#[serde(default = "default_event_store")]
pub event_store: EventStore,
#[serde(default = "default_state_store")]
pub state_store: StateStore,
#[serde(default = "default_filesystem_store")]
pub artifact_store: ArtifactStore,
#[serde(default = "default_sqlite_vector_store")]
pub vector_store: VectorStore,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EventStore {
#[serde(default = "default_sqlite")]
pub r#type: String,
pub path: Option<String>,
pub dsn: Option<String>,
}
fn default_sqlite() -> String {
"sqlite".to_string()
}
impl Default for EventStore {
fn default() -> Self {
Self {
r#type: "jsonl".to_string(),
path: Some("events".to_string()),
dsn: None,
}
}
}
fn default_event_store() -> EventStore {
EventStore::default()
}
impl Default for StateStore {
fn default() -> Self {
Self {
r#type: "jsonl".to_string(),
path: Some("state".to_string()),
dsn: None,
}
}
}
fn default_state_store() -> StateStore {
StateStore::default()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StateStore {
#[serde(default = "default_sqlite")]
pub r#type: String,
pub path: Option<String>,
pub dsn: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ArtifactStore {
#[serde(default = "default_filesystem")]
pub r#type: String,
pub path: Option<String>,
#[serde(default = "default_zstd")]
pub compression: String,
}
fn default_filesystem() -> String {
"filesystem".to_string()
}
fn default_zstd() -> String {
"zstd".to_string()
}
impl Default for ArtifactStore {
fn default() -> Self {
Self {
r#type: "filesystem".to_string(),
path: None,
compression: "zstd".to_string(),
}
}
}
fn default_filesystem_store() -> ArtifactStore {
ArtifactStore::default()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VectorStore {
#[serde(default = "default_sqlite")]
pub r#type: String,
pub url: Option<String>,
pub collection: Option<String>,
pub path: Option<String>,
pub dsn: Option<String>,
}
impl Default for VectorStore {
fn default() -> Self {
Self {
r#type: "sqlite".to_string(),
url: None,
collection: None,
path: None,
dsn: None,
}
}
}
fn default_sqlite_vector_store() -> VectorStore {
VectorStore::default()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Tools {
#[serde(default)]
pub ingestion: IngestionTools,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct IngestionTools {
#[serde(default = "default_pdf")]
pub pdf: PdfIngestion,
#[serde(default = "default_ocr")]
pub ocr: OcrIngestion,
#[serde(default = "default_embeddings")]
pub embeddings: EmbeddingsIngestion,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PdfIngestion {
#[serde(default = "default_pdfium")]
pub engine: String,
}
impl Default for PdfIngestion {
fn default() -> Self {
Self {
engine: "pdfium".to_string(),
}
}
}
fn default_pdfium() -> String {
"pdfium".to_string()
}
fn default_pdf() -> PdfIngestion {
PdfIngestion {
engine: "pdfium".to_string(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OcrIngestion {
#[serde(default = "default_tesseract")]
pub engine: String,
#[serde(default = "default_languages")]
pub languages: Vec<String>,
}
impl Default for OcrIngestion {
fn default() -> Self {
Self {
engine: "tesseract".to_string(),
languages: vec!["eng".to_string()],
}
}
}
fn default_tesseract() -> String {
"tesseract".to_string()
}
fn default_languages() -> Vec<String> {
vec!["eng".to_string()]
}
fn default_ocr() -> OcrIngestion {
OcrIngestion {
engine: "tesseract".to_string(),
languages: vec!["eng".to_string()],
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbeddingsIngestion {
#[serde(default = "default_fastembed")]
pub engine: String,
pub model: Option<String>,
}
impl Default for EmbeddingsIngestion {
fn default() -> Self {
Self {
engine: "fastembed".to_string(),
model: None,
}
}
}
fn default_fastembed() -> String {
"fastembed".to_string()
}
fn default_embeddings() -> EmbeddingsIngestion {
EmbeddingsIngestion {
engine: "fastembed".to_string(),
model: None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Cloud {
pub api_url: Option<String>,
pub tenant_id: Option<String>,
#[serde(default)]
pub auto_sync: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApprovalConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_approval_policy")]
pub policy: String,
pub max_steps: Option<usize>,
pub require_patterns: Option<Vec<String>>,
#[serde(default = "default_approval_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub tool_overrides: Option<std::collections::HashMap<String, String>>,
}
fn default_approval_policy() -> String {
"always_approve".to_string()
}
fn default_approval_timeout() -> u64 {
300 }
impl Default for ApprovalConfig {
fn default() -> Self {
Self {
enabled: false,
policy: "always_approve".to_string(),
max_steps: None,
require_patterns: None,
timeout_seconds: 300,
tool_overrides: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MemoryConfig {
#[serde(default = "default_memory_backend")]
pub backend: String,
#[serde(default = "default_daily_logs_dir")]
pub daily_logs_dir: String,
#[serde(default = "default_max_daily_entries")]
pub max_daily_entries: usize,
#[serde(default)]
pub auto_consolidate: bool,
pub consolidation_time: Option<String>,
pub retention_days: Option<u32>,
#[serde(default = "default_true")]
pub include_timestamps: bool,
}
fn default_memory_backend() -> String {
"markdown".to_string()
}
fn default_daily_logs_dir() -> String {
"memory".to_string()
}
fn default_max_daily_entries() -> usize {
100
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
backend: "markdown".to_string(),
daily_logs_dir: "memory".to_string(),
max_daily_entries: 100,
auto_consolidate: true,
consolidation_time: Some("03:00".to_string()),
retention_days: Some(30),
include_timestamps: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionConfig {
#[serde(default = "default_max_turns")]
pub max_turns: usize,
#[serde(default = "default_rotation_threshold_pct")]
pub rotation_threshold_pct: u8,
#[serde(default = "default_idle_timeout_secs")]
pub idle_timeout_secs: u64,
#[serde(default = "default_cleanup_interval_secs")]
pub cleanup_interval_secs: u64,
#[serde(default = "default_cleanup_idle_threshold_secs")]
pub cleanup_idle_threshold_secs: u64,
}
fn default_max_turns() -> usize {
20
}
fn default_rotation_threshold_pct() -> u8 {
80
}
fn default_idle_timeout_secs() -> u64 {
1800 }
fn default_cleanup_interval_secs() -> u64 {
300 }
fn default_cleanup_idle_threshold_secs() -> u64 {
3600 }
impl Default for SessionConfig {
fn default() -> Self {
Self {
max_turns: default_max_turns(),
rotation_threshold_pct: default_rotation_threshold_pct(),
idle_timeout_secs: default_idle_timeout_secs(),
cleanup_interval_secs: default_cleanup_interval_secs(),
cleanup_idle_threshold_secs: default_cleanup_idle_threshold_secs(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SessionConfigOverride {
pub max_turns: Option<usize>,
pub rotation_threshold_pct: Option<u8>,
pub idle_timeout_secs: Option<u64>,
pub cleanup_interval_secs: Option<u64>,
pub cleanup_idle_threshold_secs: Option<u64>,
}
impl SessionConfigOverride {
pub fn apply_to(&self, base: &SessionConfig) -> SessionConfig {
SessionConfig {
max_turns: self.max_turns.unwrap_or(base.max_turns),
rotation_threshold_pct: self
.rotation_threshold_pct
.unwrap_or(base.rotation_threshold_pct),
idle_timeout_secs: self.idle_timeout_secs.unwrap_or(base.idle_timeout_secs),
cleanup_interval_secs: self
.cleanup_interval_secs
.unwrap_or(base.cleanup_interval_secs),
cleanup_idle_threshold_secs: self
.cleanup_idle_threshold_secs
.unwrap_or(base.cleanup_idle_threshold_secs),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServerConfig {
#[serde(default = "default_server_port")]
pub port: u16,
#[serde(default = "default_server_host")]
pub host: String,
#[serde(default = "default_grpc_port")]
pub grpc_port: u16,
#[serde(default = "default_docs_port")]
pub docs_port: u16,
#[serde(default)]
pub docs_base_url: Option<String>,
}
fn default_server_port() -> u16 {
8080
}
fn default_server_host() -> String {
"0.0.0.0".to_string()
}
fn default_grpc_port() -> u16 {
50051
}
fn default_docs_port() -> u16 {
1111
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
port: default_server_port(),
host: default_server_host(),
grpc_port: default_grpc_port(),
docs_port: default_docs_port(),
docs_base_url: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LoggingConfig {
#[serde(default = "default_daemon_log")]
pub daemon_log: String,
#[serde(default = "default_serve_log")]
pub serve_log: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ObservabilityConfig {
#[serde(default = "default_true")]
pub trace_llm_calls: bool,
#[serde(default)]
pub log_full_prompts: bool,
#[serde(default)]
pub log_full_responses: bool,
#[serde(default = "default_true")]
pub track_token_usage: bool,
#[serde(default = "default_true")]
pub trace_memory_access: bool,
#[serde(default)]
pub enable_context_snapshots: bool,
#[serde(default)]
pub capture_reasoning_traces: bool,
#[serde(default = "default_max_content_length")]
pub max_content_length: usize,
}
fn default_max_content_length() -> usize {
1000
}
impl Default for ObservabilityConfig {
fn default() -> Self {
Self {
trace_llm_calls: true,
log_full_prompts: false,
log_full_responses: false,
track_token_usage: true,
trace_memory_access: true,
enable_context_snapshots: false,
capture_reasoning_traces: false,
max_content_length: 1000,
}
}
}
fn default_daemon_log() -> String {
"logs/daemon.log".to_string()
}
fn default_serve_log() -> String {
"logs/serve.log".to_string()
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
daemon_log: default_daemon_log(),
serve_log: default_serve_log(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub default_model_id: Option<String>,
#[serde(default)]
pub providers: Providers,
#[serde(default)]
pub runtime: Runtime,
#[serde(default)]
pub storage: Storage,
#[serde(default)]
pub tools: Tools,
pub cloud: Option<Cloud>,
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub approval: ApprovalConfig,
#[serde(default)]
pub memory: MemoryConfig,
#[serde(default)]
pub session: SessionConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub observability: ObservabilityConfig,
}
impl Config {
pub fn load_from_home() -> Result<Self> {
let path = crate::home::enact_home().join("config.yaml");
Self::load_from_yaml_path(&path)
}
pub fn ensure_default_at(path: &Path) -> Result<()> {
if path.exists() {
return Ok(());
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("Failed to create config directory")?;
}
Self::default().save_to_yaml_path(path)
}
pub fn save_to_home(&self) -> Result<()> {
crate::home::create_config_backup()?;
let path = crate::home::enact_home().join("config.yaml");
self.save_to_yaml_path(&path)
}
pub fn load_from_yaml_path(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
let s = std::fs::read_to_string(path).context("Failed to read config file")?;
let config: Config = serde_yaml::from_str(&s).context("Failed to parse config YAML")?;
Ok(config)
}
pub fn save_to_yaml_path(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("Failed to create config directory")?;
}
let s = serde_yaml::to_string(self).context("Failed to serialize config to YAML")?;
std::fs::write(path, s).context("Failed to write config file")?;
Ok(())
}
}