Skip to main content

ToolsetBackend

Trait ToolsetBackend 

Source
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;
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§

Source

type Handler: ToolHandlerBackend

Unique resumable handler produced by this tool source.

Required Methods§

Source

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.

§Examples

use tools_kit::{Toolset, ToolsetBackend};
let tools = Toolset::local(Vec::new())?;
assert!(tools.list_tools().await?.is_empty());
tools.shutdown().await?;
Source

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.

§Examples

use tools_kit::{declaration::{ToolInfo, ToolName}, schema::JsonSchema};
let info = ToolInfo::new(ToolName::new("ping")?, JsonSchema::strict(serde_json::json!({"type":"object"})));
use tools_kit::{local::ToolRegistration, execution::ToolResult};
let registration = ToolRegistration::function(info, |_| async { Ok(ToolResult::text("pong")) });
use tools_kit::{Toolset, ToolsetBackend, ToolHandlerBackend, execution::{ToolHandlerInput, ToolHandlerOutput, ToolInvocation}};
let tools = Toolset::local(vec![registration])?;
let mut handler = tools.tool_handler(ToolName::new("ping")?).await?;
let result = handler.step(ToolHandlerInput::Invoke(ToolInvocation { call_id: None, arguments: serde_json::json!({}) })).await;
let cleanup = tools.shutdown().await;
assert!(matches!(result?, ToolHandlerOutput::Completed(_)));
cleanup?;
Source

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".

Implementors§