tools_kit/local.rs
1//! Local executable registrations and declared caller interactions.
2//!
3//! [`ToolRegistration::function`](crate::local::ToolRegistration::function) accepts normalized invocation values;
4//! [`ToolRegistration::typed`](crate::local::ToolRegistration::typed) derives schemas for typed arguments and results.
5//! Interactive handlers use their [`ToolInvocationContext`](crate::local::ToolInvocationContext) and resume through
6//! the same unique handler. Construct a source with [`crate::Toolset::local`].
7//!
8//! # Caller interaction and cancellation
9//!
10//! The [`crate::local::ToolInvocationContext::request_text`] and
11//! [`crate::local::ToolInvocationContext::request_form`] examples below execute the complete
12//! submit/resume path and inspect the result. Cancellation uses the same unique
13//! handler and exact pending interaction id. It is an outer cancellation outcome,
14//! not a completed model-visible result.
15//!
16//! ```
17//! use tools_kit::{Toolset, ToolsetBackend, ToolHandlerBackend, ToolError};
18//! use tools_kit::declaration::{ToolInfo, ToolName};
19//! use tools_kit::execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult};
20//! use tools_kit::interaction::{ToolInteractionCapabilities, ToolInteractionResponse};
21//! use tools_kit::local::ToolRegistration;
22//! use tools_kit::schema::JsonSchema;
23//! # #[tokio::main(flavor = "current_thread")]
24//! # async fn main() -> Result<(), ToolError> {
25//! let name = ToolName::new("ask")?;
26//! let mut info = ToolInfo::new(
27//! name.clone(),
28//! JsonSchema::strict(serde_json::json!({"type":"object"})),
29//! );
30//! info.interactions = ToolInteractionCapabilities { text: true, ..Default::default() };
31//! let tools = Toolset::local(vec![ToolRegistration::interactive(info, |_input, context| async move {
32//! let answer = context.request_text("Which city?", false).await?;
33//! Ok(ToolResult::text(answer))
34//! })])?;
35//! let work = async {
36//! let mut handler = tools.tool_handler(name.clone()).await?;
37//! let output = handler.step(ToolHandlerInput::Invoke(ToolInvocation {
38//! call_id: Some("city-call".into()), arguments: serde_json::json!({}),
39//! })).await?;
40//! let ToolHandlerOutput::Interaction(request) = output else { panic!("interaction expected") };
41//! assert_eq!(request.tool_name, name);
42//! assert_eq!(request.call_id.as_deref(), Some("city-call"));
43//! let reply = ToolInteractionResponse::cancel(request.id, Some("caller stopped".into()));
44//! let cancelled = handler.step(ToolHandlerInput::Interaction(reply)).await;
45//! assert!(matches!(cancelled, Err(ToolError::Cancelled { .. })));
46//! assert!(handler.step(ToolHandlerInput::Cancel { reason: None }).await.is_err());
47//! Ok::<(), ToolError>(())
48//! }.await;
49//! let cleanup = tools.shutdown().await;
50//! if let Err(error) = &cleanup {
51//! eprintln!("Cleanup failed: {error}");
52//! }
53//! work?;
54//! cleanup?;
55//! # Ok(())
56//! # }
57//! ```
58
59use std::{fmt, future::Future, pin::Pin, sync::Arc};
60
61use schemars::JsonSchema as SchemarsJsonSchema;
62use serde::{Serialize, de::DeserializeOwned};
63use tokio::sync::{mpsc, oneshot};
64
65use crate::{
66 ToolError,
67 declaration::ToolInfo,
68 execution::{ToolInvocation, ToolResult},
69 interaction::{
70 InteractionResolution, ToolInteractionCapabilities, ToolInteractionId, ToolInteractionKind,
71 ToolInteractionRequest, ToolInteractionResponse,
72 },
73 name::ToolName,
74 schema::JsonSchema,
75};
76
77/// One complete local tool declaration plus its private executable handler.
78/// Registration constructs passive declarations and closures; execution begins through
79/// the selected unique handler. Cloning a registration shares its immutable handler.
80///
81/// # Examples
82///
83/// ```
84///
85/// # #[tokio::main(flavor = "current_thread")]
86/// # async fn main() -> Result<(), tools_kit::ToolError> {
87/// use tools_kit::{declaration::{ToolInfo, ToolName}, schema::JsonSchema};
88/// let info = ToolInfo::new(
89/// ToolName::new("ping")?,
90/// JsonSchema::strict(serde_json::json!({"type":"object"})),
91/// );
92/// use tools_kit::{local::ToolRegistration, execution::ToolResult};
93/// let registration = ToolRegistration::function(info, |_| async { Ok(ToolResult::text("pong")) });
94/// use tools_kit::{
95/// ToolHandlerBackend, Toolset, ToolsetBackend,
96/// execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation},
97/// };
98/// let tools = Toolset::local(vec![registration])?;
99/// let result = async {
100/// let mut handler = tools.tool_handler(ToolName::new("ping")?).await?;
101/// handler
102/// .step(ToolHandlerInput::Invoke(ToolInvocation {
103/// call_id: None,
104/// arguments: serde_json::json!({}),
105/// }))
106/// .await
107/// }.await;
108/// let cleanup = tools.shutdown().await;
109/// if let Err(error) = &cleanup {
110/// eprintln!("Cleanup failed: {error}");
111/// }
112/// assert!(matches!(result?, ToolHandlerOutput::Completed(_)));
113/// cleanup?;
114///
115/// # Ok(())
116/// # }
117/// ```
118///
119#[derive(Clone)]
120pub struct ToolRegistration {
121 inner: Arc<RegisteredTool>,
122}
123
124pub(crate) struct RegisteredTool {
125 info: ToolInfo,
126 callback: Arc<dyn RegisteredToolCallback>,
127}
128
129pub(crate) struct RegisteredToolCall {
130 future: Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send>>,
131}
132
133impl RegisteredToolCall {
134 pub(crate) async fn run(self) -> Result<ToolResult, ToolError> {
135 self.future.await
136 }
137}
138
139trait RegisteredToolCallback: Send + Sync {
140 fn start(
141 &self,
142 invocation: ToolInvocation,
143 context: ToolInvocationContext,
144 ) -> RegisteredToolCall;
145}
146
147impl<F, Fut> RegisteredToolCallback for F
148where
149 F: Fn(ToolInvocation, ToolInvocationContext) -> Fut + Send + Sync,
150 Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
151{
152 fn start(
153 &self,
154 invocation: ToolInvocation,
155 context: ToolInvocationContext,
156 ) -> RegisteredToolCall {
157 RegisteredToolCall {
158 future: Box::pin(self(invocation, context)),
159 }
160 }
161}
162
163impl RegisteredTool {
164 pub(crate) fn info(&self) -> &ToolInfo {
165 &self.info
166 }
167
168 pub(crate) fn call(
169 &self,
170 invocation: ToolInvocation,
171 context: ToolInvocationContext,
172 ) -> Result<RegisteredToolCall, ToolError> {
173 let issues = self.info.input_schema.validate(&invocation.arguments)?;
174 if !issues.is_empty() {
175 return Err(ToolError::InvalidArguments {
176 tool: self.info.name.clone(),
177 message: "tool arguments failed schema validation".to_owned(),
178 issues,
179 });
180 }
181 Ok(self.callback.start(invocation, context))
182 }
183}
184
185/// Context available to local tools that declare caller interaction support.
186///
187/// # Examples
188///
189/// ```
190///
191/// # #[tokio::main(flavor = "current_thread")]
192/// # async fn main() -> Result<(), tools_kit::ToolError> {
193/// use tools_kit::{
194/// ToolHandlerBackend, Toolset, ToolsetBackend,
195/// declaration::{ToolInfo, ToolName},
196/// execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult},
197/// interaction::{ToolInteractionCapabilities, ToolInteractionResponse},
198/// local::ToolRegistration,
199/// schema::JsonSchema,
200/// };
201/// let name = ToolName::new("ask")?;
202/// let mut info = ToolInfo::new(
203/// name.clone(),
204/// JsonSchema::strict(serde_json::json!({"type":"object"})),
205/// );
206/// info.interactions = ToolInteractionCapabilities { text: true, ..Default::default() };
207/// let registration = ToolRegistration::interactive(info, |_input, context| async move {
208/// let city = context.request_text("Which city?", false).await?;
209/// Ok(ToolResult::text(city))
210/// });
211/// let tools = Toolset::local(vec![registration])?;
212/// let observed = async {
213/// let mut handler = tools.tool_handler(name).await?;
214/// let pending = handler.step(ToolHandlerInput::Invoke(ToolInvocation { call_id: None, arguments: serde_json::json!({}) })).await?;
215/// let ToolHandlerOutput::Interaction(request) = pending else { panic!("interaction expected") };
216/// let reply = ToolInteractionResponse::submit(request.id, serde_json::json!("Chicago"));
217/// let done = handler.step(ToolHandlerInput::Interaction(reply)).await?;
218/// assert!(matches!(done, ToolHandlerOutput::Completed(_)));
219/// Ok::<(), tools_kit::ToolError>(())
220/// }.await;
221/// let cleanup = tools.shutdown().await;
222/// if let Err(error) = &cleanup {
223/// eprintln!("Cleanup failed: {error}");
224/// }
225/// observed?;
226/// cleanup?;
227///
228/// # Ok(())
229/// # }
230/// ```
231///
232#[derive(Clone)]
233pub struct ToolInvocationContext {
234 tool_name: ToolName,
235 call_id: Option<String>,
236 interactions: mpsc::Sender<LocalInteractionEnvelope>,
237 capabilities: ToolInteractionCapabilities,
238}
239
240pub(crate) struct LocalInteractionEnvelope {
241 request: ToolInteractionRequest,
242 response: oneshot::Sender<ToolInteractionResponse>,
243}
244
245impl LocalInteractionEnvelope {
246 pub(crate) fn into_parts(
247 self,
248 ) -> (
249 ToolInteractionRequest,
250 oneshot::Sender<ToolInteractionResponse>,
251 ) {
252 (self.request, self.response)
253 }
254}
255
256impl ToolRegistration {
257 pub(crate) fn into_registered(self) -> RegisteredTool {
258 Arc::try_unwrap(self.inner).unwrap_or_else(|inner| (*inner).clone())
259 }
260
261 /// Registers a non-interactive local handler.
262 ///
263 /// See [`crate::local::ToolRegistration`] for a complete example.
264 ///
265 #[must_use]
266 pub fn function<F, Fut>(info: ToolInfo, handler: F) -> Self
267 where
268 F: Fn(ToolInvocation) -> Fut + Send + Sync + 'static,
269 Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
270 {
271 let callback = move |invocation, _context| handler(invocation);
272 Self::new(info, callback)
273 }
274
275 /// Registers a handler that may request declared caller interactions.
276 ///
277 /// # Examples
278 ///
279 /// ```
280 ///
281 /// # #[tokio::main(flavor = "current_thread")]
282 /// # async fn main() -> Result<(), tools_kit::ToolError> {
283 /// use tools_kit::{
284 /// ToolHandlerBackend, Toolset, ToolsetBackend,
285 /// declaration::{ToolInfo, ToolName},
286 /// execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult},
287 /// interaction::{ToolInteractionCapabilities, ToolInteractionResponse},
288 /// local::ToolRegistration,
289 /// schema::JsonSchema,
290 /// };
291 /// let name = ToolName::new("ask")?;
292 /// let mut info = ToolInfo::new(
293 /// name.clone(),
294 /// JsonSchema::strict(serde_json::json!({"type":"object"})),
295 /// );
296 /// info.interactions = ToolInteractionCapabilities { text: true, ..Default::default() };
297 /// let registration = ToolRegistration::interactive(info, |_input, context| async move {
298 /// let city = context.request_text("Which city?", false).await?;
299 /// Ok(ToolResult::text(city))
300 /// });
301 /// let tools = Toolset::local(vec![registration])?;
302 /// let observed = async {
303 /// let mut handler = tools.tool_handler(name).await?;
304 /// let pending = handler.step(ToolHandlerInput::Invoke(ToolInvocation { call_id: None, arguments: serde_json::json!({}) })).await?;
305 /// let ToolHandlerOutput::Interaction(request) = pending else { panic!("interaction expected") };
306 /// let reply = ToolInteractionResponse::submit(request.id, serde_json::json!("Chicago"));
307 /// let done = handler.step(ToolHandlerInput::Interaction(reply)).await?;
308 /// let ToolHandlerOutput::Completed(result) = done else { panic!("completion expected") };
309 /// assert!(!result.is_error);
310 /// assert!(matches!(result.content.as_slice(), [tools_kit::execution::ToolContent::Text { text, .. }] if text == "Chicago"));
311 /// Ok::<(), tools_kit::ToolError>(())
312 /// }.await;
313 /// let cleanup = tools.shutdown().await;
314 /// if let Err(error) = &cleanup {
315 /// eprintln!("Cleanup failed: {error}");
316 /// }
317 /// observed?;
318 /// cleanup?;
319 ///
320 /// # Ok(())
321 /// # }
322 /// ```
323 ///
324 #[must_use]
325 pub fn interactive<F, Fut>(info: ToolInfo, handler: F) -> Self
326 where
327 F: Fn(ToolInvocation, ToolInvocationContext) -> Fut + Send + Sync + 'static,
328 Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
329 {
330 let callback = move |invocation, context| handler(invocation, context);
331 Self::new(info, callback)
332 }
333
334 /// Registers a typed local function and derives strict input/output schemas.
335 ///
336 /// # Errors
337 ///
338 /// Returns [`ToolError::InvalidConfiguration`] for an invalid name or [`ToolError::Protocol`] when schema serialization fails. Later invocation validates and deserializes arguments and serializes the returned output.
339 ///
340 /// # Examples
341 ///
342 /// ```
343 ///
344 /// # #[tokio::main(flavor = "current_thread")]
345 /// # async fn main() -> Result<(), tools_kit::ToolError> {
346 /// use tools_kit::{Toolset, ToolsetBackend, ToolHandlerBackend, local::ToolRegistration};
347 /// use tools_kit::declaration::ToolName;
348 /// use tools_kit::execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation};
349 /// let registration = ToolRegistration::typed("double", |value: i64| async move { Ok(value * 2) })?;
350 /// let tools = Toolset::local(vec![registration])?;
351 /// let observed = async {
352 /// assert!(tools.list_tools().await?[0].output_schema.is_some());
353 /// let mut handler = tools.tool_handler(ToolName::new("double")?).await?;
354 /// let output = handler.step(ToolHandlerInput::Invoke(ToolInvocation {
355 /// call_id: Some("double-1".into()), arguments: serde_json::json!(21),
356 /// })).await?;
357 /// let ToolHandlerOutput::Completed(result) = output else { panic!("completion expected") };
358 /// assert!(!result.is_error);
359 /// assert_eq!(result.structured_content, Some(serde_json::json!(42)));
360 /// Ok::<(), tools_kit::ToolError>(())
361 /// }.await;
362 /// let cleanup = tools.shutdown().await;
363 /// if let Err(error) = &cleanup {
364 /// eprintln!("Cleanup failed: {error}");
365 /// }
366 /// observed?;
367 /// cleanup?;
368 ///
369 /// # Ok(())
370 /// # }
371 /// ```
372 ///
373 pub fn typed<N, TArgs, TOutput, F, Fut>(name: N, handler: F) -> Result<Self, ToolError>
374 where
375 N: TryInto<ToolName>,
376 N::Error: fmt::Display,
377 TArgs: DeserializeOwned + SchemarsJsonSchema + Send + 'static,
378 TOutput: Serialize + SchemarsJsonSchema + Send + 'static,
379 F: Fn(TArgs) -> Fut + Send + Sync + 'static,
380 Fut: Future<Output = Result<TOutput, ToolError>> + Send + 'static,
381 {
382 let name = name
383 .try_into()
384 .map_err(|error| ToolError::InvalidConfiguration {
385 message: error.to_string(),
386 })?;
387 let handler_name = name.clone();
388 let handler = Arc::new(handler);
389 let input_schema = Self::schema_document::<TArgs>()?;
390 let output_schema = Self::schema_document::<TOutput>()?;
391 let mut info = ToolInfo::new(name, input_schema);
392 info.output_schema = Some(output_schema);
393 Ok(Self::function(info, move |invocation| {
394 let handler_name = handler_name.clone();
395 let handler = Arc::clone(&handler);
396 async move {
397 let arguments =
398 Self::deserialize_arguments::<TArgs>(&handler_name, invocation.arguments)?;
399 let output = handler(arguments).await?;
400 let value = serde_json::to_value(output).map_err(|error| ToolError::Protocol {
401 message: format!("failed to serialize typed tool output: {error}"),
402 })?;
403 Ok(ToolResult::json(value))
404 }
405 }))
406 }
407
408 fn new<F, Fut>(info: ToolInfo, callback: F) -> Self
409 where
410 F: Fn(ToolInvocation, ToolInvocationContext) -> Fut + Send + Sync + 'static,
411 Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
412 {
413 Self {
414 inner: Arc::new(RegisteredTool {
415 info,
416 callback: Arc::new(callback),
417 }),
418 }
419 }
420
421 /// Replaces the description on this registration.
422 ///
423 /// # Examples
424 ///
425 /// ```
426 ///
427 /// # #[tokio::main(flavor = "current_thread")]
428 /// # async fn main() -> Result<(), tools_kit::ToolError> {
429 /// use tools_kit::{declaration::{ToolInfo, ToolName}, schema::JsonSchema};
430 /// let info = ToolInfo::new(
431 /// ToolName::new("ping")?,
432 /// JsonSchema::strict(serde_json::json!({"type":"object"})),
433 /// );
434 /// use tools_kit::{local::ToolRegistration, execution::ToolResult};
435 /// let registration = ToolRegistration::function(info, |_| async { Ok(ToolResult::text("pong")) });
436 /// use tools_kit::{Toolset, ToolsetBackend};
437 /// let tools = Toolset::local(vec![registration.description("Responds with pong")])?;
438 /// let inventory = tools.list_tools().await;
439 /// let cleanup = tools.shutdown().await;
440 /// if let Err(error) = &cleanup {
441 /// eprintln!("Cleanup failed: {error}");
442 /// }
443 /// assert_eq!(inventory?[0].description.as_deref(), Some("Responds with pong"));
444 /// cleanup?;
445 ///
446 /// # Ok(())
447 /// # }
448 /// ```
449 ///
450 #[must_use]
451 pub fn description(self, description: impl Into<String>) -> Self {
452 self.map_info(|info| info.description = Some(description.into()))
453 }
454
455 /// Replaces the interaction declaration on this registration.
456 ///
457 /// # Examples
458 ///
459 /// ```
460 ///
461 /// # #[tokio::main(flavor = "current_thread")]
462 /// # async fn main() -> Result<(), tools_kit::ToolError> {
463 /// use tools_kit::{declaration::{ToolInfo, ToolName}, schema::JsonSchema};
464 /// let info = ToolInfo::new(
465 /// ToolName::new("ping")?,
466 /// JsonSchema::strict(serde_json::json!({"type":"object"})),
467 /// );
468 /// use tools_kit::{local::ToolRegistration, execution::ToolResult};
469 /// let registration = ToolRegistration::function(info, |_| async { Ok(ToolResult::text("pong")) });
470 /// use tools_kit::{Toolset, ToolsetBackend, interaction::ToolInteractionCapabilities};
471 /// let registration = registration.interactions(ToolInteractionCapabilities::default());
472 /// let tools = Toolset::local(vec![registration])?;
473 /// let inventory = tools.list_tools().await;
474 /// let cleanup = tools.shutdown().await;
475 /// if let Err(error) = &cleanup {
476 /// eprintln!("Cleanup failed: {error}");
477 /// }
478 /// assert!(!inventory?[0].interactions.text);
479 /// cleanup?;
480 ///
481 /// # Ok(())
482 /// # }
483 /// ```
484 ///
485 #[must_use]
486 pub fn interactions(self, interactions: ToolInteractionCapabilities) -> Self {
487 self.map_info(|info| info.interactions = interactions)
488 }
489
490 fn map_info(self, update: impl FnOnce(&mut ToolInfo)) -> Self {
491 let mut inner = Arc::try_unwrap(self.inner).unwrap_or_else(|inner| (*inner).clone());
492 update(&mut inner.info);
493 Self {
494 inner: Arc::new(inner),
495 }
496 }
497
498 fn schema_document<T: SchemarsJsonSchema>() -> Result<JsonSchema, ToolError> {
499 let schema = serde_json::to_value(schemars::schema_for!(T)).map_err(|error| {
500 ToolError::Protocol {
501 message: format!("failed to serialize generated JSON Schema: {error}"),
502 }
503 })?;
504 Ok(JsonSchema::strict(schema))
505 }
506
507 fn deserialize_arguments<TArgs>(
508 tool: &ToolName,
509 arguments: serde_json::Value,
510 ) -> Result<TArgs, ToolError>
511 where
512 TArgs: DeserializeOwned,
513 {
514 let serialized = arguments.to_string();
515 let mut deserializer = serde_json::Deserializer::from_str(&serialized);
516 serde_path_to_error::deserialize(&mut deserializer).map_err(|error| {
517 ToolError::InvalidArguments {
518 tool: tool.clone(),
519 message: error.to_string(),
520 issues: Vec::new(),
521 }
522 })
523 }
524}
525
526impl Clone for RegisteredTool {
527 fn clone(&self) -> Self {
528 Self {
529 info: self.info.clone(),
530 callback: Arc::clone(&self.callback),
531 }
532 }
533}
534
535impl fmt::Debug for RegisteredTool {
536 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
537 formatter
538 .debug_struct("RegisteredTool")
539 .field("info", &self.info)
540 .finish_non_exhaustive()
541 }
542}
543
544impl fmt::Debug for ToolRegistration {
545 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
546 formatter
547 .debug_struct("ToolRegistration")
548 .field("name", &self.inner.info.name)
549 .finish_non_exhaustive()
550 }
551}
552
553impl ToolInvocationContext {
554 pub(crate) fn new(
555 tool_name: ToolName,
556 call_id: Option<String>,
557 interactions: mpsc::Sender<LocalInteractionEnvelope>,
558 capabilities: ToolInteractionCapabilities,
559 ) -> Self {
560 Self {
561 tool_name,
562 call_id,
563 interactions,
564 capabilities,
565 }
566 }
567
568 /// Requests text from the caller and waits for its resolution.
569 ///
570 /// Returns [`ToolError::UndeclaredInteraction`] when the registration did
571 /// not declare the requested text capability.
572 ///
573 /// # Errors
574 ///
575 /// Returns [`ToolError::UndeclaredInteraction`] for absent text or multiline capability, [`ToolError::InvalidInteraction`] for an invalid reply, or the corresponding decline, cancellation, or closure error.
576 ///
577 /// See [`crate::local::ToolRegistration::interactive`] for a complete example.
578 ///
579 pub async fn request_text(
580 &self,
581 prompt: impl Into<String>,
582 multiline: bool,
583 ) -> Result<String, ToolError> {
584 if !self.capabilities.text || (multiline && !self.capabilities.multiline_text) {
585 return Err(ToolError::UndeclaredInteraction {
586 tool: self.tool_name.clone(),
587 kind: "text".to_owned(),
588 });
589 }
590 let response = self
591 .request(ToolInteractionRequest {
592 id: ToolInteractionId::new(),
593 tool_name: self.tool_name.clone(),
594 call_id: self.call_id.clone(),
595 prompt: prompt.into(),
596 kind: ToolInteractionKind::Text { multiline },
597 })
598 .await?;
599 match response.into_resolution() {
600 InteractionResolution::Submit(content) => content
601 .as_str()
602 .map(str::to_owned)
603 .ok_or_else(|| ToolError::InvalidInteraction {
604 id: "submitted-text".to_owned(),
605 message: "text interaction content must be a string".to_owned(),
606 }),
607 InteractionResolution::Decline(reason) => Err(ToolError::InteractionDeclined {
608 tool: self.tool_name.clone(),
609 reason,
610 }),
611 InteractionResolution::Cancel(reason) => Err(ToolError::Cancelled {
612 tool: self.tool_name.clone(),
613 reason,
614 }),
615 }
616 }
617
618 /// Requests a JSON form value from the caller and waits for its resolution.
619 ///
620 /// When `schema_validation` is true, the submitted object is validated
621 /// before it is returned.
622 /// Form replies must be objects even when schema validation is disabled.
623 ///
624 /// # Errors
625 ///
626 /// Returns [`ToolError::UndeclaredInteraction`] for an absent form capability, validation errors for malformed replies or schemas, or the corresponding decline, cancellation, or closure error.
627 ///
628 /// # Examples
629 ///
630 /// ```
631 ///
632 /// # #[tokio::main(flavor = "current_thread")]
633 /// # async fn main() -> Result<(), tools_kit::ToolError> {
634 /// use tools_kit::{
635 /// ToolHandlerBackend, Toolset, ToolsetBackend,
636 /// declaration::{ToolInfo, ToolName},
637 /// execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult},
638 /// interaction::{ToolInteractionCapabilities, ToolInteractionResponse},
639 /// local::ToolRegistration,
640 /// schema::JsonSchema,
641 /// };
642 /// let name = ToolName::new("ask")?;
643 /// let mut info = ToolInfo::new(
644 /// name.clone(),
645 /// JsonSchema::strict(serde_json::json!({"type":"object"})),
646 /// );
647 /// info.interactions = ToolInteractionCapabilities { form: true, form_schema_validation: true, ..Default::default() };
648 /// let registration = ToolRegistration::interactive(info, |_input, context| async move {
649 /// let city = context.request_form("Which city?", serde_json::json!({"type":"object","required":["city"],"properties":{"city":{"type":"string"}}}), true).await?;
650 /// Ok(ToolResult::json(city))
651 /// });
652 /// let tools = Toolset::local(vec![registration])?;
653 /// let observed = async {
654 /// let mut handler = tools.tool_handler(name).await?;
655 /// let pending = handler.step(ToolHandlerInput::Invoke(ToolInvocation { call_id: None, arguments: serde_json::json!({}) })).await?;
656 /// let ToolHandlerOutput::Interaction(request) = pending else { panic!("interaction expected") };
657 /// let reply = ToolInteractionResponse::submit(request.id, serde_json::json!({"city":"Chicago"}));
658 /// let done = handler.step(ToolHandlerInput::Interaction(reply)).await?;
659 /// let ToolHandlerOutput::Completed(result) = done else { panic!("completion expected") };
660 /// assert!(!result.is_error);
661 /// assert_eq!(result.structured_content, Some(serde_json::json!({"city":"Chicago"})));
662 /// Ok::<(), tools_kit::ToolError>(())
663 /// }.await;
664 /// let cleanup = tools.shutdown().await;
665 /// if let Err(error) = &cleanup {
666 /// eprintln!("Cleanup failed: {error}");
667 /// }
668 /// observed?;
669 /// cleanup?;
670 ///
671 /// # Ok(())
672 /// # }
673 /// ```
674 ///
675 pub async fn request_form(
676 &self,
677 prompt: impl Into<String>,
678 schema: serde_json::Value,
679 schema_validation: bool,
680 ) -> Result<serde_json::Value, ToolError> {
681 if !self.capabilities.form
682 || (schema_validation && !self.capabilities.form_schema_validation)
683 {
684 return Err(ToolError::UndeclaredInteraction {
685 tool: self.tool_name.clone(),
686 kind: "form".to_owned(),
687 });
688 }
689 let response = self
690 .request(ToolInteractionRequest {
691 id: ToolInteractionId::new(),
692 tool_name: self.tool_name.clone(),
693 call_id: self.call_id.clone(),
694 prompt: prompt.into(),
695 kind: ToolInteractionKind::Form {
696 schema,
697 schema_validation,
698 },
699 })
700 .await?;
701 match response.into_resolution() {
702 InteractionResolution::Submit(content) => Ok(content),
703 InteractionResolution::Decline(reason) => Err(ToolError::InteractionDeclined {
704 tool: self.tool_name.clone(),
705 reason,
706 }),
707 InteractionResolution::Cancel(reason) => Err(ToolError::Cancelled {
708 tool: self.tool_name.clone(),
709 reason,
710 }),
711 }
712 }
713
714 async fn request(
715 &self,
716 request: ToolInteractionRequest,
717 ) -> Result<ToolInteractionResponse, ToolError> {
718 let (response, receiver) = oneshot::channel();
719 self.interactions
720 .send(LocalInteractionEnvelope {
721 request: request.clone(),
722 response,
723 })
724 .await
725 .map_err(|_| ToolError::Transport {
726 tool: Some(self.tool_name.clone()),
727 message: "local interaction channel closed".to_owned(),
728 })?;
729 let response = receiver.await.map_err(|_| ToolError::Cancelled {
730 tool: self.tool_name.clone(),
731 reason: Some("local interaction response channel closed".to_owned()),
732 })?;
733 request.validate_response(&response)?;
734 Ok(response)
735 }
736}
737
738impl fmt::Debug for ToolInvocationContext {
739 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
740 formatter
741 .debug_struct("ToolInvocationContext")
742 .field("tool_name", &self.tool_name)
743 .field("capabilities", &self.capabilities)
744 .finish_non_exhaustive()
745 }
746}