pub trait ToolsetBackend:
Send
+ Sync
+ 'static {
type Handler: ToolHandlerBackend;
// Required methods
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 + '_;
}Expand description
Public implementation contract for one shared executable tool source.
Use this trait directly with concrete caller implementations and their exact
associated handlers. Toolset::from_backend is optional normalization when a
supplied Toolset is needed. Shared shutdown obligations apply to both forms.
§Examples
use tools_kit::{Toolset, ToolsetBackend};
let tools = Toolset::local(Vec::new())?;
assert!(tools.list_tools().await?.is_empty());
tools.shutdown().await?;
§Implement an executable caller Toolset
This runnable caller implementation executes a bounded string echo without tasks, networking, or asynchronous cleanup. It has one unique terminal handler per call, validates its advertised input shape itself, and seals root/handler admission on shared shutdown. More complex implementations must own accepted asynchronous work independently of a shutdown waiter. Neither this Toolset nor its Handler is Clone.
Direct use keeps the concrete associated child. Moving the same Toolset into
Toolset::from_backend is optional; normalization does not supply missing
backend validation, cancellation, or lifecycle behavior.
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use tools_kit::{ToolError, ToolHandlerBackend, Toolset, ToolsetBackend};
use tools_kit::declaration::{ToolInfo, ToolName};
use tools_kit::execution::{
ToolContent, ToolHandlerInput, ToolHandlerOutput, ToolInvocation, ToolResult,
};
use tools_kit::schema::JsonSchema;
struct EchoTools { info: ToolInfo, closed: Arc<AtomicBool> }
struct EchoHandler { name: ToolName, closed: Arc<AtomicBool>, terminal: bool }
impl EchoTools {
fn check(&self) -> Result<(), ToolError> {
if self.closed.load(Ordering::SeqCst) {
return Err(ToolError::Closed { operation: "echo toolset".into() });
}
Ok(())
}
}
impl ToolsetBackend for EchoTools {
type Handler = EchoHandler;
async fn list_tools(&self) -> Result<Vec<ToolInfo>, ToolError> {
self.check()?;
Ok(vec![self.info.clone()])
}
async fn tool_handler(&self, name: ToolName) -> Result<EchoHandler, ToolError> {
self.check()?;
if name != self.info.name { return Err(ToolError::UnknownTool { name }); }
Ok(EchoHandler { name, closed: Arc::clone(&self.closed), terminal: false })
}
async fn shutdown(&self) -> Result<(), ToolError> {
// No await: this synchronous toolset owns no unfinished external work.
self.closed.store(true, Ordering::SeqCst);
Ok(())
}
}
impl ToolHandlerBackend for EchoHandler {
async fn step(&mut self, input: ToolHandlerInput) -> Result<ToolHandlerOutput, ToolError> {
if self.closed.load(Ordering::SeqCst) {
return Err(ToolError::Closed { operation: "echo step".into() });
}
if self.terminal {
return Err(ToolError::Protocol { message: "handler already terminal".into() });
}
match input {
ToolHandlerInput::Invoke(invocation) => {
let Some(text) = invocation.arguments.as_str().filter(|text| text.chars().count() <= 1024) else {
return Err(ToolError::InvalidArguments {
tool: self.name.clone(), message: "expected a string of at most 1024 characters".into(), issues: Vec::new(),
});
};
self.terminal = true;
Ok(ToolHandlerOutput::Completed(ToolResult::text(text)))
}
ToolHandlerInput::Cancel { reason } => {
self.terminal = true;
Err(ToolError::Cancelled { tool: self.name.clone(), reason })
}
ToolHandlerInput::Interaction(_) => Err(ToolError::Protocol {
message: "this tool never requests interaction".into(),
}),
}
}
}
let name = ToolName::new("echo")?;
let concrete = EchoTools {
info: ToolInfo::new(name.clone(), JsonSchema::strict(serde_json::json!({"type":"string","maxLength":1024}))),
closed: Arc::new(AtomicBool::new(false)),
};
let mut direct = concrete.tool_handler(name.clone()).await?;
let input = ToolInvocation {
call_id: Some("call-1".into()),
arguments: serde_json::json!("unchanged"),
};
let ToolHandlerOutput::Completed(result) = direct.step(ToolHandlerInput::Invoke(input.clone())).await?
else { panic!("echo does not interact") };
assert!(!result.is_error);
assert!(
matches!(result.content.as_slice(), [ToolContent::Text { text, .. }] if text == "unchanged")
);
assert!(direct.step(ToolHandlerInput::Invoke(input.clone())).await.is_err());
let tools = Toolset::from_backend(concrete); // Sole public erased ingress.
let mut normalized = tools.tool_handler(name).await?;
let observed = normalized.step(ToolHandlerInput::Invoke(input)).await;
let cleanup = tools.shutdown().await;
if let Err(error) = &cleanup {
eprintln!("Cleanup failed: {error}");
}
let ToolHandlerOutput::Completed(result) = observed? else { panic!("echo does not interact") };
assert!(
matches!(result.content.as_slice(), [ToolContent::Text { text, .. }] if text == "unchanged")
);
cleanup?;
assert!(tools.list_tools().await.is_err());
tools.shutdown().await?;Required Associated Types§
Sourcetype Handler: ToolHandlerBackend
type Handler: ToolHandlerBackend
Unique resumable handler produced by this tool source.
Required Methods§
Sourcefn list_tools(
&self,
) -> impl Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_
fn list_tools( &self, ) -> impl Future<Output = Result<Vec<ToolInfo>, ToolError>> + Send + '_
Lists the complete executable inventory.
§Errors
Returns ToolError::Closed after sealing, or the underlying inventory failure.
See crate::ToolsetBackend for a complete example.
Sourcefn tool_handler(
&self,
name: ToolName,
) -> impl Future<Output = Result<Self::Handler, ToolError>> + Send + '_
fn tool_handler( &self, name: ToolName, ) -> impl Future<Output = Result<Self::Handler, ToolError>> + Send + '_
Creates a unique handler for one selected tool name.
§Errors
Returns an unknown-name error when no declared tool matches, ToolError::Closed after sealing, or the underlying handler-construction failure.
See crate::local::ToolRegistration for a complete example.
Sourcefn shutdown(&self) -> impl Future<Output = Result<(), ToolError>> + Send + '_
fn shutdown(&self) -> impl Future<Output = Result<(), ToolError>> + Send + '_
Seals admission and performs shared exactly-once cleanup.
This contract applies to direct caller implementations as well as supplied handles. On first acceptance, retain and track the cleanup independently of this awaiting future. Dropping a shutdown waiter or the last public value must not abandon accepted cleanup. Repeated calls observe its terminal result; they must not start another cleanup. Listing and handler admission reject after sealing. A consumer may bound its wait with a deadline, so putting all cleanup solely inside this borrowed future is insufficient.
§Errors
Replays the shared cleanup error, including a cleanup timeout, to concurrent and later callers. A failed observation does not authorize abandoning accepted cleanup.
§Examples
use tools_kit::{Toolset, ToolsetBackend};
let tools = Toolset::local(Vec::new())?;
let shared = tools.clone();
tools.shutdown().await?;
shared.shutdown().await?;
assert!(shared.list_tools().await.is_err());
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".