tools_kit/interaction.rs
1//! Caller input requested by a suspended tool invocation.
2//!
3//! Declare capabilities before execution. Preserve the pending request id in
4//! [`ToolInteractionResponse`](crate::interaction::ToolInteractionResponse); text replies are strings and form replies are objects.
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::{ToolError, name::ToolName, schema::JsonSchema};
10
11/// Interaction behavior declared by one tool.
12///
13/// # Examples
14///
15/// ```
16/// use tools_kit::interaction::ToolInteractionCapabilities;
17/// let capabilities = ToolInteractionCapabilities { text: true, ..Default::default() };
18/// assert!(capabilities.text);
19/// assert!(!capabilities.form);
20/// ```
21///
22#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct ToolInteractionCapabilities {
25 /// Whether the tool may request structured form input.
26 pub form: bool,
27 /// Whether form responses may be validated against the supplied schema.
28 pub form_schema_validation: bool,
29 /// Whether the tool may request single-line text input.
30 pub text: bool,
31 /// Whether the tool may request multiline text input.
32 pub multiline_text: bool,
33}
34
35/// Opaque interaction id.
36/// Keep the returned identity unchanged when constructing the response. Its spelling
37/// is opaque; do not derive invocation or tool identity from it.
38///
39/// # Examples
40///
41/// ```
42/// use tools_kit::interaction::ToolInteractionId;
43/// let id = ToolInteractionId::new();
44/// let encoded = serde_json::to_value(&id).unwrap();
45/// assert_eq!(encoded.as_str(), Some(id.as_str()));
46/// ```
47///
48#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
49#[serde(transparent)]
50pub struct ToolInteractionId(String);
51
52impl ToolInteractionId {
53 /// Creates a fresh opaque interaction id.
54 ///
55 /// See [`crate::interaction::ToolInteractionId`] for a complete example.
56 ///
57 #[must_use]
58 pub fn new() -> Self {
59 Self(Uuid::new_v4().to_string())
60 }
61
62 /// Borrows the serialized interaction id.
63 ///
64 /// See [`crate::interaction::ToolInteractionId`] for a complete example.
65 ///
66 #[must_use]
67 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70}
71
72impl Default for ToolInteractionId {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78/// Source-neutral interaction request produced by a tool handler.
79///
80/// # Examples
81///
82/// ```
83/// use tools_kit::{
84/// declaration::ToolName,
85/// interaction::{
86/// ToolInteractionId, ToolInteractionKind, ToolInteractionRequest, ToolInteractionResponse,
87/// },
88/// };
89/// let request = ToolInteractionRequest {
90/// id: ToolInteractionId::new(), tool_name: ToolName::new("ask")?,
91/// call_id: Some("call-1".into()), prompt: "Which city?".into(),
92/// kind: ToolInteractionKind::Text { multiline: false },
93/// };
94/// let response = ToolInteractionResponse::submit(request.id.clone(), serde_json::json!("Chicago"));
95/// assert_eq!(response.interaction_id(), &request.id);
96///
97/// # Ok::<(), tools_kit::ToolError>(())
98/// ```
99///
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct ToolInteractionRequest {
103 /// Opaque id used to correlate the caller response.
104 pub id: ToolInteractionId,
105 /// Tool that requested the interaction.
106 pub tool_name: ToolName,
107 /// Optional caller-owned invocation correlation id.
108 pub call_id: Option<String>,
109 /// Human-readable prompt shown to the caller.
110 pub prompt: String,
111 /// Input shape requested from the caller.
112 pub kind: ToolInteractionKind,
113}
114
115impl ToolInteractionRequest {
116 pub(crate) fn validate_response(
117 &self,
118 response: &ToolInteractionResponse,
119 ) -> Result<(), ToolError> {
120 if response.interaction_id() != &self.id {
121 return Err(ToolError::InvalidInteraction {
122 id: response.interaction_id().as_str().to_owned(),
123 message: format!(
124 "response id does not match pending interaction `{}`",
125 self.id.as_str()
126 ),
127 });
128 }
129
130 match (response, &self.kind) {
131 (
132 ToolInteractionResponse::Submit { content, .. },
133 ToolInteractionKind::Form {
134 schema,
135 schema_validation: true,
136 },
137 ) => {
138 if !content.is_object() {
139 return Err(ToolError::InvalidInteraction {
140 id: self.id.as_str().to_owned(),
141 message: "form responses must be JSON objects".to_owned(),
142 });
143 }
144 let issues = JsonSchema::validate_raw(schema, content)?;
145 if issues.is_empty() {
146 Ok(())
147 } else {
148 Err(ToolError::InvalidArguments {
149 tool: self.tool_name.clone(),
150 message: "form response failed schema validation".to_owned(),
151 issues,
152 })
153 }
154 }
155 (
156 ToolInteractionResponse::Submit { content, .. },
157 ToolInteractionKind::Form {
158 schema_validation: false,
159 ..
160 },
161 ) if content.is_object() => Ok(()),
162 (ToolInteractionResponse::Submit { content, .. }, ToolInteractionKind::Text { .. })
163 if content.is_string() =>
164 {
165 Ok(())
166 }
167 (ToolInteractionResponse::Submit { .. }, _) => Err(ToolError::InvalidInteraction {
168 id: self.id.as_str().to_owned(),
169 message: "interaction response content does not match request kind".to_owned(),
170 }),
171 (
172 ToolInteractionResponse::Decline { .. } | ToolInteractionResponse::Cancel { .. },
173 _,
174 ) => Ok(()),
175 }
176 }
177}
178
179/// Source-neutral interaction request kind.
180#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
181#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
182pub enum ToolInteractionKind {
183 /// Requests a JSON object described by a JSON Schema.
184 Form {
185 /// JSON Schema describing the expected object.
186 schema: serde_json::Value,
187 /// Whether the response must be validated against `schema`.
188 schema_validation: bool,
189 },
190 /// Requests textual input.
191 Text {
192 /// Whether the response may contain multiple lines.
193 multiline: bool,
194 },
195}
196
197/// Caller response to a pending tool interaction.
198#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
199#[serde(tag = "decision", rename_all = "snake_case", deny_unknown_fields)]
200pub enum ToolInteractionResponse {
201 /// Supplies caller input and resumes execution.
202 Submit {
203 /// Id of the interaction being resolved.
204 id: ToolInteractionId,
205 /// Submitted text or form value.
206 content: serde_json::Value,
207 },
208 /// Declines to provide the requested input.
209 Decline {
210 /// Id of the interaction being resolved.
211 id: ToolInteractionId,
212 /// Optional caller-safe explanation.
213 reason: Option<String>,
214 },
215 /// Cancels the pending interaction and invocation.
216 Cancel {
217 /// Id of the interaction being resolved.
218 id: ToolInteractionId,
219 /// Optional caller-safe cancellation reason.
220 reason: Option<String>,
221 },
222}
223
224pub(crate) enum InteractionResolution {
225 Submit(serde_json::Value),
226 Decline(Option<String>),
227 Cancel(Option<String>),
228}
229
230impl ToolInteractionResponse {
231 /// Creates a submitted interaction response.
232 ///
233 /// # Examples
234 ///
235 /// ```
236 /// use tools_kit::interaction::{ToolInteractionId, ToolInteractionResponse};
237 /// let id = ToolInteractionId::new();
238 /// let response = ToolInteractionResponse::submit(id.clone(), serde_json::json!("Chicago"));
239 /// assert_eq!(response.interaction_id(), &id);
240 /// ```
241 ///
242 #[must_use]
243 pub fn submit(id: ToolInteractionId, content: serde_json::Value) -> Self {
244 Self::Submit { id, content }
245 }
246
247 /// Creates a declined interaction response.
248 ///
249 /// # Examples
250 ///
251 /// ```
252 /// use tools_kit::interaction::{ToolInteractionId, ToolInteractionResponse};
253 /// let id = ToolInteractionId::new();
254 /// let response = ToolInteractionResponse::decline(id.clone(), Some("not available".into()));
255 /// assert_eq!(response.interaction_id(), &id);
256 /// ```
257 ///
258 #[must_use]
259 pub fn decline(id: ToolInteractionId, reason: Option<String>) -> Self {
260 Self::Decline { id, reason }
261 }
262
263 /// Creates a cancelled interaction response.
264 ///
265 /// # Examples
266 ///
267 /// ```
268 /// use tools_kit::interaction::{ToolInteractionId, ToolInteractionResponse};
269 /// let id = ToolInteractionId::new();
270 /// let response = ToolInteractionResponse::cancel(id.clone(), None);
271 /// assert_eq!(response.interaction_id(), &id);
272 /// ```
273 ///
274 #[must_use]
275 pub fn cancel(id: ToolInteractionId, reason: Option<String>) -> Self {
276 Self::Cancel { id, reason }
277 }
278
279 /// Borrows the id of the interaction being resolved.
280 ///
281 /// # Examples
282 ///
283 /// ```
284 /// use tools_kit::interaction::{ToolInteractionId, ToolInteractionResponse};
285 /// let id = ToolInteractionId::new();
286 /// let response = ToolInteractionResponse::submit(id.clone(), serde_json::json!({"approved":true}));
287 /// assert_eq!(response.interaction_id(), &id);
288 /// ```
289 ///
290 #[must_use]
291 pub fn interaction_id(&self) -> &ToolInteractionId {
292 match self {
293 Self::Submit { id, .. } | Self::Decline { id, .. } | Self::Cancel { id, .. } => id,
294 }
295 }
296
297 pub(crate) fn into_resolution(self) -> InteractionResolution {
298 match self {
299 Self::Submit { content, .. } => InteractionResolution::Submit(content),
300 Self::Decline { reason, .. } => InteractionResolution::Decline(reason),
301 Self::Cancel { reason, .. } => InteractionResolution::Cancel(reason),
302 }
303 }
304}