Skip to main content

defect_agent/session/
capabilities.rs

1//! Session-level capability configuration and startup-time decision.
2//!
3//! Capability management for sessions.
4//!
5//! `WebSearchCapabilityMode` controls whether this session uses provider-hosted web
6//! search:
7//! - `Delegate`: use provider-hosted web search (fails at startup if the adapter does not
8//!   support it)
9//! - `Disabled`: do not expose hosted web search
10//!
11//! Note: the local grep/glob tool (`search` tool) is **not** managed at the capability
12//! layer; it is controlled independently by `[tools.search].enabled` and is completely
13//! separate from `web_search`. Both can be enabled simultaneously, and the LLM will see
14//! both the hosted `web_search` and the local `search` tools.
15//!
16//! Decision timing: once at session startup. The `(provider, mode)` pair is fixed for the
17//! session lifetime; the turn loop directly reuses the [`HostedCapabilities`] flag stored
18//! on the session.
19
20use serde::{Deserialize, Serialize};
21
22use crate::llm::HostedCapabilities;
23
24use super::SessionInitError;
25
26/// Toggle for hosted web search capability.
27///
28/// TOML representation: `"delegate"` / `"disabled"`.
29#[non_exhaustive]
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum WebSearchCapabilityMode {
33    /// Delegate to provider-hosted web search. Session startup fails if the provider does
34    /// not support it.
35    Delegate,
36    /// Do not expose hosted web search.
37    #[default]
38    Disabled,
39}
40
41/// Configuration for a single capability. Reserved for future capabilities of the same
42/// form, such as `image_generation` / `code_execution`.
43#[non_exhaustive]
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct WebSearchCapabilityConfig {
46    pub mode: WebSearchCapabilityMode,
47}
48
49impl WebSearchCapabilityConfig {
50    /// Constructs from a single `mode`. Cross-crate callers need this entry point because
51    /// the struct is `#[non_exhaustive]` and cannot be built with a struct literal
52    /// directly.
53    #[must_use]
54    pub const fn new(mode: WebSearchCapabilityMode) -> Self {
55        Self { mode }
56    }
57}
58
59/// Entry point for session-level capability configuration.
60///
61/// Constructed by `defect-config` on `EffectiveConfig.capabilities`, overlaid with
62/// `providers.<p>.capabilities` overrides, and finally passed to the session during
63/// assembly in [`AgentCore::create_session`][crate::session::AgentCore::create_session].
64#[non_exhaustive]
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
66pub struct SessionCapabilitiesConfig {
67    pub web_search: WebSearchCapabilityConfig,
68}
69
70impl SessionCapabilitiesConfig {
71    /// Construct from a single [`WebSearchCapabilityConfig`]. Cross-crate callers (e.g.
72    /// `defect-config`) need this entry point because the struct is `#[non_exhaustive]`
73    /// and cannot be built with a struct literal directly.
74    #[must_use]
75    pub const fn with_web_search(web_search: WebSearchCapabilityConfig) -> Self {
76        Self { web_search }
77    }
78}
79
80/// Runtime capabilities resolved at session startup.
81///
82/// Distinct from [`SessionCapabilitiesConfig`]: that is the user's configuration
83/// (intent), while this is the actual enabled set after intersecting with the provider's
84/// [`HostedCapabilities`].
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
86pub struct ResolvedSessionCapabilities {
87    /// Whether this session uses hosted web search.
88    /// `Delegate × supported` → `true`; otherwise → `false`.
89    pub hosted: HostedCapabilities,
90}
91
92impl ResolvedSessionCapabilities {
93    /// Resolve once: map `(mode, provider_hosted)` to the result.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`SessionInitError::CapabilityUnsatisfied`] when the mode is `Delegate`
98    /// but the provider does not support hosted web search.
99    pub fn resolve(
100        config: SessionCapabilitiesConfig,
101        provider_hosted: HostedCapabilities,
102        provider_id: &str,
103    ) -> Result<Self, SessionInitError> {
104        let mut hosted = HostedCapabilities::default();
105
106        match config.web_search.mode {
107            WebSearchCapabilityMode::Delegate => {
108                if !provider_hosted.web_search {
109                    return Err(SessionInitError::CapabilityUnsatisfied {
110                        capability: "web_search",
111                        provider: provider_id.to_string(),
112                    });
113                }
114                hosted.web_search = true;
115            }
116            WebSearchCapabilityMode::Disabled => {}
117        }
118
119        Ok(Self { hosted })
120    }
121}
122
123#[cfg(test)]
124mod tests;