Skip to main content

ferrin_tool/
error.rs

1//! Tool-level errors.
2
3use std::time::Duration;
4
5use ferrin_spec::JsonValue;
6use ferrin_spec::ToolName;
7
8type BoxError = Box<dyn std::error::Error + Send + Sync>;
9
10/// Error returned by a tool execution.
11///
12/// Tool errors are not fatal: the core turns them into `error-text` /
13/// `error-json` tool results that are fed back to the model. Only
14/// [`ToolError::Cancelled`] aborts the whole call.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum ToolError {
18    /// A textual error.
19    #[error("{message}")]
20    Message {
21        /// Explanation shown to the model.
22        message: String,
23        /// Underlying error.
24        #[source]
25        cause: Option<BoxError>,
26    },
27    /// A structured error payload.
28    #[error("tool returned error payload")]
29    Json {
30        /// The payload.
31        value: JsonValue,
32    },
33    /// The execution exceeded its timeout.
34    #[error("tool execution timed out after {0:?}")]
35    Timeout(Duration),
36    /// The execution was cancelled.
37    #[error("tool execution cancelled")]
38    Cancelled,
39}
40
41impl ToolError {
42    /// A textual error.
43    #[must_use]
44    pub fn message(message: impl Into<String>) -> Self {
45        Self::Message {
46            message: message.into(),
47            cause: None,
48        }
49    }
50
51    /// A structured error.
52    #[must_use]
53    pub fn json(value: JsonValue) -> Self {
54        Self::Json { value }
55    }
56
57    /// Wraps any error, using its display text as the message.
58    #[must_use]
59    pub fn from_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
60        Self::Message {
61            message: error.to_string(),
62            cause: Some(Box::new(error)),
63        }
64    }
65
66    /// Attaches a cause to a [`ToolError::Message`]; other variants are
67    /// returned unchanged.
68    #[must_use]
69    pub fn with_cause(self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
70        match self {
71            Self::Message { message, .. } => Self::Message {
72                message,
73                cause: Some(Box::new(cause)),
74            },
75            other => other,
76        }
77    }
78
79    /// Returns `true` for [`ToolError::Cancelled`].
80    #[must_use]
81    pub fn is_cancelled(&self) -> bool {
82        matches!(self, Self::Cancelled)
83    }
84}
85
86impl From<serde_json::Error> for ToolError {
87    fn from(error: serde_json::Error) -> Self {
88        Self::from_error(error)
89    }
90}
91
92/// A tool name was inserted twice into a [`crate::ToolSet`].
93#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
94#[error("tool `{name}` is already defined")]
95pub struct DuplicateToolError {
96    /// The duplicated name.
97    pub name: ToolName,
98}