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}