use std::time::Duration;
use crate::config::helpers::{parse_bool_env, parse_option_env, parse_optional_env};
use crate::error::ConfigError;
use crate::settings::Settings;
#[derive(Debug, Clone)]
pub struct AgentConfig {
pub name: String,
pub max_parallel_jobs: usize,
pub job_timeout: Duration,
pub stuck_threshold: Duration,
pub repair_check_interval: Duration,
pub max_repair_attempts: u32,
pub use_planning: bool,
pub session_idle_timeout: Duration,
pub allow_local_tools: bool,
pub max_cost_per_day_cents: Option<u64>,
pub max_actions_per_hour: Option<u64>,
pub max_tool_iterations: usize,
pub auto_approve_tools: bool,
pub default_timezone: String,
pub max_tokens_per_job: u64,
}
impl AgentConfig {
#[cfg(feature = "libsql")]
pub fn for_testing() -> Self {
Self {
name: "test-rig".to_string(),
max_parallel_jobs: 1,
job_timeout: Duration::from_secs(30),
stuck_threshold: Duration::from_secs(300),
repair_check_interval: Duration::from_secs(3600),
max_repair_attempts: 0,
use_planning: false,
session_idle_timeout: Duration::from_secs(3600),
allow_local_tools: true,
max_cost_per_day_cents: None,
max_actions_per_hour: None,
max_tool_iterations: 10,
auto_approve_tools: true,
default_timezone: "UTC".to_string(),
max_tokens_per_job: 0,
}
}
pub(crate) fn resolve(settings: &Settings) -> Result<Self, ConfigError> {
Ok(Self {
name: parse_optional_env("AGENT_NAME", settings.agent.name.clone())?,
max_parallel_jobs: parse_optional_env(
"AGENT_MAX_PARALLEL_JOBS",
settings.agent.max_parallel_jobs as usize,
)?,
job_timeout: Duration::from_secs(parse_optional_env(
"AGENT_JOB_TIMEOUT_SECS",
settings.agent.job_timeout_secs,
)?),
stuck_threshold: Duration::from_secs(parse_optional_env(
"AGENT_STUCK_THRESHOLD_SECS",
settings.agent.stuck_threshold_secs,
)?),
repair_check_interval: Duration::from_secs(parse_optional_env(
"SELF_REPAIR_CHECK_INTERVAL_SECS",
settings.agent.repair_check_interval_secs,
)?),
max_repair_attempts: parse_optional_env(
"SELF_REPAIR_MAX_ATTEMPTS",
settings.agent.max_repair_attempts,
)?,
use_planning: parse_bool_env("AGENT_USE_PLANNING", settings.agent.use_planning)?,
session_idle_timeout: Duration::from_secs(parse_optional_env(
"SESSION_IDLE_TIMEOUT_SECS",
settings.agent.session_idle_timeout_secs,
)?),
allow_local_tools: parse_bool_env("ALLOW_LOCAL_TOOLS", false)?,
max_cost_per_day_cents: parse_option_env("MAX_COST_PER_DAY_CENTS")?,
max_actions_per_hour: parse_option_env("MAX_ACTIONS_PER_HOUR")?,
max_tool_iterations: parse_optional_env(
"AGENT_MAX_TOOL_ITERATIONS",
settings.agent.max_tool_iterations,
)?,
auto_approve_tools: parse_bool_env(
"AGENT_AUTO_APPROVE_TOOLS",
settings.agent.auto_approve_tools,
)?,
default_timezone: {
let tz: String = parse_optional_env(
"DEFAULT_TIMEZONE",
settings.agent.default_timezone.clone(),
)?;
if crate::timezone::parse_timezone(&tz).is_none() {
return Err(ConfigError::InvalidValue {
key: "DEFAULT_TIMEZONE".into(),
message: format!("invalid IANA timezone: '{tz}'"),
});
}
tz
},
max_tokens_per_job: parse_optional_env(
"AGENT_MAX_TOKENS_PER_JOB",
settings.agent.max_tokens_per_job,
)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_timezone_rejects_invalid() {
let mut settings = Settings::default();
settings.agent.default_timezone = "Fake/Zone".to_string();
let result = AgentConfig::resolve(&settings);
assert!(result.is_err(), "invalid IANA timezone should be rejected");
}
#[test]
fn test_default_timezone_accepts_valid() {
let settings = Settings::default(); let config = AgentConfig::resolve(&settings).expect("resolve");
assert_eq!(config.default_timezone, "UTC");
}
}