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}