Skip to main content

tandem_types/
tool.rs

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}