use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::features::FeaturesToml;
use crate::hooks::HooksConfig;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SearchProvider {
#[default]
DuckDuckGo,
Bing,
Tavily,
Bocha,
Metaso,
Baidu,
Volcengine,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SearchConfig {
pub provider: Option<SearchProvider>,
pub api_key: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RetryConfig {
pub enabled: Option<bool>,
pub max_retries: Option<u32>,
pub initial_delay: Option<f64>,
pub max_delay: Option<f64>,
pub exponential_base: Option<f64>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TuiConfig {
pub alternate_screen: Option<String>,
pub mouse_capture: Option<bool>,
pub terminal_probe_timeout_ms: Option<u64>,
pub status_items: Option<Vec<StatusItem>>,
pub osc8_links: Option<bool>,
pub notification_condition: Option<NotificationCondition>,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NotificationCondition {
Always,
Never,
}
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum NotificationMethod {
#[default]
Auto,
Osc9,
Bel,
Off,
}
fn default_threshold_secs() -> u64 {
30
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct NotificationsConfig {
#[serde(default)]
pub method: NotificationMethod,
#[serde(default = "default_threshold_secs")]
pub threshold_secs: u64,
#[serde(default)]
pub include_summary: bool,
}
fn default_snapshots_enabled() -> bool {
true
}
fn default_snapshot_max_age_days() -> u64 {
crate::snapshot::DEFAULT_MAX_AGE.as_secs() / (24 * 60 * 60)
}
fn default_snapshot_max_workspace_gb() -> f64 {
zagens_runtime_adapters::snapshot::DEFAULT_SNAPSHOT_MAX_WORKSPACE_GB
}
#[derive(Debug, Clone, Deserialize)]
pub struct SnapshotsConfig {
#[serde(default = "default_snapshots_enabled")]
pub enabled: bool,
#[serde(default = "default_snapshot_max_age_days")]
pub max_age_days: u64,
#[serde(default = "default_snapshot_max_workspace_gb")]
pub max_workspace_gb: f64,
}
impl Default for SnapshotsConfig {
fn default() -> Self {
Self {
enabled: default_snapshots_enabled(),
max_age_days: default_snapshot_max_age_days(),
max_workspace_gb: default_snapshot_max_workspace_gb(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MemoryConfig {
#[serde(default)]
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TopicMemoryConfig {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub graph_path: Option<String>,
#[serde(default)]
pub inject_interval: Option<u32>,
#[serde(default)]
pub attribution: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SessionConfig {
#[serde(default = "default_session_max_file_mb")]
pub max_file_mb: u64,
}
fn default_session_max_file_mb() -> u64 {
5
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
max_file_mb: default_session_max_file_mb(),
}
}
}
impl SnapshotsConfig {
#[must_use]
pub fn max_age(&self) -> std::time::Duration {
std::time::Duration::from_secs(self.max_age_days.saturating_mul(24 * 60 * 60))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "snake_case")]
pub enum StatusItem {
Mode,
Model,
Cost,
Status,
Coherence,
Agents,
ReasoningReplay,
Cache,
ContextPercent,
GitBranch,
LastToolElapsed,
RateLimit,
}
impl StatusItem {
#[must_use]
pub fn default_footer() -> Vec<StatusItem> {
vec![
StatusItem::Mode,
StatusItem::Model,
StatusItem::Cost,
StatusItem::Status,
StatusItem::Coherence,
StatusItem::Agents,
StatusItem::ReasoningReplay,
StatusItem::Cache,
StatusItem::ContextPercent,
]
}
#[must_use]
pub fn key(self) -> &'static str {
match self {
StatusItem::Mode => "mode",
StatusItem::Model => "model",
StatusItem::Cost => "cost",
StatusItem::Status => "status",
StatusItem::Coherence => "coherence",
StatusItem::Agents => "agents",
StatusItem::ReasoningReplay => "reasoning_replay",
StatusItem::Cache => "cache",
StatusItem::ContextPercent => "context_percent",
StatusItem::GitBranch => "git_branch",
StatusItem::LastToolElapsed => "last_tool_elapsed",
StatusItem::RateLimit => "rate_limit",
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
StatusItem::Mode => "Mode",
StatusItem::Model => "Model",
StatusItem::Cost => "Session cost",
StatusItem::Status => "Activity (ready/draft/working)",
StatusItem::Coherence => "Coherence interventions",
StatusItem::Agents => "Sub-agents in flight",
StatusItem::ReasoningReplay => "Reasoning replay tokens",
StatusItem::Cache => "Prompt cache hit rate",
StatusItem::ContextPercent => "Context window %",
StatusItem::GitBranch => "Git branch",
StatusItem::LastToolElapsed => "Last tool elapsed",
StatusItem::RateLimit => "Rate-limit remaining",
}
}
#[must_use]
pub fn hint(self) -> &'static str {
match self {
StatusItem::Mode => "agent · yolo · plan",
StatusItem::Model => "the model id you'll send to",
StatusItem::Cost => "running total for this session",
StatusItem::Status => "what the agent is doing right now",
StatusItem::Coherence => "shown only when the engine intervenes",
StatusItem::Agents => "agents or RLM work in progress",
StatusItem::ReasoningReplay => "thinking tokens replayed each turn",
StatusItem::Cache => "% of prompt served from cache",
StatusItem::ContextPercent => "tokens used / model context window",
StatusItem::GitBranch => "current branch (placeholder)",
StatusItem::LastToolElapsed => "ms of the most recent tool call (placeholder)",
StatusItem::RateLimit => "remaining requests in the budget (placeholder)",
}
}
#[must_use]
pub fn all() -> &'static [StatusItem] {
&[
StatusItem::Mode,
StatusItem::Model,
StatusItem::Cost,
StatusItem::Status,
StatusItem::Coherence,
StatusItem::Agents,
StatusItem::ReasoningReplay,
StatusItem::Cache,
StatusItem::ContextPercent,
StatusItem::GitBranch,
StatusItem::LastToolElapsed,
StatusItem::RateLimit,
]
}
#[must_use]
pub fn is_left_cluster(self) -> bool {
matches!(
self,
StatusItem::Mode | StatusItem::Model | StatusItem::Cost | StatusItem::Status
)
}
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub enabled: bool,
pub max_retries: u32,
pub initial_delay: f64,
pub max_delay: f64,
pub exponential_base: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CapacityConfig {
pub enabled: Option<bool>,
pub low_risk_max: Option<f64>,
pub medium_risk_max: Option<f64>,
pub severe_min_slack: Option<f64>,
pub severe_violation_ratio: Option<f64>,
pub refresh_cooldown_turns: Option<u64>,
pub replan_cooldown_turns: Option<u64>,
pub max_replay_per_turn: Option<usize>,
pub min_turns_before_guardrail: Option<u64>,
pub profile_window: Option<usize>,
pub deepseek_v3_2_chat_prior: Option<f64>,
pub deepseek_v3_2_reasoner_prior: Option<f64>,
pub deepseek_v4_pro_prior: Option<f64>,
pub deepseek_v4_flash_prior: Option<f64>,
pub fallback_default_prior: Option<f64>,
}
impl RetryPolicy {
#[must_use]
#[allow(dead_code)] pub fn delay_for_attempt(&self, attempt: u32) -> std::time::Duration {
let exponent = i32::try_from(attempt).unwrap_or(i32::MAX);
let delay = self.initial_delay * self.exponential_base.powi(exponent);
let delay = delay.min(self.max_delay);
let delay = delay.clamp(0.0, 300.0);
std::time::Duration::from_secs_f64(delay)
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CompactionConfigToml {
#[serde(default)]
pub auto_compact: Option<bool>,
#[serde(default)]
pub token_threshold: Option<usize>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ContextConfig {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub verbatim_window_turns: Option<usize>,
#[serde(default)]
pub l1_threshold: Option<usize>,
#[serde(default)]
pub l2_threshold: Option<usize>,
#[serde(default)]
pub l3_threshold: Option<usize>,
#[serde(default)]
pub cycle_threshold: Option<usize>,
#[serde(default)]
pub seam_model: Option<String>,
#[serde(default)]
pub per_model: Option<HashMap<String, PerModelContextConfig>>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct SubagentsConfig {
#[serde(default)]
pub default_model: Option<String>,
#[serde(default)]
pub worker_model: Option<String>,
#[serde(default)]
pub explorer_model: Option<String>,
#[serde(default)]
pub awaiter_model: Option<String>,
#[serde(default)]
pub review_model: Option<String>,
#[serde(default)]
pub implementer_model: Option<String>,
#[serde(default)]
pub verifier_model: Option<String>,
#[serde(default)]
pub auditor_model: Option<String>,
#[serde(default)]
pub custom_model: Option<String>,
#[serde(default)]
pub models: Option<HashMap<String, String>>,
#[serde(default)]
pub max_concurrent: Option<usize>,
#[serde(default)]
pub step_timeout_secs: Option<u64>,
#[serde(default)]
pub heartbeat_timeout_secs: Option<u64>,
}
pub const DEFAULT_SUBAGENT_STEP_TIMEOUT_SECS: u64 = 600;
pub const MIN_SUBAGENT_STEP_TIMEOUT_SECS: u64 = 120;
pub const MAX_SUBAGENT_STEP_TIMEOUT_SECS: u64 = 1800;
pub const DEFAULT_SUBAGENT_HEARTBEAT_TIMEOUT_SECS: u64 = 300;
pub const MIN_SUBAGENT_HEARTBEAT_TIMEOUT_SECS: u64 = 60;
pub const MAX_SUBAGENT_HEARTBEAT_TIMEOUT_SECS: u64 = 3600;
#[derive(Debug, Clone, Deserialize)]
pub struct PerModelContextConfig {
#[serde(default)]
pub l1_threshold: Option<usize>,
#[serde(default)]
pub l2_threshold: Option<usize>,
#[serde(default)]
pub l3_threshold: Option<usize>,
#[serde(default)]
pub cycle_threshold: Option<usize>,
}
#[derive(Clone, Default, Deserialize)]
pub struct Config {
pub provider: Option<String>,
pub api_key: Option<String>,
pub base_url: Option<String>,
pub http_headers: Option<HashMap<String, String>>,
pub default_text_model: Option<String>,
pub reasoning_effort: Option<String>,
#[serde(default)]
pub cost_currency: Option<String>,
pub tools_file: Option<String>,
pub skills_dir: Option<String>,
pub mcp_config_path: Option<String>,
pub notes_path: Option<String>,
pub memory_path: Option<String>,
pub strict_tool_mode: Option<bool>,
pub instructions: Option<Vec<String>>,
pub allow_shell: Option<bool>,
pub approval_policy: Option<String>,
pub sandbox_mode: Option<String>,
#[serde(default)]
pub prefer_bwrap: Option<bool>,
pub sandbox_backend: Option<String>,
pub sandbox_url: Option<String>,
pub sandbox_api_key: Option<String>,
pub managed_config_path: Option<String>,
pub requirements_path: Option<String>,
pub max_subagents: Option<usize>,
pub retry: Option<RetryConfig>,
pub capacity: Option<CapacityConfig>,
pub features: Option<FeaturesToml>,
pub tui: Option<TuiConfig>,
#[serde(default)]
pub hooks: Option<HooksConfig>,
#[serde(default)]
pub providers: Option<ProvidersConfig>,
#[serde(default)]
pub vision: Option<VisionConfig>,
#[serde(default)]
pub notifications: Option<NotificationsConfig>,
#[serde(default)]
pub network: Option<NetworkPolicyToml>,
#[serde(default)]
pub skills: Option<SkillsConfig>,
#[serde(default)]
pub snapshots: Option<SnapshotsConfig>,
#[serde(default)]
pub search: Option<SearchConfig>,
#[serde(default)]
pub memory: Option<MemoryConfig>,
#[serde(default)]
pub topic_memory: Option<TopicMemoryConfig>,
#[serde(default)]
pub session: Option<SessionConfig>,
#[serde(default)]
pub lsp: Option<LspConfigToml>,
#[serde(default)]
pub context: ContextConfig,
#[serde(default)]
pub subagents: Option<SubagentsConfig>,
#[serde(default)]
pub runtime_api: Option<RuntimeApiConfig>,
#[serde(default)]
pub workshop: Option<crate::tools::large_output_router::WorkshopConfig>,
#[serde(default)]
pub scratchpad: Option<crate::scratchpad::ScratchpadConfigToml>,
#[serde(default)]
pub long_horizon: Option<zagens_core::long_horizon::LongHorizonConfigToml>,
#[serde(default)]
pub compaction: Option<CompactionConfigToml>,
#[serde(default)]
pub windows: Option<zagens_config::WindowsConfigToml>,
#[serde(default)]
pub tools: Option<ToolsConfigToml>,
#[serde(default)]
pub kernel: Option<KernelConfigToml>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ToolsConfigToml {
#[serde(default)]
pub policy: Option<String>,
#[serde(default)]
pub scheduler: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
}
pub use zagens_core::engine::KernelMachineMode;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct KernelConfigToml {
#[serde(default)]
pub machine: Option<String>,
#[serde(default)]
pub log_transcript_repair: Option<bool>,
#[serde(default)]
pub log_transcript_repair_persist: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ToolsPolicyMode {
Legacy,
Shadow,
#[default]
Engine,
}
impl ToolsPolicyMode {
#[must_use]
pub fn parse(value: Option<&str>) -> Self {
match value.map(str::trim).map(str::to_ascii_lowercase).as_deref() {
Some("legacy") => Self::Legacy,
Some("shadow") => Self::Shadow,
_ => Self::Engine,
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Legacy => "legacy",
Self::Shadow => "shadow",
Self::Engine => "engine",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ToolsSchedulerMode {
Legacy,
Shadow,
#[default]
Dag,
}
impl ToolsSchedulerMode {
#[must_use]
pub fn parse(value: Option<&str>) -> Self {
match value.map(str::trim).map(str::to_ascii_lowercase).as_deref() {
Some("legacy") => Self::Legacy,
Some("shadow") => Self::Shadow,
Some("dag") => Self::Dag,
_ => Self::Dag,
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Legacy => "legacy",
Self::Shadow => "shadow",
Self::Dag => "dag",
}
}
#[must_use]
pub fn uses_dag_groups(self) -> bool {
matches!(self, Self::Dag)
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct RuntimeApiConfig {
#[serde(default)]
pub cors_origins: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct SkillsConfig {
#[serde(default)]
pub registry_url: Option<String>,
#[serde(default)]
pub max_install_size_bytes: Option<u64>,
}
impl SkillsConfig {
#[must_use]
pub fn registry_url(&self) -> String {
self.registry_url
.clone()
.unwrap_or_else(|| crate::skills::install::DEFAULT_REGISTRY_URL.to_string())
}
#[must_use]
pub fn max_install_size_bytes(&self) -> u64 {
self.max_install_size_bytes
.unwrap_or(crate::skills::install::DEFAULT_MAX_SIZE_BYTES)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct NetworkPolicyToml {
#[serde(default = "default_network_decision")]
pub default: String,
#[serde(default)]
pub allow: Vec<String>,
#[serde(default)]
pub deny: Vec<String>,
#[serde(default = "default_network_audit")]
pub audit: bool,
}
fn default_network_decision() -> String {
"prompt".to_string()
}
fn default_network_audit() -> bool {
true
}
impl Default for NetworkPolicyToml {
fn default() -> Self {
Self {
default: default_network_decision(),
allow: Vec::new(),
deny: Vec::new(),
audit: default_network_audit(),
}
}
}
impl NetworkPolicyToml {
#[must_use]
pub fn into_runtime(self) -> crate::network_policy::NetworkPolicy {
crate::network_policy::NetworkPolicy {
default: crate::network_policy::Decision::parse(&self.default).into(),
allow: self.allow,
deny: self.deny,
audit: self.audit,
}
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct LspConfigToml {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub poll_after_edit_ms: Option<u64>,
#[serde(default)]
pub max_diagnostics_per_file: Option<usize>,
#[serde(default)]
pub include_warnings: Option<bool>,
#[serde(default)]
pub servers: Option<HashMap<String, Vec<String>>>,
}
impl LspConfigToml {
#[must_use]
pub fn into_runtime(self) -> crate::lsp::LspConfig {
let defaults = crate::lsp::LspConfig::default();
crate::lsp::LspConfig {
enabled: self.enabled.unwrap_or(defaults.enabled),
poll_after_edit_ms: self
.poll_after_edit_ms
.unwrap_or(defaults.poll_after_edit_ms),
max_diagnostics_per_file: self
.max_diagnostics_per_file
.unwrap_or(defaults.max_diagnostics_per_file),
include_warnings: self.include_warnings.unwrap_or(defaults.include_warnings),
servers: self.servers.unwrap_or_default(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ProviderConfig {
pub api_key: Option<String>,
pub base_url: Option<String>,
pub model: Option<String>,
pub http_headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct VisionConfig {
pub api_key: Option<String>,
pub base_url: Option<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ProvidersConfig {
#[serde(default)]
pub deepseek: ProviderConfig,
#[serde(default)]
pub deepseek_cn: ProviderConfig,
#[serde(default)]
pub nvidia_nim: ProviderConfig,
#[serde(default)]
pub openai: ProviderConfig,
#[serde(default)]
pub openrouter: ProviderConfig,
#[serde(default)]
pub novita: ProviderConfig,
#[serde(default)]
pub fireworks: ProviderConfig,
#[serde(default)]
pub sglang: ProviderConfig,
#[serde(default)]
pub vllm: ProviderConfig,
#[serde(default)]
pub ollama: ProviderConfig,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub(crate) struct ConfigFile {
#[serde(flatten)]
pub(crate) base: Config,
pub(crate) profiles: Option<HashMap<String, Config>>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub(crate) struct RequirementsFile {
#[serde(default)]
pub(crate) allowed_approval_policies: Vec<String>,
#[serde(default)]
pub(crate) allowed_sandbox_modes: Vec<String>,
#[serde(default)]
pub(crate) allowed_windows_sandbox_modes: Vec<String>,
#[serde(default)]
pub(crate) require_windows_sandbox_setup: bool,
}
fn debug_redact_secret(value: &Option<String>) -> &'static str {
if value.as_ref().is_some_and(|s| !s.is_empty()) {
"<redacted>"
} else {
"None"
}
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("provider", &self.provider)
.field("api_key", &debug_redact_secret(&self.api_key))
.field(
"sandbox_api_key",
&debug_redact_secret(&self.sandbox_api_key),
)
.field("default_text_model", &self.default_text_model)
.field("allow_shell", &self.allow_shell)
.field("approval_policy", &self.approval_policy)
.field("sandbox_mode", &self.sandbox_mode)
.finish_non_exhaustive()
}
}