Skip to main content

everruns_core/capabilities/
claude_tool_search.rs

1// Claude (Anthropic) Tool Search Capability
2//
3// When added to an agent, enables Anthropic's hosted tool_search (deferred tool
4// loading) for Claude models with tool_search=true in their profile. Each tool's
5// full parameter schema is loaded on-demand by the model via a server-side
6// `tool_search_tool_*_20251119` tool instead of being sent upfront.
7//
8// Like `openai_tool_search`, this capability provides no tools itself — it sets a
9// provider-agnostic `ToolSearchConfig`. The Anthropic driver consumes that config
10// and renders the hosted format (`defer_loading: true` per tool plus a
11// `tool_search_tool_bm25_20251119` entry). See `crates/anthropic/src/driver.rs`.
12//
13// If the model does not support tool_search (tool_search=false in the Anthropic
14// profile), this capability is silently ignored — no error, no crash. Use
15// `auto_tool_search` for a model-adaptive default that picks this on native
16// Claude models and the generic client-side mechanism elsewhere.
17
18use super::{Capability, CapabilityLocalization, CapabilityStatus, SystemPromptContext};
19use crate::driver_registry::ToolSearchConfig;
20use async_trait::async_trait;
21
22pub use super::openai_tool_search::DEFAULT_TOOL_SEARCH_THRESHOLD;
23
24/// Capability ID for Claude (Anthropic) tool search.
25pub const CLAUDE_TOOL_SEARCH_CAPABILITY_ID: &str = "claude_tool_search";
26
27/// Claude Tool Search capability.
28///
29/// Adding this capability to an agent/harness enables deferred tool loading for
30/// Claude models that support it. The `threshold` controls the minimum number of
31/// tools before tool_search activates (default: [`DEFAULT_TOOL_SEARCH_THRESHOLD`]).
32pub struct ClaudeToolSearchCapability {
33    threshold: usize,
34}
35
36impl ClaudeToolSearchCapability {
37    pub fn new() -> Self {
38        Self {
39            threshold: DEFAULT_TOOL_SEARCH_THRESHOLD,
40        }
41    }
42
43    pub fn with_threshold(threshold: usize) -> Self {
44        Self { threshold }
45    }
46
47    /// Returns the `ToolSearchConfig` for this capability.
48    ///
49    /// The config is provider-agnostic — the same shape `openai_tool_search`
50    /// produces. The Anthropic driver renders it into the hosted Messages-API
51    /// format; the OpenAI Responses driver renders it into the Responses format.
52    /// Whichever driver handles the request decides the wire shape, so this
53    /// carries no provider tag.
54    pub fn tool_search_config(&self) -> ToolSearchConfig {
55        ToolSearchConfig {
56            enabled: true,
57            threshold: self.threshold,
58        }
59    }
60}
61
62/// Whether `model` natively supports Anthropic's hosted tool_search.
63///
64/// This is the single source of truth for the native-Claude decision, consulted
65/// by `auto_tool_search`'s runtime dispatch (at capability-collection time) and
66/// by `RuntimeAgentBuilder::build` (when reconciling a hosted config with the
67/// model). Native Claude tool_search is an Anthropic Messages-API feature, so the
68/// lookup is against the Anthropic provider profile regardless of how the model
69/// is otherwise routed (a Claude model reached via OpenRouter/Bedrock has the
70/// flag masked off in `get_model_profile` and falls back to client-side search).
71pub fn model_supports_native_tool_search(model: &str) -> bool {
72    crate::model_profiles::get_model_profile(&crate::provider::DriverId::Anthropic, model)
73        .is_some_and(|profile| profile.tool_search)
74}
75
76impl Default for ClaudeToolSearchCapability {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82#[async_trait]
83impl Capability for ClaudeToolSearchCapability {
84    fn id(&self) -> &str {
85        CLAUDE_TOOL_SEARCH_CAPABILITY_ID
86    }
87
88    fn name(&self) -> &str {
89        "Claude Tool Search"
90    }
91
92    fn description(&self) -> &str {
93        "Enables deferred tool loading for Claude models that support it \
94         (Sonnet 4, Opus 4, Haiku 4.5, and Fable 5 and newer). Reduces token \
95         usage by loading tool schemas on-demand instead of upfront."
96    }
97
98    fn localizations(&self) -> Vec<CapabilityLocalization> {
99        vec![CapabilityLocalization::text(
100            "uk",
101            "Пошук інструментів Claude",
102            "Вмикає відкладене завантаження інструментів для моделей Claude, які його підтримують (Sonnet 4, Opus 4, Haiku 4.5 та Fable 5 і новіші). Зменшує використання токенів, завантажуючи схеми інструментів на вимогу, а не заздалегідь.",
103        )]
104    }
105
106    fn status(&self) -> CapabilityStatus {
107        CapabilityStatus::Available
108    }
109
110    fn category(&self) -> Option<&str> {
111        Some("Optimization")
112    }
113
114    async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
115        None // No system prompt needed — deferral is handled server-side.
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    // Metadata/tool-list constants covered by builtin_capabilities_satisfy_registry_invariants.
124
125    #[test]
126    fn test_default_threshold() {
127        let cap = ClaudeToolSearchCapability::new();
128        let config = cap.tool_search_config();
129        assert!(config.enabled);
130        assert_eq!(config.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
131    }
132
133    #[test]
134    fn test_custom_threshold() {
135        let cap = ClaudeToolSearchCapability::with_threshold(5);
136        assert_eq!(cap.tool_search_config().threshold, 5);
137    }
138
139    #[test]
140    fn test_native_support_lookup() {
141        // Claude 4-family models support hosted tool_search; 3.x do not.
142        assert!(model_supports_native_tool_search("claude-opus-4-8"));
143        assert!(model_supports_native_tool_search("claude-sonnet-5"));
144        assert!(model_supports_native_tool_search("claude-sonnet-4-6"));
145        assert!(model_supports_native_tool_search("claude-haiku-4-5"));
146        assert!(model_supports_native_tool_search("claude-fable-5"));
147        assert!(!model_supports_native_tool_search("claude-3-5-haiku"));
148        // Unknown / non-Anthropic models are not native.
149        assert!(!model_supports_native_tool_search("gpt-5.5"));
150    }
151}