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