salvor_tools/error.rs
1//! The two error surfaces of the tool layer: [`HandlerError`], which a tool's
2//! own code returns, and [`ToolError`], which the type-erased dispatch layer
3//! returns.
4//!
5//! Keeping them separate is the point. The runtime loop treats a bad
6//! input from the model differently from a tool that ran and failed: the first
7//! is fed back to the model so it can correct its arguments, the second is
8//! recorded and surfaced per the effect's retry policy. That difference is
9//! encoded as distinct [`ToolError`] variants, not left to string matching.
10
11use thiserror::Error;
12
13/// The error a [`ToolHandler`](crate::ToolHandler) returns from its own code.
14///
15/// A handler is only ever called with an already-typed, already-validated
16/// input, so it never has to report a schema mismatch. What it reports is a
17/// genuine failure of the work it does: a provider rejected the request, a
18/// precondition did not hold, and so on. This type carries a human-readable
19/// message and an optional underlying source error.
20///
21/// The dispatch layer wraps a returned `HandlerError` in
22/// [`ToolError::Handler`], tagging it with the tool name.
23#[derive(Debug, Error)]
24#[error("{message}")]
25pub struct HandlerError {
26 message: String,
27 #[source]
28 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
29}
30
31impl HandlerError {
32 /// Builds a `HandlerError` from a message alone, with no underlying source.
33 pub fn message(message: impl Into<String>) -> Self {
34 Self {
35 message: message.into(),
36 source: None,
37 }
38 }
39
40 /// Builds a `HandlerError` that wraps an underlying error as its source.
41 ///
42 /// The message is taken from the source's `Display` output, so the wrapped
43 /// error's own text is preserved. Use this to forward a `?`-propagated
44 /// error out of a handler: `some_call().map_err(HandlerError::new)?`.
45 pub fn new(source: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
46 let source = source.into();
47 Self {
48 message: source.to_string(),
49 source: Some(source),
50 }
51 }
52}
53
54/// The error the type-erased dispatch layer
55/// ([`DynTool::call_json`](crate::DynTool::call_json)) returns.
56///
57/// The variants are deliberately distinct so the runtime loop can route on them:
58///
59/// - [`ToolError::InvalidInput`] means the JSON the model produced did not
60/// deserialize into the tool's `Input` type. The handler was **not** called.
61/// The loop feeds this back to the model to let it fix its arguments.
62/// - [`ToolError::Handler`] means the handler ran and returned a
63/// [`HandlerError`]. This is a real execution failure, subject to the
64/// effect's [retry policy](crate::RetryPolicy).
65/// - [`ToolError::OutputSerialization`] means the handler succeeded but its
66/// `Output` value could not be serialized to JSON. This is an internal fault
67/// in the tool definition, not the model's doing and not a retryable failure.
68#[derive(Debug, Error)]
69pub enum ToolError {
70 /// The model's JSON input did not match the tool's input schema. The
71 /// handler was never invoked.
72 #[error("input for tool `{tool}` did not match its schema: {source}")]
73 InvalidInput {
74 /// The tool that rejected the input.
75 tool: String,
76 /// The deserialization error describing why the input was rejected.
77 #[source]
78 source: serde_json::Error,
79 },
80 /// The handler ran and returned a failure.
81 #[error("tool `{tool}` failed")]
82 Handler {
83 /// The tool that failed.
84 tool: String,
85 /// The handler's own error.
86 #[source]
87 source: HandlerError,
88 },
89 /// The handler succeeded but its output could not be serialized to JSON.
90 #[error("tool `{tool}` produced output that could not be serialized: {source}")]
91 OutputSerialization {
92 /// The tool whose output failed to serialize.
93 tool: String,
94 /// The serialization error.
95 #[source]
96 source: serde_json::Error,
97 },
98}