Skip to main content

agents_kit/
model.rs

1//! Backend-advertised model, personality, and reasoning selectors.
2//!
3//! Selectors preserve their exact strings. Construction validates shape, not
4//! availability; use [`crate::AgentBackend::list_models`] for the selected
5//! backend's inventory. [`ModelInfo`] reports optional backend metadata without
6//! turning it into a cross-provider capability promise.
7
8//!
9//! # Supplied selector restrictions
10//!
11//! | Backend | Reasoning | Personality |
12//! | --- | --- | --- |
13//! | Codex | Exact effort advertised for the selected model | Rejected: the selected native protocol retired personality selection |
14//! | OpenCode | Portable override is Unsupported | Portable override is Unsupported |
15//! | Custom | `minimal`, `low`, `medium`, `high`, `maximum`, mapped to Provider's portable reasoning enum | Unsupported |
16//!
17//! Custom's selected Provider Model still validates whether it can honor the
18//! mapped effort. Arbitrary native efforts such as `xhigh` are not a Custom
19//! escape hatch. An omitted override stays omitted at the Agent request boundary;
20//! effective values also depend on conversation/workspace defaults. Availability
21//! of a model name or a syntactically valid selector is not execution evidence.
22//! Caller implementations may support their own advertised selectors.
23//!
24//! # Exact Codex selection
25//!
26//! This example requires the supported Codex executable on `PATH`, native
27//! authentication and access to the exact model. Building starts a subprocess;
28//! listing and prompting use the real native service, so this compiles without
29//! running as a doctest. No caller Provider is constructed for a native Agent.
30//!
31//! ```no_run,standalone_crate
32//! #![recursion_limit = "256"] // Nested concrete async futures need this on Linux.
33//! use std::{num::NonZeroUsize, time::Duration};
34//! use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, ConversationBackend, WorkspaceBackend};
35//! use agents_kit::config::{AgentConfig, CodexConfig};
36//! use agents_kit::conversation::NewConversation;
37//! use agents_kit::model::{ModelSelector, ReasoningEffort};
38//! use agents_kit::prompt::PromptRequest;
39//! use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
40//!     WorkspaceExecutionPolicy, WorkspaceNetworkAccess, WorkspaceRoot,
41//!     WorkspaceRootAccess, WorkspaceSandboxMode};
42//! use storage_kit::{StateStore,
43//!     state::config::{LanceStateStoreConfig, StateStoreConfig}};
44//! # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
45//! # runtime.block_on(async {
46//! let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
47//! let config = AgentConfig::new(CodexConfig::spawn(Duration::from_secs(15),
48//!     Duration::from_secs(90))?.into(), Duration::from_secs(60), NonZeroUsize::MIN)?;
49//! let agent = AgentBuilder::create(config, AgentStorage::new(store)).build().await?;
50//! let outcome = async {
51//!     let selected = ModelSelector::new("gpt-6-luna")?;
52//!     let effort = ReasoningEffort::medium();
53//!     let inventory = agent.list_models().await?;
54//!     let model = inventory.iter().find(|item| item.selector() == &selected)
55//!         .ok_or_else(|| agents_kit::KitError::unsupported("required exact model is unavailable"))?;
56//!     if !model.reasoning_efforts().contains(&effort) {
57//!         return Err(agents_kit::KitError::unsupported("required effort is unavailable").into());
58//!     }
59//!     let cwd = std::env::current_dir()?;
60//!     let context = WorkspaceContext::new(cwd.clone(),
61//!         vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
62//!         WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
63//!     let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
64//!     let work = async {
65//!         let request = PromptRequest::new("Describe the current task without changing files.")?
66//!             .with_model(selected).with_reasoning_effort(effort)
67//!             .with_timeout(Duration::from_secs(90))?;
68//!         let conversation = workspace.new_conversation(NewConversation::new(None, None, vec![])?).await?;
69//!         let response = conversation.prompt(request).await;
70//!         let close = conversation.close().await;
71//!         let response = match (response, close) {
72//!             (Ok(response), Ok(())) => Ok(response),
73//!             (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
74//!             (Err(error), _) | (_, Err(error)) => Err(error),
75//!         }?;
76//!         assert!(!response.output().trim().is_empty()); // progress, not a prescribed wording grade
77//!         Ok::<(), agents_kit::KitError>(())
78//!     }.await;
79//!     let close = workspace.close().await; // also runs if conversation creation failed
80//!     match (work, close) {
81//!         (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
82//!         (Err(error), _) | (_, Err(error)) => Err(error),
83//!         (Ok(()), Ok(())) => Ok(()),
84//!     }
85//! }.await;
86//! let shutdown = agent.shutdown().await;
87//! match (outcome, shutdown) {
88//!     (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
89//!     (Err(error), _) | (_, Err(error)) => Err(error),
90//!     (Ok(()), Ok(())) => Ok(()),
91//! }?;
92//! # Ok::<(), Box<dyn std::error::Error>>(())
93//! # })?;
94//! # Ok::<(), Box<dyn std::error::Error>>(())
95//! ```
96
97use serde::{Deserialize, Deserializer, Serialize, Serializer};
98
99use crate::KitError;
100
101#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
102/// Selects one backend model by its canonical opaque value.
103///
104/// ```
105/// use agents_kit::model::ModelSelector;
106///
107/// let selector = ModelSelector::new("model-slug")?;
108/// assert_eq!(selector.as_str(), "model-slug");
109/// # Ok::<(), agents_kit::KitError>(())
110/// ```
111pub struct ModelSelector {
112    value: String,
113}
114
115impl ModelSelector {
116    /// Validates and constructs a nonempty, unpadded selector.
117    ///
118    /// # Errors
119    /// Returns [`crate::error::ErrorKind::InvalidInput`] for an empty or padded value.
120    ///
121    /// ```
122    /// use agents_kit::model::ModelSelector;
123    /// let model = ModelSelector::new("exact-model-id")?;
124    /// assert_eq!(model.as_str(), "exact-model-id");
125    /// assert!(ModelSelector::new(" model ").is_err());
126    /// # Ok::<(), agents_kit::KitError>(())
127    /// ```
128    pub fn new(value: impl Into<String>) -> Result<Self, KitError> {
129        let value = value.into();
130        if value.is_empty() || value.trim() != value {
131            return Err(KitError::invalid_input(
132                "model selector must be nonempty and unpadded",
133            ));
134        }
135        Ok(Self { value })
136    }
137
138    /// Returns the canonical selector value.
139    ///
140    /// ```
141    /// use agents_kit::model::ModelSelector;
142    /// assert_eq!(ModelSelector::new("model-id")?.as_str(), "model-id");
143    /// # Ok::<(), agents_kit::KitError>(())
144    /// ```
145    pub fn as_str(&self) -> &str {
146        &self.value
147    }
148}
149
150impl std::fmt::Display for ModelSelector {
151    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        formatter.write_str(&self.value)
153    }
154}
155
156impl Serialize for ModelSelector {
157    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
158    where
159        S: Serializer,
160    {
161        serializer.serialize_str(&self.value)
162    }
163}
164
165impl<'de> Deserialize<'de> for ModelSelector {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: Deserializer<'de>,
169    {
170        let value = String::deserialize(deserializer)?;
171        Self::new(value).map_err(serde::de::Error::custom)
172    }
173}
174
175#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
176/// Selects one backend-advertised personality by its canonical opaque value.
177///
178/// ```
179/// use agents_kit::model::PersonalitySelector;
180/// let personality = PersonalitySelector::new("concise")?;
181/// assert_eq!(personality.as_str(), "concise");
182/// # Ok::<(), agents_kit::KitError>(())
183/// ```
184pub struct PersonalitySelector {
185    value: String,
186}
187
188impl PersonalitySelector {
189    /// Validates and constructs a nonempty, unpadded personality selector.
190    ///
191    /// # Errors
192    /// Returns [`crate::error::ErrorKind::InvalidInput`] for an empty or padded value.
193    /// This does not check whether a backend advertises the personality.
194    ///
195    /// ```
196    /// use agents_kit::model::PersonalitySelector;
197    /// assert!(PersonalitySelector::new("").is_err());
198    /// assert_eq!(PersonalitySelector::new("concise")?.as_str(), "concise");
199    /// # Ok::<(), agents_kit::KitError>(())
200    /// ```
201    pub fn new(value: impl Into<String>) -> Result<Self, KitError> {
202        let value = value.into();
203        if value.is_empty() || value.trim() != value {
204            return Err(KitError::invalid_input(
205                "personality selector must be nonempty and unpadded",
206            ));
207        }
208        Ok(Self { value })
209    }
210
211    /// Returns the canonical selector value.
212    ///
213    /// ```
214    /// use agents_kit::model::PersonalitySelector;
215    /// assert_eq!(PersonalitySelector::new("concise")?.as_str(), "concise");
216    /// # Ok::<(), agents_kit::KitError>(())
217    /// ```
218    pub fn as_str(&self) -> &str {
219        &self.value
220    }
221}
222
223impl std::fmt::Display for PersonalitySelector {
224    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        formatter.write_str(&self.value)
226    }
227}
228
229impl Serialize for PersonalitySelector {
230    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231    where
232        S: Serializer,
233    {
234        serializer.serialize_str(&self.value)
235    }
236}
237
238impl<'de> Deserialize<'de> for PersonalitySelector {
239    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
240    where
241        D: Deserializer<'de>,
242    {
243        let value = String::deserialize(deserializer)?;
244        Self::new(value).map_err(serde::de::Error::custom)
245    }
246}
247
248#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
249/// Describes an open-ended backend reasoning-effort value.
250///
251/// The conventional constructors do not establish support on a selected model.
252/// See the [supplied selector restrictions](self#supplied-selector-restrictions).
253///
254/// ```
255/// use agents_kit::model::ReasoningEffort;
256/// assert_eq!(ReasoningEffort::high().as_str(), "high");
257/// ```
258pub struct ReasoningEffort {
259    value: String,
260}
261
262impl ReasoningEffort {
263    /// Validates and constructs a nonempty, unpadded effort value.
264    ///
265    /// # Errors
266    /// Returns [`crate::error::ErrorKind::InvalidInput`] for an empty or padded value.
267    ///
268    /// ```
269    /// use agents_kit::model::ReasoningEffort;
270    /// assert_eq!(ReasoningEffort::new("custom-effort")?.as_str(), "custom-effort");
271    /// assert!(ReasoningEffort::new(" high").is_err());
272    /// # Ok::<(), agents_kit::KitError>(())
273    /// ```
274    pub fn new(value: impl Into<String>) -> Result<Self, KitError> {
275        let value = value.into();
276        if value.is_empty() || value.trim() != value {
277            return Err(KitError::invalid_input(
278                "reasoning effort must be nonempty and unpadded",
279            ));
280        }
281        Ok(Self { value })
282    }
283
284    /// Constructs the conventional low effort value.
285    ///
286    /// ```
287    /// assert_eq!(agents_kit::model::ReasoningEffort::low().as_str(), "low");
288    /// ```
289    pub fn low() -> Self {
290        Self {
291            value: "low".to_owned(),
292        }
293    }
294
295    /// Constructs the conventional medium effort value.
296    ///
297    /// ```
298    /// assert_eq!(agents_kit::model::ReasoningEffort::medium().as_str(), "medium");
299    /// ```
300    pub fn medium() -> Self {
301        Self {
302            value: "medium".to_owned(),
303        }
304    }
305
306    /// Constructs the conventional high effort value.
307    ///
308    /// ```
309    /// assert_eq!(agents_kit::model::ReasoningEffort::high().as_str(), "high");
310    /// ```
311    pub fn high() -> Self {
312        Self {
313            value: "high".to_owned(),
314        }
315    }
316
317    /// Returns the canonical effort value.
318    ///
319    /// ```
320    /// use agents_kit::model::ReasoningEffort;
321    /// let effort = ReasoningEffort::low();
322    /// assert_eq!(effort.as_str(), "low");
323    /// ```
324    pub fn as_str(&self) -> &str {
325        &self.value
326    }
327}
328
329impl std::fmt::Display for ReasoningEffort {
330    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        formatter.write_str(&self.value)
332    }
333}
334
335impl Serialize for ReasoningEffort {
336    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
337    where
338        S: Serializer,
339    {
340        serializer.serialize_str(&self.value)
341    }
342}
343
344impl<'de> Deserialize<'de> for ReasoningEffort {
345    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
346    where
347        D: Deserializer<'de>,
348    {
349        let value = String::deserialize(deserializer)?;
350        Self::new(value).map_err(serde::de::Error::custom)
351    }
352}
353
354#[derive(Clone, Debug, Eq, PartialEq)]
355/// Describes one selectable model returned by agent inventory.
356///
357/// ```
358/// use agents_kit::model::{ModelInfo, ModelSelector};
359/// let info = ModelInfo::new(ModelSelector::new("model-id")?, None, vec![], true)?;
360/// assert!(info.is_default());
361/// assert_eq!(info.selector().as_str(), "model-id");
362/// # Ok::<(), agents_kit::KitError>(())
363/// ```
364pub struct ModelInfo {
365    selector: ModelSelector,
366    display_name: Option<String>,
367    reasoning_efforts: Vec<ReasoningEffort>,
368    personalities: Vec<PersonalitySelector>,
369    is_default: bool,
370}
371
372impl ModelInfo {
373    /// Constructs validated model inventory information.
374    ///
375    /// # Errors
376    /// Returns [`crate::error::ErrorKind::InvalidInput`] for a present empty or
377    /// padded display name. It performs no model discovery or network request.
378    ///
379    /// ```
380    /// use agents_kit::model::{ModelInfo, ModelSelector, ReasoningEffort};
381    /// let info = ModelInfo::new(ModelSelector::new("model-id")?,
382    ///     Some("Example model".into()), vec![ReasoningEffort::low()], false)?;
383    /// assert_eq!(info.display_name(), Some("Example model"));
384    /// # Ok::<(), agents_kit::KitError>(())
385    /// ```
386    pub fn new(
387        selector: ModelSelector,
388        display_name: Option<String>,
389        reasoning_efforts: Vec<ReasoningEffort>,
390        is_default: bool,
391    ) -> Result<Self, KitError> {
392        if display_name
393            .as_deref()
394            .is_some_and(|name| name.is_empty() || name.trim() != name)
395        {
396            return Err(KitError::invalid_input(
397                "model display name must be nonempty and unpadded when present",
398            ));
399        }
400        Ok(Self {
401            selector,
402            display_name,
403            reasoning_efforts,
404            personalities: Vec::new(),
405            is_default,
406        })
407    }
408
409    /// Replaces the ordered backend-advertised personality values.
410    ///
411    /// ```
412    /// use agents_kit::model::{ModelInfo, ModelSelector, PersonalitySelector};
413    /// let info = ModelInfo::new(ModelSelector::new("model-id")?, None, vec![], false)?
414    ///     .with_personalities(vec![PersonalitySelector::new("concise")?]);
415    /// assert_eq!(info.personalities()[0].as_str(), "concise");
416    /// # Ok::<(), agents_kit::KitError>(())
417    /// ```
418    #[must_use]
419    pub fn with_personalities(mut self, personalities: Vec<PersonalitySelector>) -> Self {
420        self.personalities = personalities;
421        self
422    }
423
424    /// Returns the selector callers can round-trip into prompts and defaults.
425    ///
426    /// ```
427    /// use agents_kit::model::{ModelInfo, ModelSelector};
428    /// let info = ModelInfo::new(ModelSelector::new("model-id")?, None, vec![], false)?;
429    /// let request = agents_kit::prompt::PromptRequest::new("Hello")?.with_model(info.selector().clone());
430    /// assert_eq!(request.model(), Some(info.selector()));
431    /// # Ok::<(), agents_kit::KitError>(())
432    /// ```
433    pub fn selector(&self) -> &ModelSelector {
434        &self.selector
435    }
436
437    /// Returns the optional caller-facing model name.
438    ///
439    /// ```
440    /// use agents_kit::model::{ModelInfo, ModelSelector};
441    /// let info = ModelInfo::new(ModelSelector::new("model-id")?, None, vec![], false)?;
442    /// assert_eq!(info.display_name(), None);
443    /// # Ok::<(), agents_kit::KitError>(())
444    /// ```
445    pub fn display_name(&self) -> Option<&str> {
446        self.display_name.as_deref()
447    }
448
449    /// Returns the advertised open-ended reasoning-effort values.
450    ///
451    /// ```
452    /// use agents_kit::model::{ModelInfo, ModelSelector, ReasoningEffort};
453    /// let info = ModelInfo::new(ModelSelector::new("model-id")?, None,
454    ///     vec![ReasoningEffort::low()], false)?;
455    /// assert_eq!(info.reasoning_efforts(), &[ReasoningEffort::low()]);
456    /// # Ok::<(), agents_kit::KitError>(())
457    /// ```
458    pub fn reasoning_efforts(&self) -> &[ReasoningEffort] {
459        &self.reasoning_efforts
460    }
461
462    /// Returns the ordered backend-advertised personality values.
463    ///
464    /// ```
465    /// use agents_kit::model::{ModelInfo, ModelSelector};
466    /// let info = ModelInfo::new(ModelSelector::new("model-id")?, None, vec![], false)?;
467    /// assert!(info.personalities().is_empty());
468    /// # Ok::<(), agents_kit::KitError>(())
469    /// ```
470    pub fn personalities(&self) -> &[PersonalitySelector] {
471        &self.personalities
472    }
473
474    /// Reports whether this is the sourced agent-wide default model.
475    ///
476    /// ```
477    /// use agents_kit::model::{ModelInfo, ModelSelector};
478    /// let info = ModelInfo::new(ModelSelector::new("model-id")?, None, vec![], true)?;
479    /// assert!(info.is_default());
480    /// # Ok::<(), agents_kit::KitError>(())
481    /// ```
482    pub fn is_default(&self) -> bool {
483        self.is_default
484    }
485}