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;