//! `loopd` configuration: read `config.toml` into a typed `Config`. //! //! This is our own format, so unknown keys are errors in every table: a //! misspelt `liveness_ms` that silently fell back to its default would be a //! limit the owner believes is set and is not. use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct Infer { pub socket: std::path::PathBuf, pub model: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct Slots { pub main: u32, pub background: u32, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct Expect { pub template_sha256: proto::Hash32, pub n_ctx: u64, pub slots: u32, } #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Sampling { pub temperature: f64, pub top_p: f64, pub top_k: u32, } impl Default for Sampling { fn default() -> Self { Self { temperature: 0.6, top_p: 0.95, top_k: 20, } } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Limits { pub poll_ms: u64, pub busy_wait_ms: u64, pub load_wait_ms: u64, pub idle_grace_ms: u64, pub liveness_ms: u64, pub thinking_cap: u64, pub thinking_overrun: u64, pub max_tokens: u64, pub queue_len: usize, pub retry_attempts: u32, pub retry_backoff_ms: Vec, pub retry_window_ms: u64, } impl Default for Limits { fn default() -> Self { Self { poll_ms: 5_000, busy_wait_ms: 600_000, load_wait_ms: 180_000, idle_grace_ms: 30_000, liveness_ms: 30_000, thinking_cap: 4_096, thinking_overrun: 256, max_tokens: 8_192, queue_len: 8, retry_attempts: 4, retry_backoff_ms: vec![2_000, 8_000, 30_000], retry_window_ms: 300_000, } } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)] #[serde(deny_unknown_fields, default)] pub struct Channel { pub socket: std::path::PathBuf, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Paths { pub home: std::path::PathBuf, } impl Default for Paths { fn default() -> Self { Self { home: std::env::var_os("BOXMAKER_HOME") .map(std::path::PathBuf::from) .unwrap_or_else(|| std::path::PathBuf::from("/var/lib/boxmaker")), } } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Loop { pub tool_iterations: u32, pub repeat_detection: bool, pub tool_result_cap: usize, } impl Default for Loop { fn default() -> Self { Self { tool_iterations: 8, repeat_detection: true, tool_result_cap: 16_384, } } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Baseline { pub system: std::path::PathBuf, } impl Default for Baseline { fn default() -> Self { Self { system: std::path::PathBuf::from("system.md"), } } } /// The tool broker (`brokerd`). No socket means no broker: every non-core tool call fails. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Broker { pub socket: Option, pub timeout_ms: u64, } impl Default for Broker { fn default() -> Self { Self { socket: None, timeout_ms: 120_000, } } } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { pub infer: Infer, pub slots: Slots, pub expect: Expect, #[serde(default)] pub sampling: Sampling, #[serde(default)] pub limits: Limits, #[serde(default)] pub paths: Paths, #[serde(default)] pub channel: Channel, #[serde(default)] pub r#loop: Loop, #[serde(default)] pub baseline: Baseline, #[serde(default)] pub broker: Broker, } #[derive(Debug)] pub enum ConfigError { Read(std::path::PathBuf, std::io::Error), Parse(std::path::PathBuf, toml::de::Error), } impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { ConfigError::Read(path, err) => write!(f, "{}: {err}", path.display()), ConfigError::Parse(path, err) => write!(f, "{}: {err}", path.display()), } } } impl std::error::Error for ConfigError {} impl Config { pub fn parse(text: &str) -> Result { toml::from_str(text) } pub fn load(path: &std::path::Path) -> Result { let text = std::fs::read_to_string(path).map_err(|e| ConfigError::Read(path.to_path_buf(), e))?; let mut config: Config = toml::from_str(&text).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))?; if let Some(parent) = path.parent() { config.baseline.system = parent.join(&config.baseline.system); } Ok(config) } pub fn channel_socket(&self) -> std::path::PathBuf { if self.channel.socket.as_os_str().is_empty() { self.paths.home.join("run/loop/loop.sock") } else { self.channel.socket.clone() } } }