Skip to main content

agents_kit/config/
mod.rs

1//! Validated passive configuration for Agent construction.
2//!
3//! Constructing these values performs no process startup or network I/O.
4//! [`crate::AgentBuilder::build`] owns that work. Choose one [`BackendConfig`],
5//! set lifecycle and prompt admission bounds, then resupply integrations when
6//! reopening an existing Agent. Workspace and prompt defaults remain separate.
7
8mod codex;
9mod custom;
10mod opencode;
11
12pub use codex::CodexConfig;
13pub use custom::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
14pub use opencode::OpenCodeConfig;
15
16pub(crate) use codex::CodexMode;
17pub(crate) use opencode::OpenCodeMode;
18
19use std::num::NonZeroUsize;
20use std::time::Duration;
21
22use crate::KitError;
23
24/// Chooses one backend and carries only that backend's passive configuration.
25#[derive(Clone, Debug)]
26pub enum BackendConfig {
27    /// Uses a Codex app server.
28    Codex(
29        /// The Codex connection configuration.
30        CodexConfig,
31    ),
32    /// Uses an OpenCode server.
33    OpenCode(
34        /// The OpenCode process or attachment configuration.
35        OpenCodeConfig,
36    ),
37    /// Uses the provider-driven Custom backend.
38    Custom(
39        /// Passive Custom backend policy configuration.
40        CustomAgentConfig,
41    ),
42}
43
44/// Portable agent-wide runtime configuration.
45///
46/// Workspace model, reasoning-effort, and knowledge defaults belong to
47/// [`crate::workspace::WorkspaceDefaults`], not this type.
48///
49/// # Examples
50///
51/// ```
52/// use std::num::NonZeroUsize;
53/// use std::time::Duration;
54/// use agents_kit::{
55///     config::{AgentConfig, CodexConfig},
56///     KitError,
57/// };
58///
59/// let agent_config = || -> Result<AgentConfig, KitError> {
60///     let backend = CodexConfig::spawn(
61///         Duration::from_secs(15),
62///         Duration::from_secs(60),
63///     )?;
64///     AgentConfig::new(
65///         backend.into(),
66///         Duration::from_secs(60),
67///         NonZeroUsize::new(1).expect("one is nonzero"),
68///     )?
69///     .with_display_name("documentation-agent")
70/// };
71/// let config = agent_config()?;
72/// assert_eq!(config.display_name(), Some("documentation-agent"));
73/// # Ok::<(), KitError>(())
74/// ```
75#[derive(Clone, Debug)]
76pub struct AgentConfig {
77    backend: BackendConfig,
78    display_name: Option<String>,
79    instructions: Option<String>,
80    tracing_enabled: bool,
81    lifecycle_timeout: Duration,
82    prompt_limit: NonZeroUsize,
83    default_prompt_timeout: Option<Duration>,
84}
85
86impl AgentConfig {
87    /// Creates agent-wide configuration with tracing disabled and no optional text.
88    ///
89    /// # Errors
90    /// Returns [`crate::error::ErrorKind::InvalidConfig`] when the lifecycle timeout is zero or the selected backend configuration is invalid.
91    ///
92    /// ```
93    /// use agents_kit::config::{AgentConfig, CodexConfig};
94    /// use std::{num::NonZeroUsize, time::Duration};
95    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
96    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
97    /// assert_eq!(config.lifecycle_timeout(), Duration::from_secs(30));
98    /// assert_eq!(config.prompt_limit().get(), 1);
99    /// # Ok::<(), agents_kit::KitError>(())
100    /// ```
101    pub fn new(
102        backend: BackendConfig,
103        lifecycle_timeout: Duration,
104        prompt_limit: NonZeroUsize,
105    ) -> Result<Self, KitError> {
106        let config = Self {
107            backend,
108            display_name: None,
109            instructions: None,
110            tracing_enabled: false,
111            lifecycle_timeout,
112            prompt_limit,
113            default_prompt_timeout: None,
114        };
115        config.validate()?;
116        Ok(config)
117    }
118
119    /// Sets a nonempty, unpadded display name.
120    ///
121    /// # Errors
122    /// Returns [`crate::error::ErrorKind::InvalidConfig`] for an empty or padded display name.
123    ///
124    /// ```
125    /// use agents_kit::config::{AgentConfig, CodexConfig};
126    /// use std::{num::NonZeroUsize, time::Duration};
127    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
128    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
129    /// let config = config.with_display_name("worker")?;
130    /// assert_eq!(config.display_name(), Some("worker"));
131    /// # Ok::<(), agents_kit::KitError>(())
132    /// ```
133    pub fn with_display_name(mut self, display_name: impl Into<String>) -> Result<Self, KitError> {
134        let display_name = display_name.into();
135        Self::validate_optional_text("agent display name", Some(&display_name))?;
136        self.display_name = Some(display_name);
137        Ok(self)
138    }
139
140    /// Sets nonempty, unpadded agent-level instructions.
141    ///
142    /// # Errors
143    /// Returns [`crate::error::ErrorKind::InvalidConfig`] for empty or padded instructions.
144    ///
145    /// ```
146    /// use agents_kit::config::{AgentConfig, CodexConfig};
147    /// use std::{num::NonZeroUsize, time::Duration};
148    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
149    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
150    /// let config = config.with_instructions("Use concise answers.")?;
151    /// assert_eq!(config.instructions(), Some("Use concise answers."));
152    /// # Ok::<(), agents_kit::KitError>(())
153    /// ```
154    pub fn with_instructions(mut self, instructions: impl Into<String>) -> Result<Self, KitError> {
155        let instructions = instructions.into();
156        Self::validate_optional_text("agent instructions", Some(&instructions))?;
157        self.instructions = Some(instructions);
158        Ok(self)
159    }
160
161    /// Enables or disables runtime tracing for this agent.
162    ///
163    /// ```
164    /// use agents_kit::config::{AgentConfig, CodexConfig};
165    /// use std::{num::NonZeroUsize, time::Duration};
166    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
167    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
168    /// let config = config.with_tracing_enabled(true);
169    /// assert!(config.tracing_enabled());
170    /// # Ok::<(), agents_kit::KitError>(())
171    /// ```
172    pub fn with_tracing_enabled(mut self, tracing_enabled: bool) -> Self {
173        self.tracing_enabled = tracing_enabled;
174        self
175    }
176
177    /// Sets the default foreground prompt timeout used when a request omits one.
178    ///
179    /// # Errors
180    /// Returns [`crate::error::ErrorKind::InvalidConfig`] when the timeout is zero.
181    ///
182    /// ```
183    /// use agents_kit::config::{AgentConfig, CodexConfig};
184    /// use std::{num::NonZeroUsize, time::Duration};
185    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
186    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
187    /// let config = config.with_default_prompt_timeout(Duration::from_secs(45))?;
188    /// assert_eq!(config.default_prompt_timeout(), Some(Duration::from_secs(45)));
189    /// # Ok::<(), agents_kit::KitError>(())
190    /// ```
191    pub fn with_default_prompt_timeout(mut self, timeout: Duration) -> Result<Self, KitError> {
192        if timeout.is_zero() {
193            return Err(KitError::invalid_config(
194                "agent default prompt timeout must be greater than zero",
195            ));
196        }
197        self.default_prompt_timeout = Some(timeout);
198        Ok(self)
199    }
200
201    /// Returns the selected backend configuration.
202    ///
203    /// ```
204    /// use agents_kit::config::{AgentConfig, CodexConfig};
205    /// use std::{num::NonZeroUsize, time::Duration};
206    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
207    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
208    /// assert!(matches!(config.backend(), agents_kit::config::BackendConfig::Codex(_)));
209    /// # Ok::<(), agents_kit::KitError>(())
210    /// ```
211    pub fn backend(&self) -> &BackendConfig {
212        &self.backend
213    }
214
215    /// Returns the configured display name, when present.
216    ///
217    /// ```
218    /// use agents_kit::config::{AgentConfig, CodexConfig};
219    /// use std::{num::NonZeroUsize, time::Duration};
220    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
221    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
222    /// assert_eq!(config.display_name(), None);
223    /// # Ok::<(), agents_kit::KitError>(())
224    /// ```
225    pub fn display_name(&self) -> Option<&str> {
226        self.display_name.as_deref()
227    }
228
229    /// Returns the configured agent-level instructions, when present.
230    ///
231    /// ```
232    /// use agents_kit::config::{AgentConfig, CodexConfig};
233    /// use std::{num::NonZeroUsize, time::Duration};
234    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
235    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
236    /// assert_eq!(config.instructions(), None);
237    /// # Ok::<(), agents_kit::KitError>(())
238    /// ```
239    pub fn instructions(&self) -> Option<&str> {
240        self.instructions.as_deref()
241    }
242
243    /// Returns whether runtime tracing is enabled.
244    ///
245    /// ```
246    /// use agents_kit::config::{AgentConfig, CodexConfig};
247    /// use std::{num::NonZeroUsize, time::Duration};
248    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
249    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
250    /// assert!(!config.tracing_enabled());
251    /// # Ok::<(), agents_kit::KitError>(())
252    /// ```
253    pub fn tracing_enabled(&self) -> bool {
254        self.tracing_enabled
255    }
256
257    /// Returns the bound used for agent lifecycle and recovery work.
258    ///
259    /// ```
260    /// use agents_kit::config::{AgentConfig, CodexConfig};
261    /// use std::{num::NonZeroUsize, time::Duration};
262    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
263    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
264    /// assert_eq!(config.lifecycle_timeout(), Duration::from_secs(30));
265    /// # Ok::<(), agents_kit::KitError>(())
266    /// ```
267    pub fn lifecycle_timeout(&self) -> Duration {
268        self.lifecycle_timeout
269    }
270
271    /// Returns the maximum number of concurrently admitted prompts for the agent.
272    ///
273    /// ```
274    /// use agents_kit::config::{AgentConfig, CodexConfig};
275    /// use std::{num::NonZeroUsize, time::Duration};
276    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
277    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
278    /// assert_eq!(config.prompt_limit().get(), 1);
279    /// # Ok::<(), agents_kit::KitError>(())
280    /// ```
281    pub fn prompt_limit(&self) -> NonZeroUsize {
282        self.prompt_limit
283    }
284
285    /// Returns the default foreground prompt timeout, when configured.
286    #[must_use]
287    ///
288    /// ```
289    /// use agents_kit::config::{AgentConfig, CodexConfig};
290    /// use std::{num::NonZeroUsize, time::Duration};
291    /// let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
292    ///     Duration::from_secs(60))?.into(), Duration::from_secs(30), NonZeroUsize::MIN)?;
293    /// assert_eq!(config.default_prompt_timeout(), None);
294    /// # Ok::<(), agents_kit::KitError>(())
295    /// ```
296    pub fn default_prompt_timeout(&self) -> Option<Duration> {
297        self.default_prompt_timeout
298    }
299
300    pub(crate) fn validate(&self) -> Result<(), KitError> {
301        if self.lifecycle_timeout.is_zero() {
302            return Err(KitError::invalid_config(
303                "agent lifecycle timeout must be greater than zero",
304            ));
305        }
306        if self
307            .default_prompt_timeout
308            .is_some_and(|timeout| timeout.is_zero())
309        {
310            return Err(KitError::invalid_config(
311                "agent default prompt timeout must be greater than zero",
312            ));
313        }
314        Self::validate_optional_text("agent display name", self.display_name.as_deref())?;
315        Self::validate_optional_text("agent instructions", self.instructions.as_deref())?;
316        match &self.backend {
317            BackendConfig::Codex(config) => config.validate(),
318            BackendConfig::OpenCode(config) => config.validate(),
319            BackendConfig::Custom(config) => config.validate(),
320        }
321    }
322
323    fn validate_optional_text(label: &str, value: Option<&str>) -> Result<(), KitError> {
324        if value.is_some_and(|value| value.is_empty() || value.trim() != value) {
325            return Err(KitError::invalid_config(format!(
326                "{label} must be nonempty and unpadded when present"
327            )));
328        }
329        Ok(())
330    }
331}
332
333impl From<CodexConfig> for BackendConfig {
334    fn from(config: CodexConfig) -> Self {
335        Self::Codex(config)
336    }
337}
338
339impl From<OpenCodeConfig> for BackendConfig {
340    fn from(config: OpenCodeConfig) -> Self {
341        Self::OpenCode(config)
342    }
343}
344
345impl From<CustomAgentConfig> for BackendConfig {
346    fn from(config: CustomAgentConfig) -> Self {
347        Self::Custom(config)
348    }
349}