use std::{
future::Future,
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll},
};
use tokio::sync::{Notify, watch};
use tokio::task::JoinHandle;
use crate::{
ToolError, ToolHandler,
declaration::ToolInfo,
local::ToolRegistration,
mcp::McpClientConfig,
name::ToolName,
sources::{local::LocalToolset, mcp::McpToolset},
};
pub trait ToolsetBackend: Send + Sync + 'static {
type Handler: crate::ToolHandlerBackend;
fn list_tools(&self) -> impl Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_;
fn tool_handler(
&self,
name: ToolName,
) -> impl Future<Output = Result<Self::Handler, ToolError>> + Send + '_;
fn shutdown(&self) -> impl Future<Output = Result<(), ToolError>> + Send + '_;
}
#[derive(Clone)]
pub struct Toolset {
runtime: Arc<ToolsetRuntime>,
}
struct ToolsetRuntime {
backend: InnerToolset,
lifecycle: ToolsetLifecycle,
}
enum InnerToolset {
Local(LocalToolset),
Mcp(McpToolset),
Extension(Arc<dyn ErasedToolsetBackend>),
}
impl Toolset {
pub fn local(registrations: Vec<ToolRegistration>) -> Result<Self, ToolError> {
Ok(Self::from_local(LocalToolset::build(registrations)?))
}
pub async fn mcp(config: McpClientConfig) -> Result<Self, ToolError> {
Ok(Self::from_mcp(McpToolset::connect(config).await?))
}
#[must_use]
pub fn from_backend<B: ToolsetBackend>(backend: B) -> Self {
Self {
runtime: Arc::new(ToolsetRuntime::new(InnerToolset::Extension(Arc::new(
backend,
)))),
}
}
fn from_local(toolset: LocalToolset) -> Self {
Self {
runtime: Arc::new(ToolsetRuntime::new(InnerToolset::Local(toolset))),
}
}
fn from_mcp(toolset: McpToolset) -> Self {
Self {
runtime: Arc::new(ToolsetRuntime::new(InnerToolset::Mcp(toolset))),
}
}
}
impl std::fmt::Debug for Toolset {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.debug_struct("Toolset").finish_non_exhaustive()
}
}
trait ErasedToolsetBackend: Send + Sync {
fn list_tools_erased(
&self,
) -> Pin<Box<dyn Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_>>;
fn tool_handler_erased(
&self,
name: ToolName,
) -> Pin<Box<dyn Future<Output = Result<ToolHandler, ToolError>> + Send + '_>>;
fn shutdown_erased(&self) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + '_>>;
}
impl<B: ToolsetBackend> ErasedToolsetBackend for B {
fn list_tools_erased(
&self,
) -> Pin<Box<dyn Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_>> {
Box::pin(self.list_tools())
}
fn tool_handler_erased(
&self,
name: ToolName,
) -> Pin<Box<dyn Future<Output = Result<ToolHandler, ToolError>> + Send + '_>> {
Box::pin(async move {
self.tool_handler(name)
.await
.map(ToolHandler::from_extension)
})
}
fn shutdown_erased(&self) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + '_>> {
Box::pin(self.shutdown())
}
}
impl ToolsetBackend for InnerToolset {
type Handler = ToolHandler;
async fn list_tools(&self) -> Result<Vec<ToolInfo>, ToolError> {
match self {
Self::Local(toolset) => toolset.list_tools().await,
Self::Mcp(toolset) => toolset.list_tools().await,
Self::Extension(toolset) => toolset.list_tools_erased().await,
}
}
async fn tool_handler(&self, name: ToolName) -> Result<Self::Handler, ToolError> {
match self {
Self::Local(toolset) => toolset
.tool_handler(name)
.await
.map(ToolHandler::from_local),
Self::Mcp(toolset) => toolset.tool_handler(name).await.map(ToolHandler::from_mcp),
Self::Extension(toolset) => toolset.tool_handler_erased(name).await,
}
}
async fn shutdown(&self) -> Result<(), ToolError> {
match self {
Self::Local(toolset) => toolset.shutdown().await,
Self::Mcp(toolset) => toolset.shutdown().await,
Self::Extension(toolset) => toolset.shutdown_erased().await,
}
}
}
impl ToolsetBackend for Toolset {
type Handler = ToolHandler;
async fn list_tools(&self) -> Result<Vec<ToolInfo>, ToolError> {
let _admission = self.runtime.admit("list_tools")?;
self.runtime.backend.list_tools().await
}
async fn tool_handler(&self, name: ToolName) -> Result<Self::Handler, ToolError> {
let _admission = self.runtime.admit("tool_handler")?;
self.runtime.backend.tool_handler(name).await
}
async fn shutdown(&self) -> Result<(), ToolError> {
self.runtime.shutdown().await
}
}
struct ToolsetLifecycle {
state: Mutex<ToolsetState>,
changed: Notify,
}
enum ToolsetState {
Open {
admitted: usize,
},
Closing {
admitted: usize,
completion: watch::Receiver<Option<Result<(), ToolError>>>,
task: JoinHandle<()>,
},
Closed {
terminal: Result<(), ToolError>,
},
}
struct ToolsetAdmission {
runtime: Arc<ToolsetRuntime>,
}
impl ToolsetRuntime {
fn new(backend: InnerToolset) -> Self {
Self {
backend,
lifecycle: ToolsetLifecycle {
state: Mutex::new(ToolsetState::Open { admitted: 0 }),
changed: Notify::new(),
},
}
}
fn admit(self: &Arc<Self>, operation: &'static str) -> Result<ToolsetAdmission, ToolError> {
let mut state = self.lifecycle.state();
match &mut *state {
ToolsetState::Open { admitted } => {
*admitted = admitted
.checked_add(1)
.ok_or_else(|| ToolError::Transport {
tool: None,
message: "toolset admission is exhausted".to_owned(),
})?;
Ok(ToolsetAdmission {
runtime: Arc::clone(self),
})
}
ToolsetState::Closing { .. } | ToolsetState::Closed { .. } => {
Err(ToolError::closed(operation))
}
}
}
async fn shutdown(self: &Arc<Self>) -> Result<(), ToolError> {
self.start_shutdown()?;
self.lifecycle.terminal().await
}
fn start_shutdown(self: &Arc<Self>) -> Result<(), ToolError> {
let mut state = self.lifecycle.state();
let admitted = match &*state {
ToolsetState::Open { admitted } => *admitted,
ToolsetState::Closing { .. } | ToolsetState::Closed { .. } => return Ok(()),
};
let handle = tokio::runtime::Handle::try_current().map_err(|_| ToolError::Transport {
tool: None,
message: "toolset shutdown requires a Tokio runtime".to_owned(),
})?;
let (terminal, completion) = watch::channel(None);
let runtime = Arc::clone(self);
let task = handle.spawn(async move {
terminal.send_replace(Some(runtime.finish_shutdown().await));
});
*state = ToolsetState::Closing {
admitted,
completion,
task,
};
Ok(())
}
async fn finish_shutdown(self: Arc<Self>) -> Result<(), ToolError> {
self.lifecycle.drain().await;
self.backend.shutdown().await
}
}
impl ToolsetLifecycle {
fn state(&self) -> std::sync::MutexGuard<'_, ToolsetState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn release(&self) {
let should_notify = {
let mut state = self.state();
match &mut *state {
ToolsetState::Open { admitted } => {
debug_assert_ne!(*admitted, 0);
*admitted = admitted.saturating_sub(1);
false
}
ToolsetState::Closing { admitted, .. } => {
debug_assert_ne!(*admitted, 0);
*admitted = admitted.saturating_sub(1);
*admitted == 0
}
ToolsetState::Closed { .. } => {
debug_assert!(false, "closed toolset retained an admission");
false
}
}
};
if should_notify {
self.changed.notify_waiters();
}
}
async fn drain(&self) {
loop {
let notified = self.changed.notified();
tokio::pin!(notified);
notified.as_mut().enable();
let drained = match &*self.state() {
ToolsetState::Open { admitted } | ToolsetState::Closing { admitted, .. } => {
*admitted == 0
}
ToolsetState::Closed { .. } => true,
};
if drained {
return;
}
notified.await;
}
}
async fn terminal(&self) -> Result<(), ToolError> {
let mut completion = match &*self.state() {
ToolsetState::Closing { completion, .. } => completion.clone(),
ToolsetState::Closed { terminal } => return terminal.clone(),
ToolsetState::Open { .. } => {
return Err(ToolError::Transport {
tool: None,
message: "toolset shutdown lost lifecycle ownership".to_owned(),
});
}
};
let terminal = match completion.wait_for(Option::is_some).await {
Ok(terminal) => terminal
.as_ref()
.cloned()
.ok_or_else(|| ToolError::Transport {
tool: None,
message: "toolset shutdown result is unavailable".to_owned(),
})?,
Err(_) => Err(ToolError::Transport {
tool: None,
message: "toolset shutdown owner exited without a terminal result".to_owned(),
}),
};
loop {
let settled = {
let mut state = self.state();
match &mut *state {
ToolsetState::Closed { terminal } => return terminal.clone(),
ToolsetState::Closing { task, .. } if task.is_finished() => {
let waker = std::task::Waker::noop();
let mut context = Context::from_waker(waker);
match Pin::new(task).poll(&mut context) {
Poll::Ready(joined) => {
let terminal = if joined.is_ok() {
terminal.clone()
} else {
Err(ToolError::Transport {
tool: None,
message: "toolset shutdown owner could not be joined"
.to_owned(),
})
};
*state = ToolsetState::Closed {
terminal: terminal.clone(),
};
Some(terminal)
}
Poll::Pending => None,
}
}
ToolsetState::Closing { .. } => None,
ToolsetState::Open { .. } => {
let terminal = Err(ToolError::Transport {
tool: None,
message: "toolset shutdown lost lifecycle ownership".to_owned(),
});
*state = ToolsetState::Closed {
terminal: terminal.clone(),
};
Some(terminal)
}
}
};
if let Some(terminal) = settled {
self.changed.notify_waiters();
return terminal;
}
tokio::task::yield_now().await;
}
}
}
impl Drop for ToolsetAdmission {
fn drop(&mut self) {
self.runtime.lifecycle.release();
}
}