Skip to main content

tools_kit/
toolset.rs

1use std::{
2    future::Future,
3    pin::Pin,
4    sync::{Arc, Mutex},
5    task::{Context, Poll},
6};
7use tokio::sync::{Notify, watch};
8use tokio::task::JoinHandle;
9
10use crate::{
11    ToolError, ToolHandler,
12    declaration::ToolInfo,
13    local::ToolRegistration,
14    mcp::McpClientConfig,
15    name::ToolName,
16    sources::{local::LocalToolset, mcp::McpToolset},
17};
18
19/// Public implementation contract for one shared executable tool source.
20/// Use this trait directly with concrete caller implementations and their exact
21/// associated handlers. [`Toolset::from_backend`] is optional normalization when a
22/// supplied Toolset is needed. Shared shutdown obligations apply to both forms.
23///
24/// # Examples
25///
26/// ```
27///
28/// # #[tokio::main(flavor = "current_thread")]
29/// # async fn main() -> Result<(), tools_kit::ToolError> {
30/// use tools_kit::{Toolset, ToolsetBackend};
31/// let tools = Toolset::local(Vec::new())?;
32/// assert!(tools.list_tools().await?.is_empty());
33/// tools.shutdown().await?;
34///
35/// # Ok(())
36/// # }
37/// ```
38///
39/// # Implement an executable caller Toolset
40///
41/// This runnable caller implementation executes a bounded string echo without tasks,
42/// networking, or asynchronous cleanup. It has one unique terminal handler per call,
43/// validates its advertised input shape itself, and seals root/handler admission on
44/// shared shutdown. More complex implementations must own accepted asynchronous work
45/// independently of a shutdown waiter. Neither this Toolset nor its Handler is Clone.
46///
47/// Direct use keeps the concrete associated child. Moving the same Toolset into
48/// [`Toolset::from_backend`] is optional; normalization does not supply missing
49/// backend validation, cancellation, or lifecycle behavior.
50///
51/// ```
52/// use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
53/// use tools_kit::{ToolError, ToolHandlerBackend, Toolset, ToolsetBackend};
54/// use tools_kit::declaration::{ToolInfo, ToolName};
55/// use tools_kit::execution::{ToolContent, ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult};
56/// use tools_kit::schema::JsonSchema;
57///
58/// struct EchoTools { info: ToolInfo, closed: Arc<AtomicBool> }
59/// struct EchoHandler { name: ToolName, closed: Arc<AtomicBool>, terminal: bool }
60/// impl EchoTools {
61///     fn check(&self) -> Result<(), ToolError> {
62///         if self.closed.load(Ordering::SeqCst) {
63///             return Err(ToolError::Closed { operation: "echo toolset".into() });
64///         }
65///         Ok(())
66///     }
67/// }
68/// impl ToolsetBackend for EchoTools {
69///     type Handler = EchoHandler;
70///     async fn list_tools(&self) -> Result<Vec<ToolInfo>, ToolError> {
71///         self.check()?;
72///         Ok(vec![self.info.clone()])
73///     }
74///     async fn tool_handler(&self, name: ToolName) -> Result<EchoHandler, ToolError> {
75///         self.check()?;
76///         if name != self.info.name { return Err(ToolError::UnknownTool { name }); }
77///         Ok(EchoHandler { name, closed: Arc::clone(&self.closed), terminal: false })
78///     }
79///     async fn shutdown(&self) -> Result<(), ToolError> {
80///         // No await: this synchronous toolset owns no unfinished external work.
81///         self.closed.store(true, Ordering::SeqCst);
82///         Ok(())
83///     }
84/// }
85/// impl ToolHandlerBackend for EchoHandler {
86///     async fn step(&mut self, input: ToolHandlerInput) -> Result<ToolHandlerOutput, ToolError> {
87///         if self.closed.load(Ordering::SeqCst) {
88///             return Err(ToolError::Closed { operation: "echo step".into() });
89///         }
90///         if self.terminal {
91///             return Err(ToolError::Protocol { message: "handler already terminal".into() });
92///         }
93///         match input {
94///             ToolHandlerInput::Invoke(invocation) => {
95///                 let Some(text) = invocation.arguments.as_str().filter(|text| text.chars().count() <= 1024) else {
96///                     return Err(ToolError::InvalidArguments {
97///                         tool: self.name.clone(), message: "expected a string of at most 1024 characters".into(), issues: Vec::new(),
98///                     });
99///                 };
100///                 self.terminal = true;
101///                 Ok(ToolHandlerOutput::Completed(ToolResult::text(text)))
102///             }
103///             ToolHandlerInput::Cancel { reason } => {
104///                 self.terminal = true;
105///                 Err(ToolError::Cancelled { tool: self.name.clone(), reason })
106///             }
107///             ToolHandlerInput::Interaction(_) => Err(ToolError::Protocol {
108///                 message: "this tool never requests interaction".into(),
109///             }),
110///         }
111///     }
112/// }
113///
114/// # #[tokio::main(flavor = "current_thread")]
115/// # async fn main() -> Result<(), ToolError> {
116/// let name = ToolName::new("echo")?;
117/// let concrete = EchoTools {
118///     info: ToolInfo::new(name.clone(), JsonSchema::strict(serde_json::json!({"type":"string","maxLength":1024}))),
119///     closed: Arc::new(AtomicBool::new(false)),
120/// };
121/// let mut direct = concrete.tool_handler(name.clone()).await?;
122/// let input = ToolInvocation { call_id: Some("call-1".into()), arguments: serde_json::json!("unchanged") };
123/// let ToolHandlerOutput::Completed(result) = direct.step(ToolHandlerInput::Invoke(input.clone())).await?
124///     else { panic!("echo does not interact") };
125/// assert!(!result.is_error);
126/// assert!(matches!(result.content.as_slice(), [ToolContent::Text { text, .. }] if text == "unchanged"));
127/// assert!(direct.step(ToolHandlerInput::Invoke(input.clone())).await.is_err());
128/// let tools = Toolset::from_backend(concrete); // Sole public erased ingress.
129/// let mut normalized = tools.tool_handler(name).await?;
130/// let observed = normalized.step(ToolHandlerInput::Invoke(input)).await;
131/// let cleanup = tools.shutdown().await;
132/// let ToolHandlerOutput::Completed(result) = observed? else { panic!("echo does not interact") };
133/// assert!(matches!(result.content.as_slice(), [ToolContent::Text { text, .. }] if text == "unchanged"));
134/// cleanup?;
135/// assert!(tools.list_tools().await.is_err());
136/// tools.shutdown().await?;
137/// # Ok(())
138/// # }
139/// ```
140///
141pub trait ToolsetBackend: Send + Sync + 'static {
142    /// Unique resumable handler produced by this tool source.
143    type Handler: crate::ToolHandlerBackend;
144
145    /// Lists the complete executable inventory.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`ToolError::Closed`] after sealing, or the underlying inventory failure.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    ///
155    /// # #[tokio::main(flavor = "current_thread")]
156    /// # async fn main() -> Result<(), tools_kit::ToolError> {
157    /// use tools_kit::{Toolset, ToolsetBackend};
158    /// let tools = Toolset::local(Vec::new())?;
159    /// assert!(tools.list_tools().await?.is_empty());
160    /// tools.shutdown().await?;
161    ///
162    /// # Ok(())
163    /// # }
164    /// ```
165    ///
166    fn list_tools(&self) -> impl Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_;
167
168    /// Creates a unique handler for one selected tool name.
169    ///
170    /// # Errors
171    ///
172    /// Returns an unknown-name error when no declared tool matches, [`ToolError::Closed`] after sealing, or the underlying handler-construction failure.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    ///
178    /// # #[tokio::main(flavor = "current_thread")]
179    /// # async fn main() -> Result<(), tools_kit::ToolError> {
180    /// use tools_kit::{declaration::{ToolInfo, ToolName}, schema::JsonSchema};
181    /// let info = ToolInfo::new(ToolName::new("ping")?, JsonSchema::strict(serde_json::json!({"type":"object"})));
182    /// use tools_kit::{local::ToolRegistration, execution::ToolResult};
183    /// let registration = ToolRegistration::function(info, |_| async { Ok(ToolResult::text("pong")) });
184    /// use tools_kit::{Toolset, ToolsetBackend, ToolHandlerBackend, execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation}};
185    /// let tools = Toolset::local(vec![registration])?;
186    /// let mut handler = tools.tool_handler(ToolName::new("ping")?).await?;
187    /// let result = handler.step(ToolHandlerInput::Invoke(ToolInvocation { call_id: None, arguments: serde_json::json!({}) })).await;
188    /// let cleanup = tools.shutdown().await;
189    /// assert!(matches!(result?, ToolHandlerOutput::Completed(_)));
190    /// cleanup?;
191    ///
192    /// # Ok(())
193    /// # }
194    /// ```
195    ///
196    fn tool_handler(
197        &self,
198        name: ToolName,
199    ) -> impl Future<Output = Result<Self::Handler, ToolError>> + Send + '_;
200
201    /// Seals admission and performs shared exactly-once cleanup.
202    ///
203    /// This contract applies to direct caller implementations as well as supplied
204    /// handles. On first acceptance, retain and track the cleanup independently of
205    /// this awaiting future. Dropping a shutdown waiter or the last public value
206    /// must not abandon accepted cleanup. Repeated calls observe its terminal
207    /// result; they must not start another cleanup. Listing and handler admission
208    /// reject after sealing. A consumer may bound its wait with a deadline, so
209    /// putting all cleanup solely inside this borrowed future is insufficient.
210    ///
211    /// # Errors
212    ///
213    /// Replays the shared cleanup error, including a cleanup timeout, to concurrent and later callers. A failed observation does not authorize abandoning accepted cleanup.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    ///
219    /// # #[tokio::main(flavor = "current_thread")]
220    /// # async fn main() -> Result<(), tools_kit::ToolError> {
221    /// use tools_kit::{Toolset, ToolsetBackend};
222    /// let tools = Toolset::local(Vec::new())?;
223    /// let shared = tools.clone();
224    /// tools.shutdown().await?;
225    /// shared.shutdown().await?;
226    /// assert!(shared.list_tools().await.is_err());
227    ///
228    /// # Ok(())
229    /// # }
230    /// ```
231    ///
232    fn shutdown(&self) -> impl Future<Output = Result<(), ToolError>> + Send + '_;
233}
234
235/// Opaque cloneable Toolset over Local, MCP, or downstream behavior.
236/// Clones share admission and cleanup. Dropping a clone alone does not initiate
237/// asynchronous cleanup; explicitly await [`ToolsetBackend::shutdown`].
238///
239/// # Examples
240///
241/// ```
242///
243/// # #[tokio::main(flavor = "current_thread")]
244/// # async fn main() -> Result<(), tools_kit::ToolError> {
245/// use tools_kit::{Toolset, ToolsetBackend};
246/// let tools = Toolset::local(Vec::new())?;
247/// assert!(tools.list_tools().await?.is_empty());
248/// tools.shutdown().await?;
249///
250/// # Ok(())
251/// # }
252/// ```
253///
254#[derive(Clone)]
255pub struct Toolset {
256    runtime: Arc<ToolsetRuntime>,
257}
258
259struct ToolsetRuntime {
260    backend: InnerToolset,
261    lifecycle: ToolsetLifecycle,
262}
263
264enum InnerToolset {
265    Local(LocalToolset),
266    Mcp(McpToolset),
267    Extension(Arc<dyn ErasedToolsetBackend>),
268}
269
270impl Toolset {
271    /// Builds an in-process Toolset from complete registrations.
272    ///
273    /// # Errors
274    ///
275    /// Returns [`ToolError::DuplicateName`] for duplicate names; an empty inventory is valid.
276    ///
277    /// # Examples
278    ///
279    /// ```
280    ///
281    /// # #[tokio::main(flavor = "current_thread")]
282    /// # async fn main() -> Result<(), tools_kit::ToolError> {
283    /// use tools_kit::{Toolset, ToolsetBackend};
284    /// let tools = Toolset::local(Vec::new())?;
285    /// assert!(tools.list_tools().await?.is_empty());
286    /// tools.shutdown().await?;
287    ///
288    /// # Ok(())
289    /// # }
290    /// ```
291    ///
292    pub fn local(registrations: Vec<ToolRegistration>) -> Result<Self, ToolError> {
293        Ok(Self::from_local(LocalToolset::build(registrations)?))
294    }
295
296    /// Connects one MCP client Toolset and discovers its inventory.
297    /// This example requires a running MCP service at the configured endpoint.
298    ///
299    /// # Errors
300    ///
301    /// Returns [`ToolError`] for invalid connection settings, process startup, transport or initialization failure, malformed inventory, or an elapsed request deadline.
302    ///
303    /// # Examples
304    ///
305    /// ```no_run
306    ///
307    /// # #[tokio::main(flavor = "current_thread")]
308    /// # async fn main() -> Result<(), tools_kit::ToolError> {
309    /// use tools_kit::mcp::{McpClientConfig, McpConnectionConfig};
310    /// let connection = McpConnectionConfig::StreamableHttp { endpoint: "http://127.0.0.1:8000/mcp".parse().unwrap(), headers: Default::default() };
311    /// let config = McpClientConfig::new(connection);
312    /// use tools_kit::{Toolset, ToolsetBackend};
313    /// let tools = Toolset::mcp(config).await?;
314    /// let inventory = tools.list_tools().await;
315    /// let cleanup = tools.shutdown().await;
316    /// let _ = inventory?;
317    /// cleanup?;
318    ///
319    /// # Ok(())
320    /// # }
321    /// ```
322    ///
323    pub async fn mcp(config: McpClientConfig) -> Result<Self, ToolError> {
324        Ok(Self::from_mcp(McpToolset::connect(config).await?))
325    }
326
327    /// Wraps a downstream Toolset implementation.
328    /// The associated handler is normalized privately. Concrete callers may instead
329    /// retain the backend directly; there is no public child normalization constructor.
330    ///
331    /// # Examples
332    ///
333    /// ```
334    ///
335    /// # #[tokio::main(flavor = "current_thread")]
336    /// # async fn main() -> Result<(), tools_kit::ToolError> {
337    /// use std::sync::atomic::{AtomicBool, Ordering};
338    /// use tools_kit::{ToolError, Toolset, ToolsetBackend, ToolHandlerBackend};
339    /// use tools_kit::declaration::{ToolInfo, ToolName};
340    /// use tools_kit::execution::{ToolHandlerInput, ToolHandlerOutput};
341    /// struct EmptyInventory { closed: AtomicBool }
342    /// enum NoHandler {}
343    /// impl ToolHandlerBackend for NoHandler {
344    ///     async fn step(&mut self, _: ToolHandlerInput) -> Result<ToolHandlerOutput, ToolError> {
345    ///         match *self {}
346    ///     }
347    /// }
348    /// impl ToolsetBackend for EmptyInventory {
349    ///     type Handler = NoHandler;
350    ///     async fn list_tools(&self) -> Result<Vec<ToolInfo>, ToolError> {
351    ///         if self.closed.load(Ordering::Acquire) {
352    ///             return Err(ToolError::Closed { operation: "list_tools".into() });
353    ///         }
354    ///         Ok(Vec::new())
355    ///     }
356    ///     async fn tool_handler(&self, name: ToolName) -> Result<NoHandler, ToolError> {
357    ///         if self.closed.load(Ordering::Acquire) {
358    ///             return Err(ToolError::Closed { operation: "tool_handler".into() });
359    ///         }
360    ///         Err(ToolError::UnknownTool { name })
361    ///     }
362    ///     async fn shutdown(&self) -> Result<(), ToolError> {
363    ///         self.closed.store(true, Ordering::Release);
364    ///         Ok(()) // This empty implementation owns no asynchronous resources.
365    ///     }
366    /// }
367    /// let concrete = EmptyInventory { closed: AtomicBool::new(false) };
368    /// assert!(concrete.list_tools().await?.is_empty());
369    /// let normalized = Toolset::from_backend(concrete);
370    /// normalized.shutdown().await?;
371    /// assert!(normalized.list_tools().await.is_err());
372    ///
373    /// # Ok(())
374    /// # }
375    /// ```
376    ///
377    #[must_use]
378    pub fn from_backend<B: ToolsetBackend>(backend: B) -> Self {
379        Self {
380            runtime: Arc::new(ToolsetRuntime::new(InnerToolset::Extension(Arc::new(
381                backend,
382            )))),
383        }
384    }
385
386    fn from_local(toolset: LocalToolset) -> Self {
387        Self {
388            runtime: Arc::new(ToolsetRuntime::new(InnerToolset::Local(toolset))),
389        }
390    }
391
392    fn from_mcp(toolset: McpToolset) -> Self {
393        Self {
394            runtime: Arc::new(ToolsetRuntime::new(InnerToolset::Mcp(toolset))),
395        }
396    }
397}
398
399impl std::fmt::Debug for Toolset {
400    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
401        formatter.debug_struct("Toolset").finish_non_exhaustive()
402    }
403}
404
405trait ErasedToolsetBackend: Send + Sync {
406    fn list_tools_erased(
407        &self,
408    ) -> Pin<Box<dyn Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_>>;
409
410    fn tool_handler_erased(
411        &self,
412        name: ToolName,
413    ) -> Pin<Box<dyn Future<Output = Result<ToolHandler, ToolError>> + Send + '_>>;
414
415    fn shutdown_erased(&self) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + '_>>;
416}
417
418impl<B: ToolsetBackend> ErasedToolsetBackend for B {
419    fn list_tools_erased(
420        &self,
421    ) -> Pin<Box<dyn Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_>> {
422        Box::pin(self.list_tools())
423    }
424
425    fn tool_handler_erased(
426        &self,
427        name: ToolName,
428    ) -> Pin<Box<dyn Future<Output = Result<ToolHandler, ToolError>> + Send + '_>> {
429        Box::pin(async move {
430            self.tool_handler(name)
431                .await
432                .map(ToolHandler::from_extension)
433        })
434    }
435
436    fn shutdown_erased(&self) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + '_>> {
437        Box::pin(self.shutdown())
438    }
439}
440
441impl ToolsetBackend for InnerToolset {
442    type Handler = ToolHandler;
443
444    async fn list_tools(&self) -> Result<Vec<ToolInfo>, ToolError> {
445        match self {
446            Self::Local(toolset) => toolset.list_tools().await,
447            Self::Mcp(toolset) => toolset.list_tools().await,
448            Self::Extension(toolset) => toolset.list_tools_erased().await,
449        }
450    }
451
452    async fn tool_handler(&self, name: ToolName) -> Result<Self::Handler, ToolError> {
453        match self {
454            Self::Local(toolset) => toolset
455                .tool_handler(name)
456                .await
457                .map(ToolHandler::from_local),
458            Self::Mcp(toolset) => toolset.tool_handler(name).await.map(ToolHandler::from_mcp),
459            Self::Extension(toolset) => toolset.tool_handler_erased(name).await,
460        }
461    }
462
463    async fn shutdown(&self) -> Result<(), ToolError> {
464        match self {
465            Self::Local(toolset) => toolset.shutdown().await,
466            Self::Mcp(toolset) => toolset.shutdown().await,
467            Self::Extension(toolset) => toolset.shutdown_erased().await,
468        }
469    }
470}
471
472impl ToolsetBackend for Toolset {
473    type Handler = ToolHandler;
474
475    async fn list_tools(&self) -> Result<Vec<ToolInfo>, ToolError> {
476        let _admission = self.runtime.admit("list_tools")?;
477        self.runtime.backend.list_tools().await
478    }
479
480    async fn tool_handler(&self, name: ToolName) -> Result<Self::Handler, ToolError> {
481        let _admission = self.runtime.admit("tool_handler")?;
482        self.runtime.backend.tool_handler(name).await
483    }
484
485    async fn shutdown(&self) -> Result<(), ToolError> {
486        self.runtime.shutdown().await
487    }
488}
489
490struct ToolsetLifecycle {
491    state: Mutex<ToolsetState>,
492    changed: Notify,
493}
494
495enum ToolsetState {
496    Open {
497        admitted: usize,
498    },
499    Closing {
500        admitted: usize,
501        completion: watch::Receiver<Option<Result<(), ToolError>>>,
502        task: JoinHandle<()>,
503    },
504    Closed {
505        terminal: Result<(), ToolError>,
506    },
507}
508
509struct ToolsetAdmission {
510    runtime: Arc<ToolsetRuntime>,
511}
512
513impl ToolsetRuntime {
514    fn new(backend: InnerToolset) -> Self {
515        Self {
516            backend,
517            lifecycle: ToolsetLifecycle {
518                state: Mutex::new(ToolsetState::Open { admitted: 0 }),
519                changed: Notify::new(),
520            },
521        }
522    }
523
524    fn admit(self: &Arc<Self>, operation: &'static str) -> Result<ToolsetAdmission, ToolError> {
525        let mut state = self.lifecycle.state();
526        match &mut *state {
527            ToolsetState::Open { admitted } => {
528                *admitted = admitted
529                    .checked_add(1)
530                    .ok_or_else(|| ToolError::Transport {
531                        tool: None,
532                        message: "toolset admission is exhausted".to_owned(),
533                    })?;
534                Ok(ToolsetAdmission {
535                    runtime: Arc::clone(self),
536                })
537            }
538            ToolsetState::Closing { .. } | ToolsetState::Closed { .. } => {
539                Err(ToolError::closed(operation))
540            }
541        }
542    }
543
544    async fn shutdown(self: &Arc<Self>) -> Result<(), ToolError> {
545        self.start_shutdown()?;
546        self.lifecycle.terminal().await
547    }
548
549    fn start_shutdown(self: &Arc<Self>) -> Result<(), ToolError> {
550        let mut state = self.lifecycle.state();
551        let admitted = match &*state {
552            ToolsetState::Open { admitted } => *admitted,
553            ToolsetState::Closing { .. } | ToolsetState::Closed { .. } => return Ok(()),
554        };
555        let handle = tokio::runtime::Handle::try_current().map_err(|_| ToolError::Transport {
556            tool: None,
557            message: "toolset shutdown requires a Tokio runtime".to_owned(),
558        })?;
559        let (terminal, completion) = watch::channel(None);
560        let runtime = Arc::clone(self);
561        let task = handle.spawn(async move {
562            terminal.send_replace(Some(runtime.finish_shutdown().await));
563        });
564        *state = ToolsetState::Closing {
565            admitted,
566            completion,
567            task,
568        };
569        Ok(())
570    }
571
572    async fn finish_shutdown(self: Arc<Self>) -> Result<(), ToolError> {
573        self.lifecycle.drain().await;
574        self.backend.shutdown().await
575    }
576}
577
578impl ToolsetLifecycle {
579    fn state(&self) -> std::sync::MutexGuard<'_, ToolsetState> {
580        self.state
581            .lock()
582            .unwrap_or_else(std::sync::PoisonError::into_inner)
583    }
584
585    fn release(&self) {
586        let should_notify = {
587            let mut state = self.state();
588            match &mut *state {
589                ToolsetState::Open { admitted } => {
590                    debug_assert_ne!(*admitted, 0);
591                    *admitted = admitted.saturating_sub(1);
592                    false
593                }
594                ToolsetState::Closing { admitted, .. } => {
595                    debug_assert_ne!(*admitted, 0);
596                    *admitted = admitted.saturating_sub(1);
597                    *admitted == 0
598                }
599                ToolsetState::Closed { .. } => {
600                    debug_assert!(false, "closed toolset retained an admission");
601                    false
602                }
603            }
604        };
605        if should_notify {
606            self.changed.notify_waiters();
607        }
608    }
609
610    async fn drain(&self) {
611        loop {
612            let notified = self.changed.notified();
613            tokio::pin!(notified);
614            notified.as_mut().enable();
615            let drained = match &*self.state() {
616                ToolsetState::Open { admitted } | ToolsetState::Closing { admitted, .. } => {
617                    *admitted == 0
618                }
619                ToolsetState::Closed { .. } => true,
620            };
621            if drained {
622                return;
623            }
624            notified.await;
625        }
626    }
627
628    async fn terminal(&self) -> Result<(), ToolError> {
629        let mut completion = match &*self.state() {
630            ToolsetState::Closing { completion, .. } => completion.clone(),
631            ToolsetState::Closed { terminal } => return terminal.clone(),
632            ToolsetState::Open { .. } => {
633                return Err(ToolError::Transport {
634                    tool: None,
635                    message: "toolset shutdown lost lifecycle ownership".to_owned(),
636                });
637            }
638        };
639        let terminal = match completion.wait_for(Option::is_some).await {
640            Ok(terminal) => terminal
641                .as_ref()
642                .cloned()
643                .ok_or_else(|| ToolError::Transport {
644                    tool: None,
645                    message: "toolset shutdown result is unavailable".to_owned(),
646                })?,
647            Err(_) => Err(ToolError::Transport {
648                tool: None,
649                message: "toolset shutdown owner exited without a terminal result".to_owned(),
650            }),
651        };
652        loop {
653            let settled = {
654                let mut state = self.state();
655                match &mut *state {
656                    ToolsetState::Closed { terminal } => return terminal.clone(),
657                    ToolsetState::Closing { task, .. } if task.is_finished() => {
658                        let waker = std::task::Waker::noop();
659                        let mut context = Context::from_waker(waker);
660                        match Pin::new(task).poll(&mut context) {
661                            Poll::Ready(joined) => {
662                                let terminal = if joined.is_ok() {
663                                    terminal.clone()
664                                } else {
665                                    Err(ToolError::Transport {
666                                        tool: None,
667                                        message: "toolset shutdown owner could not be joined"
668                                            .to_owned(),
669                                    })
670                                };
671                                *state = ToolsetState::Closed {
672                                    terminal: terminal.clone(),
673                                };
674                                Some(terminal)
675                            }
676                            Poll::Pending => None,
677                        }
678                    }
679                    ToolsetState::Closing { .. } => None,
680                    ToolsetState::Open { .. } => {
681                        let terminal = Err(ToolError::Transport {
682                            tool: None,
683                            message: "toolset shutdown lost lifecycle ownership".to_owned(),
684                        });
685                        *state = ToolsetState::Closed {
686                            terminal: terminal.clone(),
687                        };
688                        Some(terminal)
689                    }
690                }
691            };
692            if let Some(terminal) = settled {
693                self.changed.notify_waiters();
694                return terminal;
695            }
696            tokio::task::yield_now().await;
697        }
698    }
699}
700
701impl Drop for ToolsetAdmission {
702    fn drop(&mut self) {
703        self.runtime.lifecycle.release();
704    }
705}