Skip to main content

everruns_core/capabilities/
auto_tool_search.rs

1// Auto Tool Search Capability
2//
3// A model-adaptive dispatcher over the two real tool-search mechanisms:
4//
5//   - `openai_tool_search` (hosted): on models with native tool_search support
6//     (OpenAI GPT-5.4+), the LLM driver hides parameter schemas server-side via
7//     namespaces + defer_loading. No client-side tool is added.
8//   - `tool_search` (generic, client-side): on every other model (Anthropic,
9//     Gemini, OpenAI Completions, ...), a `DeferSchemaHook` strips schemas and a
10//     `tool_search` tool loads them back on demand.
11//
12// Unlike picking one of those capabilities by hand, this one chooses at runtime.
13// It OWNS an `OpenAiToolSearchCapability` and a `ToolSearchCapability` and
14// implements `Capability::resolve_for_model`: capability collection knows the
15// agent's model (via `SystemPromptContext::model`) and delegates to whichever
16// inner capability fits. Only that one capability's contributions are collected —
17// the hosted config for the OpenAI one, or the hook + tool + system prompt for
18// the generic one. No "contribute both, prune later" step is needed.
19//
20// Use this instead of `openai_tool_search` or `tool_search` when a harness must
21// work well across providers.
22
23use super::openai_tool_search::{OpenAiToolSearchCapability, model_supports_native_tool_search};
24use super::tool_search::ToolSearchCapability;
25use super::{Capability, CapabilityStatus};
26
27pub use super::openai_tool_search::DEFAULT_TOOL_SEARCH_THRESHOLD;
28
29/// Capability ID for the model-adaptive tool search.
30pub const AUTO_TOOL_SEARCH_CAPABILITY_ID: &str = "auto_tool_search";
31
32/// Auto Tool Search capability.
33///
34/// Holds the two real tool-search capabilities and dispatches to one of them
35/// based on the agent's model. `threshold` (minimum number of tools before
36/// deferral activates) is shared by both and forwarded at construction.
37pub struct AutoToolSearchCapability {
38    openai: OpenAiToolSearchCapability,
39    generic: ToolSearchCapability,
40}
41
42impl AutoToolSearchCapability {
43    pub fn new() -> Self {
44        Self::with_threshold(DEFAULT_TOOL_SEARCH_THRESHOLD)
45    }
46
47    pub fn with_threshold(threshold: usize) -> Self {
48        Self {
49            openai: OpenAiToolSearchCapability::with_threshold(threshold),
50            generic: ToolSearchCapability::with_threshold(threshold),
51        }
52    }
53}
54
55impl Default for AutoToolSearchCapability {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl Capability for AutoToolSearchCapability {
62    fn id(&self) -> &str {
63        AUTO_TOOL_SEARCH_CAPABILITY_ID
64    }
65
66    fn name(&self) -> &str {
67        "Auto Tool Search"
68    }
69
70    fn description(&self) -> &str {
71        "Model-adaptive deferred tool loading. Uses OpenAI's hosted tool_search \
72         on models that support it (GPT-5.4 and newer) and a provider-agnostic \
73         client-side fallback on every other model. Reduces token usage for \
74         agents with many tools, regardless of provider."
75    }
76
77    fn status(&self) -> CapabilityStatus {
78        CapabilityStatus::Available
79    }
80
81    fn category(&self) -> Option<&str> {
82        Some("Optimization")
83    }
84
85    // The dispatch itself: capability collection calls this with the agent's
86    // model and collects the resolved capability's contributions in place of this
87    // one's. Models with native support get the hosted OpenAI mechanism (no
88    // client-side tool or hook); everything else — including an unknown model —
89    // gets the provider-agnostic client-side mechanism, which is safe everywhere.
90    fn resolve_for_model(&self, model: Option<&str>) -> Option<&dyn Capability> {
91        if model.is_some_and(model_supports_native_tool_search) {
92            Some(&self.openai)
93        } else {
94            Some(&self.generic)
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::capabilities::{
103        CapabilityRegistry, OPENAI_TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_CAPABILITY_ID,
104    };
105
106    #[test]
107    fn test_capability_metadata() {
108        let cap = AutoToolSearchCapability::new();
109        assert_eq!(cap.id(), AUTO_TOOL_SEARCH_CAPABILITY_ID);
110        assert_eq!(cap.name(), "Auto Tool Search");
111        assert_eq!(cap.category(), Some("Optimization"));
112    }
113
114    #[test]
115    fn test_resolves_to_generic_without_model() {
116        // No model known → safe provider-agnostic client-side mechanism.
117        let cap = AutoToolSearchCapability::new();
118        let resolved = cap.resolve_for_model(None).expect("dispatches");
119        assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
120        // The generic mechanism carries the client-side tool + hook.
121        assert_eq!(resolved.tools().len(), 1);
122        assert_eq!(resolved.tool_definition_hooks().len(), 1);
123    }
124
125    #[test]
126    fn test_resolves_to_generic_on_non_native_model() {
127        let cap = AutoToolSearchCapability::new();
128        let resolved = cap
129            .resolve_for_model(Some("claude-sonnet-4-5-20250514"))
130            .expect("dispatches");
131        assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
132    }
133
134    #[test]
135    fn test_resolves_to_hosted_on_native_model() {
136        let cap = AutoToolSearchCapability::new();
137        let resolved = cap.resolve_for_model(Some("gpt-5.4")).expect("dispatches");
138        assert_eq!(resolved.id(), OPENAI_TOOL_SEARCH_CAPABILITY_ID);
139        // The hosted mechanism contributes no client-side tool or hook.
140        assert!(resolved.tools().is_empty());
141        assert!(resolved.tool_definition_hooks().is_empty());
142    }
143
144    #[test]
145    fn test_capability_registered_in_builtins() {
146        let registry = CapabilityRegistry::with_builtins();
147        let cap = registry.get(AUTO_TOOL_SEARCH_CAPABILITY_ID).unwrap();
148        assert_eq!(cap.id(), AUTO_TOOL_SEARCH_CAPABILITY_ID);
149    }
150}