1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum ToolEffect {
7 Read,
8 Write,
9 Delete,
10 Search,
11 Execute,
12 Fetch,
13 Patch,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ToolDomain {
19 Workspace,
20 Web,
21 Shell,
22 Browser,
23 Planning,
24 Memory,
25 Collaboration,
26 Integration,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
30pub struct ToolCapabilities {
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub effects: Vec<ToolEffect>,
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub domains: Vec<ToolDomain>,
35 #[serde(default, skip_serializing_if = "is_false")]
36 pub reads_workspace: bool,
37 #[serde(default, skip_serializing_if = "is_false")]
38 pub writes_workspace: bool,
39 #[serde(default, skip_serializing_if = "is_false")]
40 pub network_access: bool,
41 #[serde(default, skip_serializing_if = "is_false")]
42 pub destructive: bool,
43 #[serde(default, skip_serializing_if = "is_false")]
44 pub requires_verification: bool,
45 #[serde(default, skip_serializing_if = "is_false")]
46 pub preferred_for_discovery: bool,
47 #[serde(default, skip_serializing_if = "is_false")]
48 pub preferred_for_validation: bool,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct ToolSchema {
53 pub name: String,
54 pub description: String,
55 pub input_schema: Value,
56 #[serde(default, skip_serializing_if = "ToolCapabilities::is_empty")]
57 pub capabilities: ToolCapabilities,
58}
59
60fn is_false(value: &bool) -> bool {
61 !*value
62}
63
64impl ToolCapabilities {
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn effect(mut self, effect: ToolEffect) -> Self {
70 if !self.effects.contains(&effect) {
71 self.effects.push(effect);
72 }
73 self
74 }
75
76 pub fn domain(mut self, domain: ToolDomain) -> Self {
77 if !self.domains.contains(&domain) {
78 self.domains.push(domain);
79 }
80 self
81 }
82
83 pub fn reads_workspace(mut self) -> Self {
84 self.reads_workspace = true;
85 self
86 }
87
88 pub fn writes_workspace(mut self) -> Self {
89 self.writes_workspace = true;
90 self
91 }
92
93 pub fn network_access(mut self) -> Self {
94 self.network_access = true;
95 self
96 }
97
98 pub fn destructive(mut self) -> Self {
99 self.destructive = true;
100 self
101 }
102
103 pub fn requires_verification(mut self) -> Self {
104 self.requires_verification = true;
105 self
106 }
107
108 pub fn preferred_for_discovery(mut self) -> Self {
109 self.preferred_for_discovery = true;
110 self
111 }
112
113 pub fn preferred_for_validation(mut self) -> Self {
114 self.preferred_for_validation = true;
115 self
116 }
117
118 pub fn is_empty(&self) -> bool {
119 self.effects.is_empty()
120 && self.domains.is_empty()
121 && !self.reads_workspace
122 && !self.writes_workspace
123 && !self.network_access
124 && !self.destructive
125 && !self.requires_verification
126 && !self.preferred_for_discovery
127 && !self.preferred_for_validation
128 }
129}
130
131impl ToolSchema {
132 pub fn new(
133 name: impl Into<String>,
134 description: impl Into<String>,
135 input_schema: Value,
136 ) -> Self {
137 Self {
138 name: name.into(),
139 description: description.into(),
140 input_schema,
141 capabilities: ToolCapabilities::default(),
142 }
143 }
144
145 pub fn with_capabilities(mut self, capabilities: ToolCapabilities) -> Self {
146 self.capabilities = capabilities;
147 self
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ToolResult {
153 pub output: String,
154 #[serde(default)]
155 pub metadata: Value,
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn tool_schema_deserializes_legacy_payload_without_capabilities() {
164 let actual: ToolSchema = serde_json::from_value(serde_json::json!({
165 "name": "read",
166 "description": "Read file contents",
167 "input_schema": {
168 "type": "object"
169 }
170 }))
171 .unwrap();
172
173 let expected = ToolSchema::new(
174 "read",
175 "Read file contents",
176 serde_json::json!({
177 "type": "object"
178 }),
179 );
180
181 assert_eq!(actual, expected);
182 }
183
184 #[test]
185 fn tool_schema_serialization_omits_empty_capabilities() {
186 let actual = serde_json::to_value(ToolSchema::new(
187 "read",
188 "Read file contents",
189 serde_json::json!({
190 "type": "object"
191 }),
192 ))
193 .unwrap();
194
195 let expected = serde_json::json!({
196 "name": "read",
197 "description": "Read file contents",
198 "input_schema": {
199 "type": "object"
200 }
201 });
202
203 assert_eq!(actual, expected);
204 }
205
206 #[test]
207 fn tool_schema_round_trips_capabilities() {
208 let actual: ToolSchema = serde_json::from_value(serde_json::json!({
209 "name": "write",
210 "description": "Write file contents",
211 "input_schema": {
212 "type": "object"
213 },
214 "capabilities": {
215 "effects": ["write"],
216 "domains": ["workspace"],
217 "writes_workspace": true,
218 "requires_verification": true
219 }
220 }))
221 .unwrap();
222
223 let expected = ToolSchema::new(
224 "write",
225 "Write file contents",
226 serde_json::json!({
227 "type": "object"
228 }),
229 )
230 .with_capabilities(
231 ToolCapabilities::new()
232 .effect(ToolEffect::Write)
233 .domain(ToolDomain::Workspace)
234 .writes_workspace()
235 .requires_verification(),
236 );
237
238 assert_eq!(actual, expected);
239 }
240}