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