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