Skip to main content

claude_api/messages/
tools.rs

1//! Tool definitions and tool-choice configuration.
2//!
3//! [`Tool`] is the unit you pass to `messages.create(...)` to expose either
4//! a user-defined function ([`CustomTool`]) or a server-side built-in
5//! ([`BuiltinTool`]) like web search or code execution. [`ToolChoice`]
6//! controls whether and which tool the model must invoke.
7//!
8//! For high-level dispatch (registry, parallel calls, agent loop) see
9//! [`crate::tool_dispatch`]. For `#[derive(Tool)]` see `claude-api-derive`.
10//!
11//! # Define and send a custom tool
12//!
13//! ```no_run
14//! use claude_api::{Client, messages::{CreateMessageRequest, Tool, CustomTool},
15//!     types::ModelId};
16//! use serde_json::json;
17//! # async fn run() -> Result<(), claude_api::Error> {
18//! let weather = Tool::Custom(
19//!     CustomTool::new(
20//!         "get_weather",
21//!         json!({
22//!             "type": "object",
23//!             "properties": {"city": {"type": "string"}},
24//!             "required": ["city"]
25//!         }),
26//!     )
27//!     .description("Return the current weather for a city."),
28//! );
29//! let client = Client::new(std::env::var("ANTHROPIC_API_KEY").unwrap());
30//! let resp = client
31//!     .messages()
32//!     .create(
33//!         CreateMessageRequest::builder()
34//!             .model(ModelId::SONNET_4_6)
35//!             .max_tokens(512)
36//!             .tools(vec![weather])
37//!             .user("What's the weather in Tokyo?")
38//!             .build()?,
39//!     )
40//!     .await?;
41//! println!("{:?}", resp.stop_reason);
42//! # Ok(())
43//! # }
44//! ```
45
46use serde::{Deserialize, Serialize};
47
48use crate::messages::cache::CacheControl;
49
50/// A tool definition the model can call during generation.
51///
52/// The wire form is [`untagged`](https://serde.rs/enum-representations.html#untagged):
53/// custom tools are recognized by the presence of an `input_schema` field;
54/// anything else is treated as a built-in tool.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56#[serde(untagged)]
57pub enum Tool {
58    /// A user-defined tool with a JSON Schema for input validation.
59    Custom(CustomTool),
60    /// A server-side built-in tool (web search, computer use, code execution, etc.).
61    /// Typed via [`KnownBuiltinTool`] with an `Other(Value)` arm for
62    /// forward-compatibility with new tools / new tool versions.
63    Builtin(BuiltinTool),
64}
65
66impl Tool {
67    /// Convenience constructor for a custom tool with a manually provided JSON Schema.
68    pub fn custom(name: impl Into<String>, input_schema: serde_json::Value) -> Self {
69        Self::Custom(CustomTool {
70            name: name.into(),
71            description: None,
72            input_schema,
73            cache_control: None,
74        })
75    }
76
77    /// Escape hatch: build a `Tool::Builtin` from raw JSON. Use the typed
78    /// constructors ([`Self::web_search`], [`Self::computer`], [`Self::bash`],
79    /// [`Self::text_editor`], [`Self::code_execution`]) instead when the
80    /// tool type is known to the SDK; this is for unknown / future tools.
81    ///
82    /// ```
83    /// use claude_api::messages::tools::Tool;
84    /// // Unknown future tool -- round-trips through Builtin::Other.
85    /// let t = Tool::builtin(serde_json::json!({
86    ///     "type": "future_tool_2099",
87    ///     "name": "future"
88    /// }));
89    /// assert!(matches!(t, Tool::Builtin(_)));
90    /// ```
91    pub fn builtin(value: serde_json::Value) -> Self {
92        Self::Builtin(BuiltinTool::Other(value))
93    }
94
95    /// Default-config web search tool (`web_search_20250305`).
96    #[must_use]
97    pub fn web_search() -> Self {
98        Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::WebSearch20250305 {
99            name: "web_search".into(),
100            max_uses: None,
101            allowed_domains: None,
102            blocked_domains: None,
103            user_location: None,
104            cache_control: None,
105        }))
106    }
107
108    /// Computer-use tool with the given display dimensions
109    /// (`computer_20250124`).
110    #[must_use]
111    pub fn computer(display_width_px: u32, display_height_px: u32) -> Self {
112        Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::Computer20250124 {
113            name: "computer".into(),
114            display_width_px,
115            display_height_px,
116            display_number: None,
117            cache_control: None,
118        }))
119    }
120
121    /// Default-config bash tool (`bash_20250124`).
122    #[must_use]
123    pub fn bash() -> Self {
124        Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::Bash20250124 {
125            name: "bash".into(),
126            cache_control: None,
127        }))
128    }
129
130    /// Default-config text editor tool (`text_editor_20250124`).
131    #[must_use]
132    pub fn text_editor() -> Self {
133        Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::TextEditor20250124 {
134            name: "str_replace_editor".into(),
135            cache_control: None,
136        }))
137    }
138
139    /// Default-config code execution tool (`code_execution_20250825`).
140    #[must_use]
141    pub fn code_execution() -> Self {
142        Self::Builtin(BuiltinTool::Known(
143            KnownBuiltinTool::CodeExecution20250825 {
144                name: "code_execution".into(),
145                cache_control: None,
146            },
147        ))
148    }
149
150    /// Build a custom tool whose input schema is derived from a Rust type via [`schemars`].
151    ///
152    /// # Panics
153    ///
154    /// Panics only if the generated `RootSchema` fails to JSON-serialize,
155    /// which `schemars` guarantees not to happen for any type that implements
156    /// [`schemars::JsonSchema`].
157    #[cfg(feature = "schemars-tools")]
158    #[cfg_attr(docsrs, doc(cfg(feature = "schemars-tools")))]
159    pub fn from_schemars<T: schemars::JsonSchema>(name: impl Into<String>) -> Self {
160        let schema = schemars::r#gen::SchemaGenerator::default().into_root_schema_for::<T>();
161        let schema_value =
162            serde_json::to_value(schema).expect("RootSchema is always JSON-serializable");
163        Self::Custom(CustomTool {
164            name: name.into(),
165            description: None,
166            input_schema: schema_value,
167            cache_control: None,
168        })
169    }
170}
171
172/// User-defined tool with a JSON Schema describing its input.
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174#[non_exhaustive]
175pub struct CustomTool {
176    /// Tool name. Must be unique within a request.
177    pub name: String,
178    /// Human-readable description; helps the model decide when to invoke.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub description: Option<String>,
181    /// JSON Schema for the tool's input arguments.
182    pub input_schema: serde_json::Value,
183    /// Optional cache breakpoint to apply to this tool's definition.
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub cache_control: Option<CacheControl>,
186}
187
188impl CustomTool {
189    /// Construct a custom tool with no description and no cache control.
190    pub fn new(name: impl Into<String>, input_schema: serde_json::Value) -> Self {
191        Self {
192            name: name.into(),
193            description: None,
194            input_schema,
195            cache_control: None,
196        }
197    }
198
199    /// Set the tool description.
200    #[must_use]
201    pub fn description(mut self, description: impl Into<String>) -> Self {
202        self.description = Some(description.into());
203        self
204    }
205
206    /// Apply a cache breakpoint at this tool's definition.
207    #[must_use]
208    pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
209        self.cache_control = Some(cache_control);
210        self
211    }
212
213    /// Shorthand: apply an ephemeral cache breakpoint at the default
214    /// (5-minute) TTL.
215    #[must_use]
216    pub fn with_ephemeral_cache(self) -> Self {
217        self.cache_control(CacheControl::ephemeral())
218    }
219}
220
221/// Server-side built-in tool from Anthropic's catalog (web search, computer
222/// use, code execution, bash, text editor).
223///
224/// Forward-compatible wrapper around [`KnownBuiltinTool`]; unknown tool
225/// types deserialize into [`BuiltinTool::Other`] preserving the raw JSON.
226/// New built-in tools or new versions of existing ones (the date-stamped
227/// type tags) work without an SDK update.
228#[derive(Debug, Clone, PartialEq)]
229pub enum BuiltinTool {
230    /// A built-in tool whose `type` is recognized by this SDK version.
231    Known(KnownBuiltinTool),
232    /// A built-in tool whose `type` is not recognized; raw JSON preserved.
233    Other(serde_json::Value),
234}
235
236/// All server-side built-in tool variants known to this SDK version.
237///
238/// `#[non_exhaustive]` on both the enum (so adding a variant is non-breaking)
239/// and on each struct variant (so adding a field is non-breaking).
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241#[serde(tag = "type")]
242#[non_exhaustive]
243pub enum KnownBuiltinTool {
244    /// Web search tool, version `2025-03-05`. The model can issue
245    /// keyword searches; results are returned as
246    /// [`KnownBlock::WebSearchToolResult`](crate::messages::content::KnownBlock::WebSearchToolResult)
247    /// blocks.
248    #[serde(rename = "web_search_20250305")]
249    WebSearch20250305 {
250        /// Tool name surfaced to the model. Conventionally `"web_search"`.
251        name: String,
252        /// Maximum number of search requests the model may make per turn.
253        #[serde(default, skip_serializing_if = "Option::is_none")]
254        max_uses: Option<u32>,
255        /// If set, restrict searches to these domains.
256        #[serde(default, skip_serializing_if = "Option::is_none")]
257        allowed_domains: Option<Vec<String>>,
258        /// If set, exclude these domains from search results.
259        #[serde(default, skip_serializing_if = "Option::is_none")]
260        blocked_domains: Option<Vec<String>>,
261        /// Approximate user location to bias results.
262        #[serde(default, skip_serializing_if = "Option::is_none")]
263        user_location: Option<UserLocation>,
264        /// Optional cache breakpoint on the tool definition.
265        #[serde(default, skip_serializing_if = "Option::is_none")]
266        cache_control: Option<CacheControl>,
267    },
268    /// Computer-use tool, version `2025-01-24`. Lets the model take
269    /// screenshots and synthesize mouse/keyboard input.
270    #[serde(rename = "computer_20250124")]
271    Computer20250124 {
272        /// Tool name. Conventionally `"computer"`.
273        name: String,
274        /// Display width in pixels.
275        display_width_px: u32,
276        /// Display height in pixels.
277        display_height_px: u32,
278        /// Optional X11 display number.
279        #[serde(default, skip_serializing_if = "Option::is_none")]
280        display_number: Option<u32>,
281        /// Optional cache breakpoint.
282        #[serde(default, skip_serializing_if = "Option::is_none")]
283        cache_control: Option<CacheControl>,
284    },
285    /// Bash tool, version `2025-01-24`.
286    #[serde(rename = "bash_20250124")]
287    Bash20250124 {
288        /// Tool name. Conventionally `"bash"`.
289        name: String,
290        /// Optional cache breakpoint.
291        #[serde(default, skip_serializing_if = "Option::is_none")]
292        cache_control: Option<CacheControl>,
293    },
294    /// Text editor tool, version `2025-01-24`.
295    #[serde(rename = "text_editor_20250124")]
296    TextEditor20250124 {
297        /// Tool name. Conventionally `"str_replace_editor"`.
298        name: String,
299        /// Optional cache breakpoint.
300        #[serde(default, skip_serializing_if = "Option::is_none")]
301        cache_control: Option<CacheControl>,
302    },
303    /// Code execution tool, version `2025-08-25`. Server-side sandboxed
304    /// Python execution.
305    #[serde(rename = "code_execution_20250825")]
306    CodeExecution20250825 {
307        /// Tool name. Conventionally `"code_execution"`.
308        name: String,
309        /// Optional cache breakpoint.
310        #[serde(default, skip_serializing_if = "Option::is_none")]
311        cache_control: Option<CacheControl>,
312    },
313}
314
315const KNOWN_BUILTIN_TAGS: &[&str] = &[
316    "web_search_20250305",
317    "computer_20250124",
318    "bash_20250124",
319    "text_editor_20250124",
320    "code_execution_20250825",
321];
322
323impl serde::Serialize for BuiltinTool {
324    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
325        match self {
326            BuiltinTool::Known(k) => k.serialize(s),
327            BuiltinTool::Other(v) => v.serialize(s),
328        }
329    }
330}
331
332impl<'de> serde::Deserialize<'de> for BuiltinTool {
333    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
334        let raw = serde_json::Value::deserialize(d)?;
335        crate::forward_compat::dispatch_known_or_other(
336            raw,
337            KNOWN_BUILTIN_TAGS,
338            BuiltinTool::Known,
339            BuiltinTool::Other,
340        )
341        .map_err(serde::de::Error::custom)
342    }
343}
344
345impl From<KnownBuiltinTool> for BuiltinTool {
346    fn from(k: KnownBuiltinTool) -> Self {
347        BuiltinTool::Known(k)
348    }
349}
350
351/// Approximate user location, used to bias web-search results.
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353#[non_exhaustive]
354pub struct UserLocation {
355    /// Location specificity. Anthropic accepts `"approximate"`.
356    #[serde(rename = "type", default = "default_user_location_kind")]
357    pub kind: String,
358    /// City name (English).
359    #[serde(default, skip_serializing_if = "Option::is_none")]
360    pub city: Option<String>,
361    /// Region (state, province) name (English).
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub region: Option<String>,
364    /// ISO 3166-1 alpha-2 country code.
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub country: Option<String>,
367    /// IANA timezone identifier (e.g. `America/Los_Angeles`).
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub timezone: Option<String>,
370}
371
372fn default_user_location_kind() -> String {
373    "approximate".to_owned()
374}
375
376/// How (or whether) the model should invoke a tool on this turn.
377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
378#[serde(tag = "type", rename_all = "snake_case")]
379#[non_exhaustive]
380pub enum ToolChoice {
381    /// Default: the model decides whether and which tool to use.
382    Auto {
383        /// Set `true` to force serial tool calls when the model decides to use tools.
384        #[serde(default, skip_serializing_if = "Option::is_none")]
385        disable_parallel_tool_use: Option<bool>,
386    },
387    /// The model must use a tool, but may choose which one.
388    Any {
389        /// Set `true` to force serial tool calls.
390        #[serde(default, skip_serializing_if = "Option::is_none")]
391        disable_parallel_tool_use: Option<bool>,
392    },
393    /// The model must use the named tool.
394    Tool {
395        /// Tool name the model must invoke.
396        name: String,
397        /// Set `true` to force a single tool call.
398        #[serde(default, skip_serializing_if = "Option::is_none")]
399        disable_parallel_tool_use: Option<bool>,
400    },
401    /// The model must not use any tool.
402    None,
403}
404
405impl ToolChoice {
406    /// Default `auto` choice with no parallelism override.
407    #[must_use]
408    pub fn auto() -> Self {
409        Self::Auto {
410            disable_parallel_tool_use: None,
411        }
412    }
413
414    /// `any` -- model must invoke some tool of its choice.
415    #[must_use]
416    pub fn any() -> Self {
417        Self::Any {
418            disable_parallel_tool_use: None,
419        }
420    }
421
422    /// Force the model to invoke the named tool.
423    #[must_use]
424    pub fn tool(name: impl Into<String>) -> Self {
425        Self::Tool {
426            name: name.into(),
427            disable_parallel_tool_use: None,
428        }
429    }
430
431    /// Forbid all tool use.
432    #[must_use]
433    pub fn none() -> Self {
434        Self::None
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use pretty_assertions::assert_eq;
442    use serde_json::json;
443
444    #[test]
445    fn custom_tool_round_trips() {
446        let t = Tool::Custom(
447            CustomTool::new(
448                "get_weather",
449                json!({"type": "object", "properties": {"city": {"type": "string"}}}),
450            )
451            .description("Look up the weather"),
452        );
453        let v = serde_json::to_value(&t).unwrap();
454        assert_eq!(
455            v,
456            json!({
457                "name": "get_weather",
458                "description": "Look up the weather",
459                "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}}
460            })
461        );
462        let parsed: Tool = serde_json::from_value(v).unwrap();
463        assert_eq!(parsed, t);
464    }
465
466    #[test]
467    fn custom_tool_with_cache_control_round_trips() {
468        let t = Tool::Custom(
469            CustomTool::new("noop", json!({"type": "object"}))
470                .cache_control(CacheControl::ephemeral()),
471        );
472        let v = serde_json::to_value(&t).unwrap();
473        assert_eq!(
474            v,
475            json!({
476                "name": "noop",
477                "input_schema": {"type": "object"},
478                "cache_control": {"type": "ephemeral"}
479            })
480        );
481        let parsed: Tool = serde_json::from_value(v).unwrap();
482        assert_eq!(parsed, t);
483    }
484
485    #[test]
486    fn unknown_builtin_round_trips_through_other() {
487        // The escape-hatch path: unknown tool type stays as Builtin::Other.
488        let raw = json!({"type": "future_builtin_2099", "name": "future_tool"});
489        let t = Tool::builtin(raw.clone());
490        let serialized = serde_json::to_value(&t).unwrap();
491        assert_eq!(serialized, raw, "Other must serialize transparently");
492        let parsed: Tool = serde_json::from_value(serialized).unwrap();
493        assert_eq!(parsed, t);
494    }
495
496    #[test]
497    fn known_builtin_parses_into_typed_variant() {
498        let raw = json!({
499            "type": "web_search_20250305",
500            "name": "web_search",
501            "max_uses": 5
502        });
503        let parsed: Tool = serde_json::from_value(raw).unwrap();
504        match parsed {
505            Tool::Builtin(BuiltinTool::Known(KnownBuiltinTool::WebSearch20250305 {
506                name,
507                max_uses,
508                ..
509            })) => {
510                assert_eq!(name, "web_search");
511                assert_eq!(max_uses, Some(5));
512            }
513            other => panic!("expected typed WebSearch20250305, got {other:?}"),
514        }
515    }
516
517    #[test]
518    fn web_search_default_serializes_to_minimal_wire_form() {
519        let t = Tool::web_search();
520        let v = serde_json::to_value(&t).unwrap();
521        assert_eq!(
522            v,
523            json!({"type": "web_search_20250305", "name": "web_search"})
524        );
525    }
526
527    #[test]
528    fn web_search_with_options_round_trips() {
529        let t = Tool::Builtin(BuiltinTool::Known(KnownBuiltinTool::WebSearch20250305 {
530            name: "web_search".into(),
531            max_uses: Some(3),
532            allowed_domains: Some(vec!["wikipedia.org".into()]),
533            blocked_domains: None,
534            user_location: Some(UserLocation {
535                kind: "approximate".into(),
536                city: Some("Paris".into()),
537                region: None,
538                country: Some("FR".into()),
539                timezone: Some("Europe/Paris".into()),
540            }),
541            cache_control: Some(CacheControl::ephemeral()),
542        }));
543        let v = serde_json::to_value(&t).unwrap();
544        assert_eq!(
545            v,
546            json!({
547                "type": "web_search_20250305",
548                "name": "web_search",
549                "max_uses": 3,
550                "allowed_domains": ["wikipedia.org"],
551                "user_location": {
552                    "type": "approximate",
553                    "city": "Paris",
554                    "country": "FR",
555                    "timezone": "Europe/Paris"
556                },
557                "cache_control": {"type": "ephemeral"}
558            })
559        );
560        let parsed: Tool = serde_json::from_value(v).unwrap();
561        assert_eq!(parsed, t);
562    }
563
564    #[test]
565    fn computer_default_serializes_with_required_dims() {
566        let t = Tool::computer(1920, 1080);
567        let v = serde_json::to_value(&t).unwrap();
568        assert_eq!(
569            v,
570            json!({
571                "type": "computer_20250124",
572                "name": "computer",
573                "display_width_px": 1920,
574                "display_height_px": 1080
575            })
576        );
577    }
578
579    #[test]
580    fn bash_text_editor_code_execution_defaults_serialize() {
581        assert_eq!(
582            serde_json::to_value(Tool::bash()).unwrap(),
583            json!({"type": "bash_20250124", "name": "bash"})
584        );
585        assert_eq!(
586            serde_json::to_value(Tool::text_editor()).unwrap(),
587            json!({"type": "text_editor_20250124", "name": "str_replace_editor"})
588        );
589        assert_eq!(
590            serde_json::to_value(Tool::code_execution()).unwrap(),
591            json!({"type": "code_execution_20250825", "name": "code_execution"})
592        );
593    }
594
595    #[test]
596    fn malformed_known_builtin_errors_not_silent_fallthrough() {
597        // Known type, but display_width_px is wrong shape.
598        let raw = json!({
599            "type": "computer_20250124",
600            "name": "computer",
601            "display_width_px": "wide",
602            "display_height_px": 1080
603        });
604        let result: Result<Tool, _> = serde_json::from_value(raw);
605        assert!(
606            result.is_err(),
607            "malformed known builtin must error, not fall through to Other"
608        );
609    }
610
611    #[test]
612    fn untagged_enum_disambiguates_custom_from_builtin() {
613        // Has `input_schema` -> Custom
614        let custom: Tool = serde_json::from_value(json!({
615            "name": "x",
616            "input_schema": {"type": "object"}
617        }))
618        .unwrap();
619        assert!(matches!(custom, Tool::Custom(_)));
620
621        // No `input_schema` -> Builtin
622        let builtin: Tool = serde_json::from_value(json!({
623            "type": "web_search_20250305",
624            "name": "web_search"
625        }))
626        .unwrap();
627        assert!(matches!(builtin, Tool::Builtin(_)));
628    }
629
630    #[test]
631    fn tool_choice_auto_round_trips() {
632        let c = ToolChoice::auto();
633        let v = serde_json::to_value(&c).unwrap();
634        assert_eq!(v, json!({"type": "auto"}));
635        let parsed: ToolChoice = serde_json::from_value(v).unwrap();
636        assert_eq!(parsed, c);
637    }
638
639    #[test]
640    fn tool_choice_any_with_no_parallel_round_trips() {
641        let c = ToolChoice::Any {
642            disable_parallel_tool_use: Some(true),
643        };
644        let v = serde_json::to_value(&c).unwrap();
645        assert_eq!(v, json!({"type": "any", "disable_parallel_tool_use": true}));
646        let parsed: ToolChoice = serde_json::from_value(v).unwrap();
647        assert_eq!(parsed, c);
648    }
649
650    #[test]
651    fn tool_choice_specific_tool_round_trips() {
652        let c = ToolChoice::tool("get_weather");
653        let v = serde_json::to_value(&c).unwrap();
654        assert_eq!(v, json!({"type": "tool", "name": "get_weather"}));
655        let parsed: ToolChoice = serde_json::from_value(v).unwrap();
656        assert_eq!(parsed, c);
657    }
658
659    #[test]
660    fn tool_choice_none_round_trips() {
661        let c = ToolChoice::none();
662        let v = serde_json::to_value(&c).unwrap();
663        assert_eq!(v, json!({"type": "none"}));
664        let parsed: ToolChoice = serde_json::from_value(v).unwrap();
665        assert_eq!(parsed, c);
666    }
667
668    #[cfg(feature = "schemars-tools")]
669    #[test]
670    fn from_schemars_builds_custom_tool() {
671        #[derive(schemars::JsonSchema, serde::Deserialize)]
672        #[allow(dead_code)]
673        struct Args {
674            city: String,
675            units: Option<String>,
676        }
677
678        let t = Tool::from_schemars::<Args>("get_weather");
679        match t {
680            Tool::Custom(c) => {
681                assert_eq!(c.name, "get_weather");
682                // Schema should be a JSON object describing the type.
683                assert!(c.input_schema.is_object());
684            }
685            Tool::Builtin(_) => panic!("expected Custom"),
686        }
687    }
688}