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}