Skip to main content

runifold_tool/
error.rs

1use std::collections::BTreeMap;
2
3use runifold_core::RetrySafety;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7
8/// Normalized tool failure category.
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[non_exhaustive]
11pub enum ToolErrorKind {
12    /// The requested tool is not registered.
13    NotFound,
14    /// Input failed tool-level validation.
15    InvalidInput,
16    /// The owning run was not granted the tool capability.
17    CapabilityDenied,
18    /// Execution was cancelled.
19    Cancelled,
20    /// The invocation deadline elapsed.
21    DeadlineExceeded,
22    /// Tool implementation failed.
23    Execution,
24    /// Tool output violated its declared contract.
25    InvalidOutput,
26}
27
28/// Structured tool execution error.
29#[derive(Clone, Debug, Deserialize, Error, PartialEq, Serialize)]
30#[error("{kind:?}: {message}")]
31pub struct ToolError {
32    /// Normalized category.
33    pub kind: ToolErrorKind,
34    /// Safe human-readable explanation.
35    pub message: String,
36    /// Retry-safety classification.
37    pub retry_safety: RetrySafety,
38    /// Namespaced diagnostic metadata.
39    pub metadata: BTreeMap<String, Value>,
40}
41
42impl ToolError {
43    /// Returns a stable diagnostic identifier without exposing error payloads.
44    ///
45    /// Resolve causes and corrective actions with `runifold ai explain <code>`.
46    /// This does not change the error's retry safety, Display or serialization.
47    pub const fn diagnostic_code(&self) -> &'static str {
48        match self.kind {
49            ToolErrorKind::NotFound => "RF-TOOL-001",
50            ToolErrorKind::InvalidInput => "RF-TOOL-002",
51            ToolErrorKind::CapabilityDenied => "RF-TOOL-003",
52            ToolErrorKind::Cancelled => "RF-TOOL-004",
53            ToolErrorKind::DeadlineExceeded => "RF-TOOL-005",
54            ToolErrorKind::Execution => "RF-TOOL-006",
55            ToolErrorKind::InvalidOutput => "RF-TOOL-007",
56        }
57    }
58
59    /// Creates a local tool error with unknown retry safety.
60    pub fn local(kind: ToolErrorKind, message: impl Into<String>) -> Self {
61        Self {
62            kind,
63            message: message.into(),
64            retry_safety: RetrySafety::Unknown,
65            metadata: BTreeMap::new(),
66        }
67    }
68}
69
70/// Explicit conversion from an application error into a safe Tool failure.
71///
72/// Implementations decide which message is safe for model and operator
73/// visibility, as well as the normalized kind and retry-safety classification.
74pub trait IntoToolError {
75    /// Converts this application failure into the canonical Tool error.
76    fn into_tool_error(self) -> ToolError;
77}
78
79impl IntoToolError for ToolError {
80    fn into_tool_error(self) -> ToolError {
81        self
82    }
83}
84
85/// Failure to add a tool to a registry.
86#[derive(Clone, Debug, Eq, Error, PartialEq)]
87#[non_exhaustive]
88pub enum ToolRegistrationError {
89    /// Tool names must not be blank.
90    #[error("tool name cannot be empty")]
91    EmptyName,
92    /// A different tool already owns this model-facing name.
93    #[error("tool `{0}` is already registered")]
94    DuplicateName(String),
95    /// A declared input or output schema could not be compiled.
96    #[error("tool `{tool}` has an invalid {direction} schema: {message}")]
97    InvalidSchema {
98        /// Tool name.
99        tool: String,
100        /// Schema direction (`input` or `output`).
101        direction: &'static str,
102        /// Safe compiler diagnostic.
103        message: String,
104    },
105}