Skip to main content

agentkit_provider_anthropic/
server_tool.rs

1//! Server-side tools that Anthropic executes inside the model turn.
2//!
3//! [`ServerTool`] is the forward-compatible extension point. Built-in tools
4//! (web search, web fetch, code execution) implement it; use [`RawServerTool`]
5//! to pass through any new tool type Anthropic ships before this crate adds
6//! first-class support.
7
8use std::sync::Arc;
9
10use serde_json::{Value, json};
11
12/// A server-side tool definition appended to the `tools` array of a Messages
13/// API request.
14///
15/// Implementations must produce the JSON object Anthropic expects and declare
16/// any `anthropic-beta` header flags required to activate the tool.
17pub trait ServerTool: Send + Sync {
18    /// JSON object to append to the top-level `tools` array.
19    ///
20    /// The object MUST include the versioned `type` string (e.g.
21    /// `"web_search_20260209"`) and a user-visible `name`.
22    fn to_tool_json(&self) -> Value;
23
24    /// Returns any `anthropic-beta` header flags required for this tool.
25    ///
26    /// Unioned across all configured server tools and appended to the request's
27    /// beta header list.
28    fn beta_flags(&self) -> Vec<String> {
29        Vec::new()
30    }
31}
32
33/// Convenience alias for an owned, clonable handle to a [`ServerTool`].
34pub type ServerToolHandle = Arc<dyn ServerTool>;
35
36/// Wraps any `ServerTool` as an `Arc` so it can live in [`AnthropicConfig`].
37///
38/// [`AnthropicConfig`]: crate::AnthropicConfig
39pub fn boxed<T: ServerTool + 'static>(tool: T) -> ServerToolHandle {
40    Arc::new(tool)
41}
42
43/// The default `type` version string for `WebSearchTool`.
44pub const DEFAULT_WEB_SEARCH_VERSION: &str = "web_search_20260209";
45/// The default `type` version string for `WebFetchTool`.
46pub const DEFAULT_WEB_FETCH_VERSION: &str = "web_fetch_20260309";
47/// The default `type` version string for `CodeExecutionTool`.
48pub const DEFAULT_CODE_EXECUTION_VERSION: &str = "code_execution_20260120";
49/// The default `type` version string for `BashCodeExecutionTool`.
50pub const DEFAULT_BASH_EXECUTION_VERSION: &str = "bash_code_execution_20260120";
51/// The default `type` version string for `TextEditorCodeExecutionTool`.
52pub const DEFAULT_TEXT_EDITOR_EXECUTION_VERSION: &str = "text_editor_code_execution_20260120";
53
54/// Anthropic server-side web search.
55#[derive(Clone, Debug)]
56pub struct WebSearchTool {
57    /// Versioned `type` string sent in the request.
58    pub version: String,
59    /// Optional cap on how many searches the model may issue in one turn.
60    pub max_uses: Option<u32>,
61    /// Restrict results to these domains (mutually exclusive with `blocked_domains`).
62    pub allowed_domains: Vec<String>,
63    /// Exclude these domains from results.
64    pub blocked_domains: Vec<String>,
65}
66
67impl Default for WebSearchTool {
68    fn default() -> Self {
69        Self {
70            version: DEFAULT_WEB_SEARCH_VERSION.into(),
71            max_uses: None,
72            allowed_domains: Vec::new(),
73            blocked_domains: Vec::new(),
74        }
75    }
76}
77
78impl WebSearchTool {
79    /// Builds a web-search tool with the default version.
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Overrides the versioned `type` string.
85    pub fn with_version(mut self, version: impl Into<String>) -> Self {
86        self.version = version.into();
87        self
88    }
89
90    /// Sets the maximum number of searches per turn.
91    pub fn with_max_uses(mut self, max: u32) -> Self {
92        self.max_uses = Some(max);
93        self
94    }
95
96    /// Restricts results to the given domain list.
97    pub fn with_allowed_domains(mut self, domains: impl IntoIterator<Item = String>) -> Self {
98        self.allowed_domains = domains.into_iter().collect();
99        self
100    }
101
102    /// Excludes the given domain list.
103    pub fn with_blocked_domains(mut self, domains: impl IntoIterator<Item = String>) -> Self {
104        self.blocked_domains = domains.into_iter().collect();
105        self
106    }
107}
108
109impl ServerTool for WebSearchTool {
110    fn to_tool_json(&self) -> Value {
111        let mut body = serde_json::Map::new();
112        body.insert("type".into(), Value::String(self.version.clone()));
113        body.insert("name".into(), Value::String("web_search".into()));
114        if let Some(max) = self.max_uses {
115            body.insert("max_uses".into(), Value::from(max));
116        }
117        if !self.allowed_domains.is_empty() {
118            body.insert(
119                "allowed_domains".into(),
120                Value::Array(
121                    self.allowed_domains
122                        .iter()
123                        .cloned()
124                        .map(Value::String)
125                        .collect(),
126                ),
127            );
128        }
129        if !self.blocked_domains.is_empty() {
130            body.insert(
131                "blocked_domains".into(),
132                Value::Array(
133                    self.blocked_domains
134                        .iter()
135                        .cloned()
136                        .map(Value::String)
137                        .collect(),
138                ),
139            );
140        }
141        Value::Object(body)
142    }
143}
144
145/// Anthropic server-side web fetch.
146#[derive(Clone, Debug)]
147pub struct WebFetchTool {
148    /// Versioned `type` string.
149    pub version: String,
150    /// Optional cap on fetches per turn.
151    pub max_uses: Option<u32>,
152    /// Optional cap on tokens extracted from a single fetched page.
153    pub max_content_tokens: Option<u32>,
154    /// Opt in to Anthropic's fetch result caching.
155    pub use_cache: Option<bool>,
156}
157
158impl Default for WebFetchTool {
159    fn default() -> Self {
160        Self {
161            version: DEFAULT_WEB_FETCH_VERSION.into(),
162            max_uses: None,
163            max_content_tokens: None,
164            use_cache: None,
165        }
166    }
167}
168
169impl WebFetchTool {
170    pub fn new() -> Self {
171        Self::default()
172    }
173
174    pub fn with_version(mut self, version: impl Into<String>) -> Self {
175        self.version = version.into();
176        self
177    }
178
179    pub fn with_max_uses(mut self, max: u32) -> Self {
180        self.max_uses = Some(max);
181        self
182    }
183
184    pub fn with_max_content_tokens(mut self, max: u32) -> Self {
185        self.max_content_tokens = Some(max);
186        self
187    }
188
189    pub fn with_use_cache(mut self, enabled: bool) -> Self {
190        self.use_cache = Some(enabled);
191        self
192    }
193}
194
195impl ServerTool for WebFetchTool {
196    fn to_tool_json(&self) -> Value {
197        let mut body = serde_json::Map::new();
198        body.insert("type".into(), Value::String(self.version.clone()));
199        body.insert("name".into(), Value::String("web_fetch".into()));
200        if let Some(max) = self.max_uses {
201            body.insert("max_uses".into(), Value::from(max));
202        }
203        if let Some(max) = self.max_content_tokens {
204            body.insert("max_content_tokens".into(), Value::from(max));
205        }
206        if let Some(flag) = self.use_cache {
207            body.insert("use_cache".into(), Value::Bool(flag));
208        }
209        Value::Object(body)
210    }
211}
212
213/// Anthropic server-side Python code execution sandbox.
214#[derive(Clone, Debug)]
215pub struct CodeExecutionTool {
216    pub version: String,
217}
218
219impl Default for CodeExecutionTool {
220    fn default() -> Self {
221        Self {
222            version: DEFAULT_CODE_EXECUTION_VERSION.into(),
223        }
224    }
225}
226
227impl CodeExecutionTool {
228    pub fn new() -> Self {
229        Self::default()
230    }
231
232    pub fn with_version(mut self, version: impl Into<String>) -> Self {
233        self.version = version.into();
234        self
235    }
236}
237
238impl ServerTool for CodeExecutionTool {
239    fn to_tool_json(&self) -> Value {
240        json!({
241            "type": self.version,
242            "name": "code_execution",
243        })
244    }
245}
246
247/// Anthropic server-side bash execution sandbox.
248#[derive(Clone, Debug)]
249pub struct BashCodeExecutionTool {
250    pub version: String,
251}
252
253impl Default for BashCodeExecutionTool {
254    fn default() -> Self {
255        Self {
256            version: DEFAULT_BASH_EXECUTION_VERSION.into(),
257        }
258    }
259}
260
261impl BashCodeExecutionTool {
262    pub fn new() -> Self {
263        Self::default()
264    }
265
266    pub fn with_version(mut self, version: impl Into<String>) -> Self {
267        self.version = version.into();
268        self
269    }
270}
271
272impl ServerTool for BashCodeExecutionTool {
273    fn to_tool_json(&self) -> Value {
274        json!({
275            "type": self.version,
276            "name": "bash_code_execution",
277        })
278    }
279}
280
281/// Anthropic server-side text editor sandbox.
282#[derive(Clone, Debug)]
283pub struct TextEditorCodeExecutionTool {
284    pub version: String,
285}
286
287impl Default for TextEditorCodeExecutionTool {
288    fn default() -> Self {
289        Self {
290            version: DEFAULT_TEXT_EDITOR_EXECUTION_VERSION.into(),
291        }
292    }
293}
294
295impl TextEditorCodeExecutionTool {
296    pub fn new() -> Self {
297        Self::default()
298    }
299
300    pub fn with_version(mut self, version: impl Into<String>) -> Self {
301        self.version = version.into();
302        self
303    }
304}
305
306impl ServerTool for TextEditorCodeExecutionTool {
307    fn to_tool_json(&self) -> Value {
308        json!({
309            "type": self.version,
310            "name": "text_editor_code_execution",
311        })
312    }
313}
314
315/// Passthrough wrapper for any server tool Anthropic has shipped but this
316/// crate has not yet added first-class support for.
317///
318/// The embedded `value` is spliced into the `tools` array verbatim; any
319/// required beta headers are unioned into the request's beta header list.
320#[derive(Clone, Debug)]
321pub struct RawServerTool {
322    /// JSON object to splice into `tools[]`.
323    pub value: Value,
324    /// Beta flags this tool needs.
325    pub betas: Vec<String>,
326}
327
328impl RawServerTool {
329    /// Wraps a raw tool definition with no extra beta flags.
330    pub fn new(value: Value) -> Self {
331        Self {
332            value,
333            betas: Vec::new(),
334        }
335    }
336
337    /// Adds a required beta flag.
338    pub fn with_beta(mut self, flag: impl Into<String>) -> Self {
339        self.betas.push(flag.into());
340        self
341    }
342}
343
344impl ServerTool for RawServerTool {
345    fn to_tool_json(&self) -> Value {
346        self.value.clone()
347    }
348
349    fn beta_flags(&self) -> Vec<String> {
350        self.betas.clone()
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn web_search_serializes_filters() {
360        let tool = WebSearchTool::new()
361            .with_max_uses(3)
362            .with_allowed_domains(["docs.rs".to_string()]);
363        let json = tool.to_tool_json();
364        assert_eq!(json["type"], DEFAULT_WEB_SEARCH_VERSION);
365        assert_eq!(json["name"], "web_search");
366        assert_eq!(json["max_uses"], 3);
367        assert_eq!(json["allowed_domains"][0], "docs.rs");
368    }
369
370    #[test]
371    fn raw_tool_preserves_body_and_betas() {
372        let raw = RawServerTool::new(json!({
373            "type": "future_tool_20271231",
374            "name": "future_tool",
375        }))
376        .with_beta("future-tool-2027-12-31");
377
378        assert_eq!(raw.to_tool_json()["name"], "future_tool");
379        assert_eq!(raw.beta_flags(), vec!["future-tool-2027-12-31".to_string()]);
380    }
381}