Skip to main content

tools_kit/
execution.rs

1//! Invocation inputs, resumable transitions, and completed tool outcomes.
2//!
3//! Invoke one unique handler, resolve its exact pending interaction, or cancel it.
4//! An accepted model-visible failure is [`ToolResult::is_error`](crate::execution::ToolResult::is_error), while [`ToolError`]
5//! reports an operation failure. The caller retains call/name correlation.
6//!
7//! # Accepted tool failure versus an operation error
8//!
9//! An unavailable application record can be a completed model-visible error. Invalid
10//! arguments never reach the callback. Neither case supplies an invented successful
11//! answer, and the caller retains invocation correlation outside `ToolResult`.
12//!
13//! ```
14//! use tools_kit::{Toolset, ToolsetBackend, ToolHandlerBackend, ToolError};
15//! use tools_kit::declaration::{ToolInfo, ToolName};
16//! use tools_kit::execution::{ToolContent, ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult};
17//! use tools_kit::local::ToolRegistration;
18//! use tools_kit::schema::JsonSchema;
19//! # #[tokio::main(flavor = "current_thread")]
20//! # async fn main() -> Result<(), ToolError> {
21//! let name = ToolName::new("record")?;
22//! let info = ToolInfo::new(name.clone(), JsonSchema::strict(serde_json::json!({
23//!     "type":"object", "required":["key"], "properties":{"key":{"type":"string"}}, "additionalProperties":false
24//! })));
25//! let tools = Toolset::local(vec![ToolRegistration::function(info, |input| async move {
26//!     // The application accepts the key but cannot find its record.
27//!     Ok(ToolResult::text(format!("No record for {}", input.arguments["key"])).with_error(true))
28//! })])?;
29//! let work = async {
30//!     let mut handler = tools.tool_handler(name.clone()).await?;
31//!     let output = handler.step(ToolHandlerInput::Invoke(ToolInvocation {
32//!         call_id: Some("owned-call".into()), arguments: serde_json::json!({"key":"missing"}),
33//!     })).await?;
34//!     let ToolHandlerOutput::Completed(result) = output else { panic!("terminal result expected") };
35//!     assert!(result.is_error);
36//!     assert!(matches!(result.content.as_slice(), [ToolContent::Text { text, .. }] if text.contains("missing")));
37//!     let mut invalid = tools.tool_handler(name).await?;
38//!     let rejected = invalid.step(ToolHandlerInput::Invoke(ToolInvocation {
39//!         call_id: None, arguments: serde_json::json!({}),
40//!     })).await;
41//!     assert!(matches!(rejected, Err(ToolError::InvalidArguments { .. })));
42//!     Ok::<(), ToolError>(())
43//! }.await;
44//! let cleanup = tools.shutdown().await;
45//! work?;
46//! cleanup?;
47//! # Ok(())
48//! # }
49//! ```
50
51use std::collections::BTreeMap;
52
53use serde::{Deserialize, Serialize};
54
55use crate::{
56    ToolError,
57    interaction::{ToolInteractionRequest, ToolInteractionResponse},
58};
59
60/// Invocation input for one tool handler.
61///
62/// # Examples
63///
64/// ```
65/// use tools_kit::execution::ToolInvocation;
66/// let input = ToolInvocation { call_id: Some("call-1".into()), arguments: serde_json::json!({"city":"Chicago"}) };
67/// assert_eq!(input.arguments["city"], "Chicago");
68/// ```
69///
70#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71#[serde(deny_unknown_fields)]
72pub struct ToolInvocation {
73    /// Opaque caller-owned correlation id.
74    pub call_id: Option<String>,
75    /// JSON arguments supplied by the caller.
76    pub arguments: serde_json::Value,
77}
78
79/// Source-neutral content returned by a tool.
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
82pub enum ToolContent {
83    /// Human-readable text content.
84    Text {
85        /// Returned text.
86        text: String,
87        /// Optional source-neutral metadata for this content item.
88        metadata: Option<serde_json::Value>,
89    },
90    /// Structured JSON content.
91    Json {
92        /// Returned JSON value.
93        value: serde_json::Value,
94        /// Optional source-neutral metadata for this content item.
95        metadata: Option<serde_json::Value>,
96    },
97    /// Base64-encoded image content.
98    Image {
99        /// Media type of the encoded image.
100        mime_type: String,
101        /// Base64-encoded image bytes.
102        data_base64: String,
103        /// Optional source-neutral metadata for this content item.
104        metadata: Option<serde_json::Value>,
105    },
106    /// Base64-encoded audio content.
107    Audio {
108        /// Media type of the encoded audio.
109        mime_type: String,
110        /// Base64-encoded audio bytes.
111        data_base64: String,
112        /// Optional source-neutral metadata for this content item.
113        metadata: Option<serde_json::Value>,
114    },
115    /// Link to a resource that the caller may retrieve.
116    ResourceLink {
117        /// Resource URI.
118        uri: String,
119        /// Optional display title.
120        title: Option<String>,
121        /// Source-neutral resource metadata.
122        metadata: serde_json::Value,
123    },
124    /// Resource contents embedded directly in the result.
125    EmbeddedResource {
126        /// Resource URI.
127        uri: String,
128        /// Source-defined resource contents.
129        contents: serde_json::Value,
130        /// Source-neutral resource metadata.
131        metadata: serde_json::Value,
132    },
133}
134
135/// Source-neutral result for a completed tool call.
136/// Successful output must include structured content when the declaration has an
137/// output schema. `Some(Value::Null)` is present content and is validated normally;
138/// `None` is missing. An error result remains a completed model-visible outcome.
139///
140/// # Examples
141///
142/// ```
143/// use tools_kit::execution::ToolResult;
144/// let result = ToolResult::json(serde_json::json!({"count":2}));
145/// assert_eq!(result.structured_content, Some(serde_json::json!({"count":2})));
146/// assert!(!result.is_error);
147/// ```
148///
149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150#[serde(deny_unknown_fields)]
151pub struct ToolResult {
152    /// Ordered model-visible content items.
153    pub content: Vec<ToolContent>,
154    /// Optional structured value validated against the tool output schema.
155    pub structured_content: Option<serde_json::Value>,
156    /// Whether this completed result represents a model-visible tool failure.
157    pub is_error: bool,
158    /// Uninterpreted source-neutral result metadata.
159    pub metadata: BTreeMap<String, serde_json::Value>,
160}
161
162impl ToolResult {
163    /// Creates a completed result.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`ToolError::InvalidOutput`] if `content` is empty. Output-schema validation occurs when the owning handler completes.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use tools_kit::execution::{ToolContent, ToolResult};
173    /// let result = ToolResult::new(vec![ToolContent::Text { text: "done".into(), metadata: None }])?;
174    /// assert_eq!(result.content.len(), 1);
175    /// assert!(ToolResult::new(Vec::new()).is_err());
176    ///
177    /// # Ok::<(), tools_kit::ToolError>(())
178    /// ```
179    ///
180    pub fn new(content: Vec<ToolContent>) -> Result<Self, ToolError> {
181        if content.is_empty() {
182            return Err(ToolError::InvalidOutput {
183                message: "tool result content must not be empty".to_owned(),
184                issues: Vec::new(),
185            });
186        }
187        Ok(Self {
188            content,
189            structured_content: None,
190            is_error: false,
191            metadata: BTreeMap::new(),
192        })
193    }
194
195    /// Creates a text result.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// let result = tools_kit::execution::ToolResult::text("done");
201    /// assert!(!result.is_error);
202    /// assert!(result.structured_content.is_none());
203    /// ```
204    ///
205    #[must_use]
206    pub fn text(text: impl Into<String>) -> Self {
207        Self {
208            content: vec![ToolContent::Text {
209                text: text.into(),
210                metadata: None,
211            }],
212            structured_content: None,
213            is_error: false,
214            metadata: BTreeMap::new(),
215        }
216    }
217
218    /// Creates a structured JSON result.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use tools_kit::execution::ToolResult;
224    /// let result = ToolResult::json(serde_json::json!({"count":2}));
225    /// assert_eq!(result.structured_content, Some(serde_json::json!({"count":2})));
226    /// assert!(!result.is_error);
227    /// ```
228    ///
229    #[must_use]
230    pub fn json(value: serde_json::Value) -> Self {
231        Self {
232            content: vec![ToolContent::Json {
233                value: value.clone(),
234                metadata: None,
235            }],
236            structured_content: Some(value),
237            is_error: false,
238            metadata: BTreeMap::new(),
239        }
240    }
241
242    /// Marks this completed result as model-visible failure.
243    /// Use a completed error result for an accepted tool-authored failure. Outer
244    /// [`ToolError`] values describe input, state, protocol, transport, or lifecycle failures.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// let result = tools_kit::execution::ToolResult::text("record unavailable").with_error(true);
250    /// assert!(result.is_error);
251    /// ```
252    ///
253    #[must_use]
254    pub fn with_error(mut self, is_error: bool) -> Self {
255        self.is_error = is_error;
256        self
257    }
258
259    /// Sets structured output.
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// let result = tools_kit::execution::ToolResult::text("no value")
265    ///     .with_structured_content(serde_json::Value::Null);
266    /// assert_eq!(result.structured_content, Some(serde_json::Value::Null));
267    /// ```
268    ///
269    #[must_use]
270    pub fn with_structured_content(mut self, value: serde_json::Value) -> Self {
271        self.structured_content = Some(value);
272        self
273    }
274}
275
276/// Input to one tool-handler transition.
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
279pub enum ToolHandlerInput {
280    /// Starts the invocation with caller-supplied arguments.
281    Invoke(ToolInvocation),
282    /// Resolves the handler's pending caller interaction.
283    Interaction(ToolInteractionResponse),
284    /// Cancels the invocation.
285    Cancel {
286        /// Optional caller-safe cancellation reason.
287        reason: Option<String>,
288    },
289}
290
291/// Output from one tool-handler transition.
292#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
293#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
294pub enum ToolHandlerOutput {
295    /// The invocation reached a terminal result.
296    Completed(ToolResult),
297    /// The invocation is suspended pending a caller response.
298    Interaction(ToolInteractionRequest),
299}