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}