mpl_core/
error.rs

1//! MPL Error Taxonomy
2//!
3//! Typed errors that distinguish semantic failures from transport issues.
4//! Each error includes hints for remediation.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use thiserror::Error;
9
10/// Result type for MPL operations
11pub type Result<T> = std::result::Result<T, MplError>;
12
13/// MPL error codes following the protocol specification
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
16pub enum MplErrorCode {
17    /// QoM metric(s) failed to meet negotiated thresholds
18    EQomBreach,
19    /// Payload failed JSON Schema validation
20    ESchemaFidelity,
21    /// Tool arguments could not be coerced to declared args_stype
22    EToolArgCoercion,
23    /// Request violated negotiated policy
24    EPolicyDenied,
25    /// Referenced SType not found in registry
26    EUnknownStype,
27    /// Referenced tool not available
28    EUnknownTool,
29    /// Handshake failed - no compatible capability set
30    ENegotiationIncompatible,
31    /// Tool outcome verification failed
32    EToolOutcomeIncorrect,
33    /// Semantic hash mismatch detected
34    ESemanticHashMismatch,
35    /// Internal error
36    EInternal,
37}
38
39impl MplErrorCode {
40    /// Get the string representation of the error code
41    pub fn as_str(&self) -> &'static str {
42        match self {
43            Self::EQomBreach => "E-QOM-BREACH",
44            Self::ESchemaFidelity => "E-SCHEMA-FIDELITY",
45            Self::EToolArgCoercion => "E-TOOL-ARG-COERCION",
46            Self::EPolicyDenied => "E-POLICY-DENIED",
47            Self::EUnknownStype => "E-UNKNOWN-STYPE",
48            Self::EUnknownTool => "E-UNKNOWN-TOOL",
49            Self::ENegotiationIncompatible => "E-NEGOTIATION-INCOMPATIBLE",
50            Self::EToolOutcomeIncorrect => "E-TOOL-OUTCOME-INCORRECT",
51            Self::ESemanticHashMismatch => "E-SEMANTIC-HASH-MISMATCH",
52            Self::EInternal => "E-INTERNAL",
53        }
54    }
55}
56
57impl std::fmt::Display for MplErrorCode {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "{}", self.as_str())
60    }
61}
62
63/// MPL Error type
64#[derive(Debug, Error)]
65pub enum MplError {
66    #[error("QoM breach: {message}")]
67    QomBreach {
68        message: String,
69        metrics: HashMap<String, f64>,
70        thresholds: HashMap<String, f64>,
71        hints: Vec<String>,
72    },
73
74    #[error("Schema validation failed: {message}")]
75    SchemaFidelity {
76        message: String,
77        stype: String,
78        errors: Vec<SchemaError>,
79        hints: Vec<String>,
80    },
81
82    #[error("Tool argument coercion failed: {message}")]
83    ToolArgCoercion {
84        message: String,
85        tool_id: String,
86        expected_stype: String,
87        hints: Vec<String>,
88    },
89
90    #[error("Policy denied: {message}")]
91    PolicyDenied {
92        message: String,
93        policy_ref: String,
94        hints: Vec<String>,
95    },
96
97    #[error("Unknown SType: {stype}")]
98    UnknownStype {
99        stype: String,
100        suggestions: Vec<String>,
101    },
102
103    #[error("Unknown tool: {tool_id}")]
104    UnknownTool {
105        tool_id: String,
106        available: Vec<String>,
107    },
108
109    #[error("Negotiation incompatible: {message}")]
110    NegotiationIncompatible {
111        message: String,
112        client_capabilities: Vec<String>,
113        server_capabilities: Vec<String>,
114    },
115
116    #[error("Invalid SType format: {stype} - {reason}")]
117    InvalidSType { stype: String, reason: String },
118
119    #[error("Semantic hash mismatch: expected {expected}, got {actual}")]
120    SemanticHashMismatch { expected: String, actual: String },
121
122    #[error("Validation error: {0}")]
123    Validation(String),
124
125    #[error("Serialization error: {0}")]
126    Serialization(#[from] serde_json::Error),
127
128    #[error("IO error: {0}")]
129    Io(#[from] std::io::Error),
130
131    #[error("Internal error: {0}")]
132    Internal(String),
133}
134
135impl MplError {
136    /// Get the error code for this error
137    pub fn code(&self) -> MplErrorCode {
138        match self {
139            Self::QomBreach { .. } => MplErrorCode::EQomBreach,
140            Self::SchemaFidelity { .. } => MplErrorCode::ESchemaFidelity,
141            Self::ToolArgCoercion { .. } => MplErrorCode::EToolArgCoercion,
142            Self::PolicyDenied { .. } => MplErrorCode::EPolicyDenied,
143            Self::UnknownStype { .. } | Self::InvalidSType { .. } => MplErrorCode::EUnknownStype,
144            Self::UnknownTool { .. } => MplErrorCode::EUnknownTool,
145            Self::NegotiationIncompatible { .. } => MplErrorCode::ENegotiationIncompatible,
146            Self::SemanticHashMismatch { .. } => MplErrorCode::ESemanticHashMismatch,
147            Self::Validation(_) | Self::Serialization(_) | Self::Io(_) | Self::Internal(_) => {
148                MplErrorCode::EInternal
149            }
150        }
151    }
152
153    /// Get remediation hints for this error
154    pub fn hints(&self) -> Vec<String> {
155        match self {
156            Self::QomBreach { hints, .. } => hints.clone(),
157            Self::SchemaFidelity { hints, .. } => hints.clone(),
158            Self::ToolArgCoercion { hints, .. } => hints.clone(),
159            Self::PolicyDenied { hints, .. } => hints.clone(),
160            Self::UnknownStype { suggestions, .. } => {
161                if suggestions.is_empty() {
162                    vec!["Register the SType in the registry or check for typos".to_string()]
163                } else {
164                    suggestions
165                        .iter()
166                        .map(|s| format!("Did you mean: {}", s))
167                        .collect()
168                }
169            }
170            Self::UnknownTool { available, .. } => {
171                vec![format!("Available tools: {}", available.join(", "))]
172            }
173            Self::NegotiationIncompatible { .. } => {
174                vec!["Check protocol versions and capability sets".to_string()]
175            }
176            Self::InvalidSType { .. } => {
177                vec!["Format: namespace.domain.Name.vN (e.g., org.calendar.Event.v1)".to_string()]
178            }
179            Self::SemanticHashMismatch { .. } => {
180                vec!["Payload may have been modified in transit".to_string()]
181            }
182            _ => vec![],
183        }
184    }
185}
186
187/// Individual schema validation error
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct SchemaError {
190    /// JSON path to the error (e.g., "/start")
191    pub path: String,
192    /// Error message
193    pub message: String,
194    /// Expected type/value
195    pub expected: Option<String>,
196    /// Actual type/value found
197    pub actual: Option<String>,
198}
199
200/// Structured error response for wire format
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct MplErrorResponse {
203    pub code: String,
204    pub message: String,
205    pub hints: Vec<String>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub details: Option<serde_json::Value>,
208}
209
210impl From<&MplError> for MplErrorResponse {
211    fn from(err: &MplError) -> Self {
212        let details = match err {
213            MplError::QomBreach {
214                metrics,
215                thresholds,
216                ..
217            } => Some(serde_json::json!({
218                "metrics": metrics,
219                "thresholds": thresholds,
220            })),
221            MplError::SchemaFidelity { stype, errors, .. } => Some(serde_json::json!({
222                "stype": stype,
223                "errors": errors,
224            })),
225            MplError::UnknownStype { stype, suggestions } => Some(serde_json::json!({
226                "stype": stype,
227                "suggestions": suggestions,
228            })),
229            MplError::NegotiationIncompatible { client_capabilities, server_capabilities, .. } => {
230                Some(serde_json::json!({
231                    "client_capabilities": client_capabilities,
232                    "server_capabilities": server_capabilities,
233                }))
234            }
235            MplError::InvalidSType { stype, reason } => Some(serde_json::json!({
236                "stype": stype,
237                "reason": reason,
238            })),
239            _ => None,
240        };
241
242        Self {
243            code: err.code().as_str().to_string(),
244            message: err.to_string(),
245            hints: err.hints(),
246            details,
247        }
248    }
249}
250
251/// Builder for constructing detailed errors with context
252pub struct ErrorBuilder {
253    context: Vec<(String, String)>,
254}
255
256impl ErrorBuilder {
257    pub fn new() -> Self {
258        Self { context: Vec::new() }
259    }
260
261    /// Add context to the error
262    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
263        self.context.push((key.into(), value.into()));
264        self
265    }
266
267    /// Build a validation error with context
268    pub fn validation_error(self, message: impl Into<String>) -> MplError {
269        let msg = message.into();
270        let context_str = if self.context.is_empty() {
271            String::new()
272        } else {
273            let pairs: Vec<String> = self.context
274                .iter()
275                .map(|(k, v)| format!("{}={}", k, v))
276                .collect();
277            format!(" [{}]", pairs.join(", "))
278        };
279        MplError::Validation(format!("{}{}", msg, context_str))
280    }
281
282    /// Build an internal error with context
283    pub fn internal_error(self, message: impl Into<String>) -> MplError {
284        let msg = message.into();
285        let context_str = if self.context.is_empty() {
286            String::new()
287        } else {
288            let pairs: Vec<String> = self.context
289                .iter()
290                .map(|(k, v)| format!("{}={}", k, v))
291                .collect();
292            format!(" [{}]", pairs.join(", "))
293        };
294        MplError::Internal(format!("{}{}", msg, context_str))
295    }
296}
297
298impl Default for ErrorBuilder {
299    fn default() -> Self {
300        Self::new()
301    }
302}
303
304/// Helper to create a schema fidelity error with detailed context
305pub fn schema_error(stype: &str, errors: Vec<SchemaError>) -> MplError {
306    let error_summary = if errors.len() == 1 {
307        errors[0].message.clone()
308    } else {
309        format!("{} validation errors", errors.len())
310    };
311
312    let hints: Vec<String> = errors
313        .iter()
314        .take(3)
315        .map(|e| {
316            if let (Some(expected), Some(actual)) = (&e.expected, &e.actual) {
317                format!("At {}: expected {}, got {}", e.path, expected, actual)
318            } else {
319                format!("At {}: {}", e.path, e.message)
320            }
321        })
322        .collect();
323
324    MplError::SchemaFidelity {
325        message: error_summary,
326        stype: stype.to_string(),
327        errors,
328        hints,
329    }
330}
331
332/// Helper to create a QoM breach error with metrics context
333pub fn qom_breach_error(
334    profile: &str,
335    metrics: HashMap<String, f64>,
336    thresholds: HashMap<String, f64>,
337) -> MplError {
338    let failed_metrics: Vec<String> = thresholds
339        .iter()
340        .filter_map(|(name, threshold)| {
341            let value = metrics.get(name).unwrap_or(&0.0);
342            if value < threshold {
343                Some(format!("{}: {} < {} (threshold)", name, value, threshold))
344            } else {
345                None
346            }
347        })
348        .collect();
349
350    let message = format!(
351        "Profile '{}' requirements not met: {}",
352        profile,
353        failed_metrics.join("; ")
354    );
355
356    let hints = vec![
357        "Check instruction compliance by validating agent behavior".to_string(),
358        "Verify schema fidelity by ensuring payload matches schema".to_string(),
359        format!("Consider using a less strict profile than '{}'", profile),
360    ];
361
362    MplError::QomBreach {
363        message,
364        metrics,
365        thresholds,
366        hints,
367    }
368}