Skip to main content

ToolHandler

Trait ToolHandler 

Source
pub trait ToolHandler:
    ToolMeta
    + Send
    + Sync {
    type Input: DeserializeOwned + JsonSchema + Send;
    type Output: Serialize;

    // Required method
    fn call<'life0, 'life1, 'async_trait>(
        &'life0 self,
        ctx: &'life1 ToolCtx,
        input: Self::Input,
    ) -> Pin<Box<dyn Future<Output = Result<ToolOutcome<Self::Output>, HandlerError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;

    // Provided methods
    fn input_schema() -> Value
       where Self: Sized { ... }
    fn output_schema() -> Option<Value>
       where Self: Sized { ... }
    fn idempotency_key(&self, input: &Self::Input) -> Option<String> { ... }
}
Expand description

The behavior half of the tool contract: typed input, typed output, and the async call that turns one into the other.

A type implements this by hand (the macro only generates its ToolMeta supertrait; see the derive seam). The associated types carry the bounds the rest of the system needs:

  • Input: DeserializeOwned so the type-erased layer can turn the model’s JSON into it, and Input: JsonSchema so a schema can be generated to hand the model in the first place.
  • Output: Serialize so the type-erased layer can turn the handler’s result back into JSON for the event log.

The Send/Sync bounds and the Send bound on Input are what let a handler be wrapped as a DynTool and dispatched from an async runtime that may move the work across threads.

call returns a ToolOutcome, not a bare Output: a human-in-the-loop tool may return ToolOutcome::Suspend to park the run. Its error type is HandlerError, the tool’s own failure; a schema mismatch is impossible here because call only ever receives an already-deserialized Input.

Required Associated Types§

Source

type Input: DeserializeOwned + JsonSchema + Send

The typed input the model must supply. DeserializeOwned drives erased dispatch; JsonSchema drives the schema handed to the model.

Source

type Output: Serialize

The typed output the tool produces on success.

Required Methods§

Source

fn call<'life0, 'life1, 'async_trait>( &'life0 self, ctx: &'life1 ToolCtx, input: Self::Input, ) -> Pin<Box<dyn Future<Output = Result<ToolOutcome<Self::Output>, HandlerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Runs the tool for one attempt.

ctx carries the idempotency key for this attempt (see ToolCtx). The input is already validated and typed. Return ToolOutcome::Output on success, ToolOutcome::Suspend to park the run for a human, or Err(HandlerError) on a genuine failure.

Provided Methods§

Source

fn input_schema() -> Value
where Self: Sized,

The JSON Schema for Input, generated by schemars.

This is a provided method: it lives on the handler trait because it needs the Input type, and no tool should override it. The runtime hands this schema to the model so the model knows how to shape its tool call. It is surfaced without erasure here and, after wrapping, through DynTool::input_schema.

Source

fn output_schema() -> Option<Value>
where Self: Sized,

The JSON Schema a completion for this tool must satisfy, if the tool declares one. None by default.

This is not a schema for Output, and nothing validates against it yet: stage one only lets a tool declare it. What it is for is the stage that follows. A tool call performed outside the salvor process (a client recording work it did itself, rather than salvor dispatching it) has no handler run to trust; the only thing standing between that and a bare unverifiable claim of “I did the work” is a schema the completion is required to match, one shaped to demand something a forger cannot cheaply produce, such as a provider transaction reference or a settlement id. Declaring the schema here is what makes that requirement expressible; nothing in this crate checks a completion against it yet.

Self::Output carries no JsonSchema bound (unlike Self::Input, which does), so this cannot default to deriving one: adding that bound would break every existing implementor of this trait outside this crate, most of which have no need for an output schema at all. A tool whose Output does implement JsonSchema can opt in with one line:

fn output_schema() -> Option<Value> {
    Some(serde_json::to_value(schemars::schema_for!(Self::Output)).unwrap())
}
Source

fn idempotency_key(&self, input: &Self::Input) -> Option<String>

The idempotency key this tool declares for input, if it declares one. None by default.

This is the typed half of DynTool::idempotency_key, which is where the full argument lives; the wrapper forwards to this method with the input already deserialized. It takes &self rather than being an associated function so a tool configured at construction (an account, a tenant, an environment) can fold that into the key.

The key must be a pure function of input and self, and it should be as specific as the effect it names:

fn idempotency_key(&self, input: &PayClaim) -> Option<String> {
    Some(format!("pay_claim:{}", input.claim_id))
}

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§