use super::{ToolCapabilities, ToolContext, ToolRegistry, ToolResult};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::{Arc, Weak};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HostDirectPolicy {
TrustedControlPlane,
GovernedControlPlane,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InvocationOrigin {
Agent,
Nested,
RuntimeInternal,
HostDirect(HostDirectPolicy),
HostDirectNested(HostDirectPolicy),
}
impl InvocationOrigin {
pub(crate) fn is_nested(self) -> bool {
matches!(
self,
Self::Nested | Self::RuntimeInternal | Self::HostDirectNested(_)
)
}
}
#[derive(Debug, Clone)]
pub(crate) struct ToolInvocation {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) args: Value,
pub(crate) origin: InvocationOrigin,
pub(crate) recent_tools: Vec<String>,
}
impl ToolInvocation {
pub(crate) fn agent(
id: impl Into<String>,
name: impl Into<String>,
args: Value,
recent_tools: Vec<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
args,
origin: InvocationOrigin::Agent,
recent_tools,
}
}
pub(crate) fn nested(name: impl Into<String>, args: Value) -> Self {
let name = name.into();
Self {
id: format!("nested-{name}-{}", uuid::Uuid::new_v4()),
name,
args,
origin: InvocationOrigin::Nested,
recent_tools: Vec::new(),
}
}
pub(crate) fn runtime_internal(name: impl Into<String>, args: Value) -> Self {
let name = name.into();
Self {
id: format!("runtime-internal-{name}-{}", uuid::Uuid::new_v4()),
name,
args,
origin: InvocationOrigin::RuntimeInternal,
recent_tools: Vec::new(),
}
}
pub(crate) fn host_direct(id: impl Into<String>, name: impl Into<String>, args: Value) -> Self {
Self {
id: id.into(),
name: name.into(),
args,
origin: InvocationOrigin::HostDirect(HostDirectPolicy::TrustedControlPlane),
recent_tools: Vec::new(),
}
}
pub(crate) fn host_governed(
id: impl Into<String>,
name: impl Into<String>,
args: Value,
) -> Self {
Self {
id: id.into(),
name: name.into(),
args,
origin: InvocationOrigin::HostDirect(HostDirectPolicy::GovernedControlPlane),
recent_tools: Vec::new(),
}
}
pub(crate) fn host_direct_nested(
name: impl Into<String>,
args: Value,
policy: HostDirectPolicy,
) -> Self {
let name = name.into();
Self {
id: format!("nested-{name}-{}", uuid::Uuid::new_v4()),
name,
args,
origin: InvocationOrigin::HostDirectNested(policy),
recent_tools: Vec::new(),
}
}
}
#[async_trait]
pub(crate) trait ToolInvoker: Send + Sync {
async fn invoke(&self, invocation: ToolInvocation, ctx: &ToolContext) -> ToolResult;
fn available_tools(&self) -> Vec<String>;
fn capabilities(&self, _name: &str, _args: &Value) -> Option<ToolCapabilities> {
None
}
}
struct RegistryToolInvoker {
registry: RegistryOwnership,
}
enum RegistryOwnership {
Standalone(Arc<ToolRegistry>),
RegistryBound(Weak<ToolRegistry>),
}
impl RegistryOwnership {
fn resolve(&self) -> Option<Arc<ToolRegistry>> {
match self {
Self::Standalone(registry) => Some(Arc::clone(registry)),
Self::RegistryBound(registry) => registry.upgrade(),
}
}
}
#[async_trait]
impl ToolInvoker for RegistryToolInvoker {
async fn invoke(&self, invocation: ToolInvocation, ctx: &ToolContext) -> ToolResult {
let Some(registry) = self.registry.resolve() else {
return ToolResult::error(&invocation.name, "Tool registry is closed".to_string());
};
let invocation_ctx = match ctx.enter_tool_invocation(&invocation.name) {
Ok(ctx) => ctx,
Err(message) => return ToolResult::error(&invocation.name, message),
};
match registry
.execute_with_context(&invocation.name, &invocation.args, &invocation_ctx)
.await
{
Ok(result) => result,
Err(error) => {
ToolResult::error(&invocation.name, format!("Tool execution error: {error}"))
}
}
}
fn available_tools(&self) -> Vec<String> {
self.registry
.resolve()
.map_or_else(Vec::new, |registry| registry.list())
}
fn capabilities(&self, name: &str, args: &Value) -> Option<ToolCapabilities> {
self.registry
.resolve()
.and_then(|registry| registry.capabilities(name, args))
}
}
pub(crate) fn registry_tool_invoker(registry: Arc<ToolRegistry>) -> Arc<dyn ToolInvoker> {
Arc::new(RegistryToolInvoker {
registry: RegistryOwnership::Standalone(registry),
})
}
pub(crate) fn registry_bound_tool_invoker(registry: Arc<ToolRegistry>) -> Arc<dyn ToolInvoker> {
Arc::new(RegistryToolInvoker {
registry: RegistryOwnership::RegistryBound(Arc::downgrade(®istry)),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn standalone_invoker_retains_its_registry() {
let registry = Arc::new(ToolRegistry::new(PathBuf::from("standalone-registry-test")));
let lifetime = Arc::downgrade(®istry);
let invoker = registry_tool_invoker(registry.clone());
drop(registry);
assert!(lifetime.upgrade().is_some());
assert!(invoker.available_tools().is_empty());
}
#[test]
fn registry_bound_invoker_does_not_retain_a_closed_registry() {
let registry = Arc::new(ToolRegistry::new(PathBuf::from("registry-cycle-test")));
let lifetime = Arc::downgrade(®istry);
let invoker = registry_bound_tool_invoker(registry.clone());
drop(registry);
assert!(lifetime.upgrade().is_none());
assert!(invoker.available_tools().is_empty());
assert!(invoker
.capabilities("read", &serde_json::json!({}))
.is_none());
}
}