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