mod codex;
mod custom;
mod opencode;
pub use codex::CodexConfig;
pub use custom::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
pub use opencode::OpenCodeConfig;
pub(crate) use codex::CodexMode;
pub(crate) use opencode::OpenCodeMode;
use std::num::NonZeroUsize;
use std::time::Duration;
use crate::KitError;
#[derive(Clone, Debug)]
pub enum BackendConfig {
Codex(
CodexConfig,
),
OpenCode(
OpenCodeConfig,
),
Custom(
CustomAgentConfig,
),
}
#[derive(Clone, Debug)]
pub struct AgentConfig {
backend: BackendConfig,
display_name: Option<String>,
instructions: Option<String>,
tracing_enabled: bool,
lifecycle_timeout: Duration,
prompt_limit: NonZeroUsize,
default_prompt_timeout: Option<Duration>,
}
impl AgentConfig {
pub fn new(
backend: BackendConfig,
lifecycle_timeout: Duration,
prompt_limit: NonZeroUsize,
) -> Result<Self, KitError> {
let config = Self {
backend,
display_name: None,
instructions: None,
tracing_enabled: false,
lifecycle_timeout,
prompt_limit,
default_prompt_timeout: None,
};
config.validate()?;
Ok(config)
}
pub fn with_display_name(mut self, display_name: impl Into<String>) -> Result<Self, KitError> {
let display_name = display_name.into();
Self::validate_optional_text("agent display name", Some(&display_name))?;
self.display_name = Some(display_name);
Ok(self)
}
pub fn with_instructions(mut self, instructions: impl Into<String>) -> Result<Self, KitError> {
let instructions = instructions.into();
Self::validate_optional_text("agent instructions", Some(&instructions))?;
self.instructions = Some(instructions);
Ok(self)
}
pub fn with_tracing_enabled(mut self, tracing_enabled: bool) -> Self {
self.tracing_enabled = tracing_enabled;
self
}
pub fn with_default_prompt_timeout(mut self, timeout: Duration) -> Result<Self, KitError> {
if timeout.is_zero() {
return Err(KitError::invalid_config(
"agent default prompt timeout must be greater than zero",
));
}
self.default_prompt_timeout = Some(timeout);
Ok(self)
}
pub fn backend(&self) -> &BackendConfig {
&self.backend
}
pub fn display_name(&self) -> Option<&str> {
self.display_name.as_deref()
}
pub fn instructions(&self) -> Option<&str> {
self.instructions.as_deref()
}
pub fn tracing_enabled(&self) -> bool {
self.tracing_enabled
}
pub fn lifecycle_timeout(&self) -> Duration {
self.lifecycle_timeout
}
pub fn prompt_limit(&self) -> NonZeroUsize {
self.prompt_limit
}
#[must_use]
pub fn default_prompt_timeout(&self) -> Option<Duration> {
self.default_prompt_timeout
}
pub(crate) fn validate(&self) -> Result<(), KitError> {
if self.lifecycle_timeout.is_zero() {
return Err(KitError::invalid_config(
"agent lifecycle timeout must be greater than zero",
));
}
if self
.default_prompt_timeout
.is_some_and(|timeout| timeout.is_zero())
{
return Err(KitError::invalid_config(
"agent default prompt timeout must be greater than zero",
));
}
Self::validate_optional_text("agent display name", self.display_name.as_deref())?;
Self::validate_optional_text("agent instructions", self.instructions.as_deref())?;
match &self.backend {
BackendConfig::Codex(config) => config.validate(),
BackendConfig::OpenCode(config) => config.validate(),
BackendConfig::Custom(config) => config.validate(),
}
}
fn validate_optional_text(label: &str, value: Option<&str>) -> Result<(), KitError> {
if value.is_some_and(|value| value.is_empty() || value.trim() != value) {
return Err(KitError::invalid_config(format!(
"{label} must be nonempty and unpadded when present"
)));
}
Ok(())
}
}
impl From<CodexConfig> for BackendConfig {
fn from(config: CodexConfig) -> Self {
Self::Codex(config)
}
}
impl From<OpenCodeConfig> for BackendConfig {
fn from(config: OpenCodeConfig) -> Self {
Self::OpenCode(config)
}
}
impl From<CustomAgentConfig> for BackendConfig {
fn from(config: CustomAgentConfig) -> Self {
Self::Custom(config)
}
}