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: DeserializeOwnedso the type-erased layer can turn the model’s JSON into it, andInput: JsonSchemaso a schema can be generated to hand the model in the first place.Output: Serializeso 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§
Sourcetype Input: DeserializeOwned + JsonSchema + Send
type Input: DeserializeOwned + JsonSchema + Send
The typed input the model must supply. DeserializeOwned drives erased
dispatch; JsonSchema drives the schema handed to the model.
Required Methods§
Sourcefn 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,
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§
Sourcefn input_schema() -> Valuewhere
Self: Sized,
fn input_schema() -> Valuewhere
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.
Sourcefn output_schema() -> Option<Value>where
Self: Sized,
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())
}Sourcefn idempotency_key(&self, input: &Self::Input) -> Option<String>
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".