1use alloc::{string::String, sync::Arc};
4
5use serde::{Deserialize, Serialize};
6
7use super::OutputSanitization;
8use crate::compat::OnceLock;
9
10#[derive(
12 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, schemars::JsonSchema,
13)]
14#[serde(rename_all = "snake_case")]
15pub enum ToolEffect {
16 ReadOnly,
18 #[default]
20 Mutating,
21 Destructive,
24}
25
26impl ToolEffect {
27 #[must_use]
29 pub const fn is_read_only(self) -> bool {
30 matches!(self, Self::ReadOnly)
31 }
32
33 #[must_use]
35 pub const fn is_destructive(self) -> bool {
36 matches!(self, Self::Destructive)
37 }
38}
39
40#[non_exhaustive]
42#[derive(Debug, Serialize)]
43pub struct ToolDefinition {
44 pub name: String,
46 pub description: String,
48 pub parameter_schema: serde_json::Value,
50 pub effect: ToolEffect,
52 #[serde(default, skip_serializing_if = "OutputSanitization::is_none")]
54 pub output_sanitization: OutputSanitization,
55 #[serde(skip)]
57 parameter_schema_json: OnceLock<Arc<str>>,
58}
59
60impl Clone for ToolDefinition {
61 fn clone(&self) -> Self {
62 Self {
63 name: self.name.clone(),
64 description: self.description.clone(),
65 parameter_schema: self.parameter_schema.clone(),
66 effect: self.effect,
67 output_sanitization: self.output_sanitization,
68 parameter_schema_json: OnceLock::new(),
69 }
70 }
71}
72
73impl PartialEq for ToolDefinition {
74 fn eq(&self, other: &Self) -> bool {
75 self.name == other.name
76 && self.description == other.description
77 && self.parameter_schema == other.parameter_schema
78 && self.effect == other.effect
79 && self.output_sanitization == other.output_sanitization
80 }
81}
82
83impl Eq for ToolDefinition {}
84
85impl<'de> Deserialize<'de> for ToolDefinition {
86 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87 where
88 D: serde::Deserializer<'de>,
89 {
90 #[derive(Deserialize)]
91 struct RawDef {
92 name: String,
93 description: String,
94 parameter_schema: serde_json::Value,
95 #[serde(default)]
96 effect: Option<ToolEffect>,
97 #[serde(default)]
98 idempotent: Option<bool>,
99 #[serde(default)]
100 output_sanitization: OutputSanitization,
101 }
102
103 let raw = RawDef::deserialize(deserializer)?;
104 let effect = match (raw.effect, raw.idempotent) {
105 (Some(eff), _) => eff,
106 (None, Some(true)) => ToolEffect::ReadOnly,
107 (None, Some(false) | None) => ToolEffect::Mutating,
108 };
109
110 Ok(Self {
111 name: raw.name,
112 description: raw.description,
113 parameter_schema: raw.parameter_schema,
114 effect,
115 output_sanitization: raw.output_sanitization,
116 parameter_schema_json: OnceLock::new(),
117 })
118 }
119}
120
121impl ToolDefinition {
122 #[must_use]
125 pub fn new(
126 name: impl Into<String>,
127 description: impl Into<String>,
128 parameter_schema: serde_json::Value,
129 ) -> Self {
130 Self::with_effect(name, description, parameter_schema, ToolEffect::Mutating)
131 }
132
133 pub fn from_schema<T: schemars::JsonSchema>(
139 name: impl Into<String>,
140 description: impl Into<String>,
141 ) -> Result<Self, serde_json::Error> {
142 let parameter_schema = serde_json::to_value(schemars::schema_for!(T))?;
143 Ok(Self::new(name, description, parameter_schema))
144 }
145
146 #[must_use]
148 pub fn with_effect(
149 name: impl Into<String>,
150 description: impl Into<String>,
151 parameter_schema: serde_json::Value,
152 effect: ToolEffect,
153 ) -> Self {
154 Self {
155 name: name.into(),
156 description: description.into(),
157 parameter_schema,
158 effect,
159 output_sanitization: OutputSanitization::default(),
160 parameter_schema_json: OnceLock::new(),
161 }
162 }
163
164 #[must_use]
166 pub const fn with_output_sanitization(mut self, sanitization: OutputSanitization) -> Self {
167 self.output_sanitization = sanitization;
168 self
169 }
170
171 pub fn set_parameter_schema(&mut self, parameter_schema: serde_json::Value) {
173 self.parameter_schema = parameter_schema;
174 self.parameter_schema_json = OnceLock::new();
175 }
176
177 pub fn parameter_schema_mut(&mut self) -> &mut serde_json::Value {
179 self.parameter_schema_json = OnceLock::new();
180 &mut self.parameter_schema
181 }
182
183 #[must_use]
186 pub fn parameter_schema_json(&self) -> &str {
187 self.parameter_schema_json_arc()
188 }
189
190 #[must_use]
193 pub fn parameter_schema_json_arc(&self) -> &Arc<str> {
194 self.parameter_schema_json.get_or_init(|| {
195 match serde_json::to_string(&self.parameter_schema) {
196 Ok(s) => Arc::from(s),
197 Err(err) => {
198 tracing::warn!(error = %err, tool = %self.name, "Failed to serialize parameter_schema to JSON");
199 Arc::from("{}")
200 }
201 }
202 })
203 }
204
205 #[must_use]
209 pub const fn is_idempotent(&self) -> bool {
210 matches!(self.effect, ToolEffect::ReadOnly)
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[derive(schemars::JsonSchema, serde::Deserialize)]
219 struct LookupParams {
220 path: String,
221 max_lines: u32,
222 }
223
224 #[test]
225 fn from_schema_derives_typed_parameter_schema() {
226 let def = ToolDefinition::from_schema::<LookupParams>("lookup", "Look up a file")
227 .expect("schema serializes");
228 assert_eq!(def.name, "lookup");
229 assert_eq!(def.description, "Look up a file");
230 let props = def.parameter_schema["properties"]
231 .as_object()
232 .expect("object schema has properties");
233 assert!(props.contains_key("path"));
234 assert!(props.contains_key("max_lines"));
235 assert!(def.parameter_schema_json().contains("\"max_lines\""));
236
237 let parsed: LookupParams =
238 serde_json::from_str(r#"{"path":"src/lib.rs","max_lines":40}"#).expect("valid args");
239 assert_eq!(parsed.path, "src/lib.rs");
240 assert_eq!(parsed.max_lines, 40);
241 }
242}