1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use thiserror::Error;
9
10pub type Result<T> = std::result::Result<T, MplError>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
16pub enum MplErrorCode {
17 EQomBreach,
19 ESchemaFidelity,
21 EToolArgCoercion,
23 EPolicyDenied,
25 EUnknownStype,
27 EUnknownTool,
29 ENegotiationIncompatible,
31 EToolOutcomeIncorrect,
33 ESemanticHashMismatch,
35 EInternal,
37}
38
39impl MplErrorCode {
40 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct SchemaError {
190 pub path: String,
192 pub message: String,
194 pub expected: Option<String>,
196 pub actual: Option<String>,
198}
199
200#[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
251pub struct ErrorBuilder {
253 context: Vec<(String, String)>,
254}
255
256impl ErrorBuilder {
257 pub fn new() -> Self {
258 Self { context: Vec::new() }
259 }
260
261 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 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 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
304pub 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
332pub 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}