Skip to main content

agents_kit/config/
custom.rs

1use std::num::NonZeroUsize;
2
3use crate::{KitError, ModelSelector, ToolSelection};
4
5/// Bounded work admitted by one Custom prompt.
6///
7/// ```
8/// use agents_kit::config::CustomLoopLimits;
9/// let limits = CustomLoopLimits::new(8, 0, 1)?;
10/// assert_eq!(limits.max_tool_actions(), 0);
11/// # Ok::<(), agents_kit::KitError>(())
12/// ```
13#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
14pub struct CustomLoopLimits {
15    max_model_turns: NonZeroUsize,
16    max_tool_actions: usize,
17    max_parallel_tool_actions: NonZeroUsize,
18}
19
20impl CustomLoopLimits {
21    /// Validates the model-turn, aggregate-action, and parallel-action bounds.
22    ///
23    /// # Errors
24    /// Returns [`crate::error::ErrorKind::InvalidConfig`] for zero model turns or
25    /// parallel actions, or more than 65,536 aggregate actions. Zero aggregate
26    /// actions disables tools without disabling model turns.
27    ///
28    /// ```
29    /// use agents_kit::config::CustomLoopLimits;
30    /// let limits = CustomLoopLimits::new(8, 0, 1)?;
31    /// assert_eq!(limits.max_tool_actions(), 0);
32    /// # Ok::<(), agents_kit::KitError>(())
33    /// ```
34    pub fn new(
35        max_model_turns: usize,
36        max_tool_actions: usize,
37        max_parallel_tool_actions: usize,
38    ) -> Result<Self, KitError> {
39        let max_model_turns = NonZeroUsize::new(max_model_turns)
40            .ok_or_else(|| KitError::invalid_config("Custom model-turn limit must be positive"))?;
41        let max_parallel_tool_actions =
42            NonZeroUsize::new(max_parallel_tool_actions).ok_or_else(|| {
43                KitError::invalid_config("Custom parallel-tool limit must be positive")
44            })?;
45        if max_tool_actions > 65_536 {
46            return Err(KitError::invalid_config(
47                "Custom aggregate tool-action limit exceeds the portable bound",
48            ));
49        }
50        Ok(Self {
51            max_model_turns,
52            max_tool_actions,
53            max_parallel_tool_actions,
54        })
55    }
56
57    /// Returns the maximum number of model turns admitted by one prompt.
58    ///
59    /// ```
60    /// use agents_kit::config::CustomLoopLimits;
61    ///
62    /// let limits = CustomLoopLimits::new(8, 16, 2)?;
63    /// assert_eq!(limits.max_model_turns(), 8);
64    /// # Ok::<(), agents_kit::KitError>(())
65    /// ```
66    #[must_use]
67    pub fn max_model_turns(self) -> usize {
68        self.max_model_turns.get()
69    }
70
71    /// Returns the aggregate tool-action limit for one prompt; zero disables actions.
72    ///
73    /// ```
74    /// use agents_kit::config::CustomLoopLimits;
75    ///
76    /// let limits = CustomLoopLimits::new(8, 0, 2)?;
77    /// assert_eq!(limits.max_tool_actions(), 0);
78    /// # Ok::<(), agents_kit::KitError>(())
79    /// ```
80    #[must_use]
81    pub fn max_tool_actions(self) -> usize {
82        self.max_tool_actions
83    }
84
85    /// Returns the maximum number of tool actions that may execute concurrently.
86    ///
87    /// ```
88    /// use agents_kit::config::CustomLoopLimits;
89    ///
90    /// let limits = CustomLoopLimits::new(8, 16, 2)?;
91    /// assert_eq!(limits.max_parallel_tool_actions(), 2);
92    /// # Ok::<(), agents_kit::KitError>(())
93    /// ```
94    #[must_use]
95    pub fn max_parallel_tool_actions(self) -> usize {
96        self.max_parallel_tool_actions.get()
97    }
98
99    pub(crate) fn validate(self) -> Result<(), KitError> {
100        if self.max_tool_actions > 65_536 {
101            return Err(KitError::invalid_state(
102                "stored Custom tool-action limit exceeds the portable bound",
103            ));
104        }
105        Ok(())
106    }
107}
108
109/// Approval behavior for model-requested tool actions.
110#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
111#[serde(rename_all = "snake_case")]
112pub enum CustomToolApproval {
113    /// Execute admitted actions without a caller interaction.
114    Allow,
115    /// Reject every model-requested action.
116    Deny,
117    /// Publish one durable notification and await its resolution.
118    Interaction,
119}
120
121/// Passive policy for the provider-backed Custom runtime.
122///
123/// ```
124/// use agents_kit::{config::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval},
125///     model::ModelSelector, tool::ToolSelection};
126/// let config = CustomAgentConfig::new(ModelSelector::new("exact-model-id")?,
127///     ToolSelection::None, CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
128/// assert_eq!(config.default_tool_selection(), &ToolSelection::None);
129/// # Ok::<(), agents_kit::KitError>(())
130/// ```
131#[derive(Clone, Debug)]
132pub struct CustomAgentConfig {
133    default_model: ModelSelector,
134    default_tool_selection: ToolSelection,
135    loop_limits: CustomLoopLimits,
136    tool_approval: CustomToolApproval,
137}
138
139impl CustomAgentConfig {
140    /// Constructs a complete Custom policy with no runtime resources.
141    ///
142    /// # Errors
143    /// Returns [`crate::error::ErrorKind::InvalidInput`] for an empty named tool
144    /// selection, duplicate names, or names outside the portable tool vocabulary.
145    /// Model availability is checked by the selected backend during use.
146    ///
147    /// ```
148    /// use agents_kit::{config::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval},
149    ///     model::ModelSelector, tool::ToolSelection};
150    /// let config = CustomAgentConfig::new(ModelSelector::new("exact-model-id")?,
151    ///     ToolSelection::None, CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
152    /// assert_eq!(config.default_tool_selection(), &ToolSelection::None);
153    /// # Ok::<(), agents_kit::KitError>(())
154    /// ```
155    pub fn new(
156        default_model: ModelSelector,
157        default_tool_selection: ToolSelection,
158        loop_limits: CustomLoopLimits,
159        tool_approval: CustomToolApproval,
160    ) -> Result<Self, KitError> {
161        default_tool_selection.validate()?;
162        Ok(Self {
163            default_model,
164            default_tool_selection,
165            loop_limits,
166            tool_approval,
167        })
168    }
169
170    /// Returns the configured default model selector.
171    ///
172    /// ```
173    /// use agents_kit::{
174    ///     config::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval},
175    ///     model::ModelSelector,
176    ///     tool::ToolSelection,
177    /// };
178    ///
179    /// let config = CustomAgentConfig::new(
180    ///     ModelSelector::new("model-slug")?, ToolSelection::Auto,
181    ///     CustomLoopLimits::new(8, 16, 2)?, CustomToolApproval::Allow,
182    /// )?;
183    /// assert_eq!(config.default_model().as_str(), "model-slug");
184    /// # Ok::<(), agents_kit::KitError>(())
185    /// ```
186    #[must_use]
187    pub fn default_model(&self) -> &ModelSelector {
188        &self.default_model
189    }
190
191    /// Returns the configured default tool selection.
192    ///
193    /// ```
194    /// use agents_kit::{
195    ///     config::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval},
196    ///     model::ModelSelector,
197    ///     tool::ToolSelection,
198    /// };
199    ///
200    /// let config = CustomAgentConfig::new(
201    ///     ModelSelector::new("model-slug")?, ToolSelection::Auto,
202    ///     CustomLoopLimits::new(8, 16, 2)?, CustomToolApproval::Allow,
203    /// )?;
204    /// assert_eq!(config.default_tool_selection(), &ToolSelection::Auto);
205    /// # Ok::<(), agents_kit::KitError>(())
206    /// ```
207    #[must_use]
208    pub fn default_tool_selection(&self) -> &ToolSelection {
209        &self.default_tool_selection
210    }
211
212    /// Returns the configured bounds for one prompt's model and tool work.
213    ///
214    /// ```
215    /// use agents_kit::{
216    ///     config::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval},
217    ///     model::ModelSelector,
218    ///     tool::ToolSelection,
219    /// };
220    ///
221    /// let limits = CustomLoopLimits::new(8, 16, 2)?;
222    /// let config = CustomAgentConfig::new(
223    ///     ModelSelector::new("model-slug")?, ToolSelection::Auto,
224    ///     limits, CustomToolApproval::Allow,
225    /// )?;
226    /// assert_eq!(config.loop_limits(), limits);
227    /// # Ok::<(), agents_kit::KitError>(())
228    /// ```
229    #[must_use]
230    pub fn loop_limits(&self) -> CustomLoopLimits {
231        self.loop_limits
232    }
233
234    /// Returns the approval policy for model-requested tool actions.
235    ///
236    /// ```
237    /// use agents_kit::{
238    ///     config::{CustomAgentConfig, CustomLoopLimits, CustomToolApproval},
239    ///     model::ModelSelector,
240    ///     tool::ToolSelection,
241    /// };
242    ///
243    /// let config = CustomAgentConfig::new(
244    ///     ModelSelector::new("model-slug")?, ToolSelection::Auto,
245    ///     CustomLoopLimits::new(8, 16, 2)?, CustomToolApproval::Interaction,
246    /// )?;
247    /// assert_eq!(config.tool_approval(), CustomToolApproval::Interaction);
248    /// # Ok::<(), agents_kit::KitError>(())
249    /// ```
250    #[must_use]
251    pub fn tool_approval(&self) -> CustomToolApproval {
252        self.tool_approval
253    }
254
255    pub(crate) fn validate(&self) -> Result<(), KitError> {
256        self.default_tool_selection.validate()?;
257        self.loop_limits.validate()
258    }
259}