1use std::collections::BTreeMap;
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum ToolKind {
30 Read,
32 Edit,
34 Delete,
36 Move,
38 Search,
40 Execute,
42 Think,
44 Fetch,
46 #[default]
49 Other,
50}
51
52impl ToolKind {
53 pub const ALL: [Self; 9] = [
54 Self::Read,
55 Self::Edit,
56 Self::Delete,
57 Self::Move,
58 Self::Search,
59 Self::Execute,
60 Self::Think,
61 Self::Fetch,
62 Self::Other,
63 ];
64
65 pub fn is_read_only(&self) -> bool {
69 matches!(self, Self::Read | Self::Search | Self::Think | Self::Fetch)
70 }
71
72 pub fn mutation_class(&self) -> &'static str {
76 match self {
77 Self::Read | Self::Search | Self::Think | Self::Fetch => "read_only",
78 Self::Edit => "workspace_write",
79 Self::Delete | Self::Move => "destructive",
80 Self::Execute => "ambient_side_effect",
81 Self::Other => "other",
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum SideEffectLevel {
90 #[default]
93 None,
94 ReadOnly,
96 WorkspaceWrite,
98 ProcessExec,
100 Network,
102}
103
104impl SideEffectLevel {
105 pub const ALL: [Self; 5] = [
106 Self::None,
107 Self::ReadOnly,
108 Self::WorkspaceWrite,
109 Self::ProcessExec,
110 Self::Network,
111 ];
112
113 pub fn rank(&self) -> usize {
116 match self {
117 Self::None => 0,
118 Self::ReadOnly => 1,
119 Self::WorkspaceWrite => 2,
120 Self::ProcessExec => 3,
121 Self::Network => 4,
122 }
123 }
124
125 pub fn as_str(&self) -> &'static str {
128 match self {
129 Self::None => "none",
130 Self::ReadOnly => "read_only",
131 Self::WorkspaceWrite => "workspace_write",
132 Self::ProcessExec => "process_exec",
133 Self::Network => "network",
134 }
135 }
136
137 pub fn parse(value: &str) -> Self {
140 match value {
141 "none" => Self::None,
142 "read_only" => Self::ReadOnly,
143 "workspace_write" => Self::WorkspaceWrite,
144 "process_exec" => Self::ProcessExec,
145 "network" => Self::Network,
146 _ => Self::None,
147 }
148 }
149}
150
151#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
160#[serde(default)]
161pub struct ToolArgSchema {
162 pub path_params: Vec<String>,
165 pub arg_aliases: BTreeMap<String, String>,
169 pub required: Vec<String>,
171}
172
173#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
177#[serde(default)]
178pub struct ToolAnnotations {
179 pub kind: ToolKind,
181 pub side_effect_level: SideEffectLevel,
183 pub arg_schema: ToolArgSchema,
185 pub capabilities: BTreeMap<String, Vec<String>>,
188 pub emits_artifacts: bool,
192 pub result_readers: Vec<String>,
194 pub inline_result: bool,
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn tool_kind_serde_roundtrip() {
205 for (kind, expected) in [
206 (ToolKind::Read, "\"read\""),
207 (ToolKind::Edit, "\"edit\""),
208 (ToolKind::Delete, "\"delete\""),
209 (ToolKind::Move, "\"move\""),
210 (ToolKind::Search, "\"search\""),
211 (ToolKind::Execute, "\"execute\""),
212 (ToolKind::Think, "\"think\""),
213 (ToolKind::Fetch, "\"fetch\""),
214 (ToolKind::Other, "\"other\""),
215 ] {
216 let encoded = serde_json::to_string(&kind).unwrap();
217 assert_eq!(encoded, expected);
218 let decoded: ToolKind = serde_json::from_str(expected).unwrap();
219 assert_eq!(decoded, kind);
220 }
221 }
222
223 #[test]
224 fn only_read_search_think_fetch_are_read_only() {
225 assert!(ToolKind::Read.is_read_only());
226 assert!(ToolKind::Search.is_read_only());
227 assert!(ToolKind::Think.is_read_only());
228 assert!(ToolKind::Fetch.is_read_only());
229 assert!(!ToolKind::Other.is_read_only());
231 assert!(!ToolKind::Edit.is_read_only());
232 assert!(!ToolKind::Delete.is_read_only());
233 assert!(!ToolKind::Move.is_read_only());
234 assert!(!ToolKind::Execute.is_read_only());
235 }
236
237 #[test]
238 fn mutation_class_derived_from_kind() {
239 assert_eq!(ToolKind::Read.mutation_class(), "read_only");
240 assert_eq!(ToolKind::Search.mutation_class(), "read_only");
241 assert_eq!(ToolKind::Edit.mutation_class(), "workspace_write");
242 assert_eq!(ToolKind::Delete.mutation_class(), "destructive");
243 assert_eq!(ToolKind::Move.mutation_class(), "destructive");
244 assert_eq!(ToolKind::Execute.mutation_class(), "ambient_side_effect");
245 assert_eq!(ToolKind::Other.mutation_class(), "other");
246 }
247
248 #[test]
249 fn side_effect_level_round_trip() {
250 for level in [
251 SideEffectLevel::None,
252 SideEffectLevel::ReadOnly,
253 SideEffectLevel::WorkspaceWrite,
254 SideEffectLevel::ProcessExec,
255 SideEffectLevel::Network,
256 ] {
257 assert_eq!(SideEffectLevel::parse(level.as_str()), level);
258 let encoded = serde_json::to_string(&level).unwrap();
259 let decoded: SideEffectLevel = serde_json::from_str(&encoded).unwrap();
260 assert_eq!(decoded, level);
261 }
262 }
263
264 #[test]
265 fn side_effect_level_rank_orders() {
266 assert!(SideEffectLevel::None.rank() < SideEffectLevel::ReadOnly.rank());
267 assert!(SideEffectLevel::ReadOnly.rank() < SideEffectLevel::WorkspaceWrite.rank());
268 assert!(SideEffectLevel::WorkspaceWrite.rank() < SideEffectLevel::ProcessExec.rank());
269 assert!(SideEffectLevel::ProcessExec.rank() < SideEffectLevel::Network.rank());
270 }
271
272 #[test]
273 fn arg_schema_defaults_empty() {
274 let schema = ToolArgSchema::default();
275 assert!(schema.path_params.is_empty());
276 assert!(schema.arg_aliases.is_empty());
277 assert!(schema.required.is_empty());
278 }
279
280 #[test]
281 fn annotations_default_result_routes_empty() {
282 let annotations = ToolAnnotations::default();
283 assert!(!annotations.emits_artifacts);
284 assert!(annotations.result_readers.is_empty());
285 assert!(!annotations.inline_result);
286 }
287}