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}