Skip to main content

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}