1use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use thiserror::Error;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum ProtocolErrorCode {
30 ParseError = -32700,
32
33 InvalidRequest = -32600,
35
36 MethodNotFound = -32601,
38
39 InvalidParams = -32602,
41
42 InternalError = -32603,
44
45 ToolNotFound = -32803,
47}
48
49impl ProtocolErrorCode {
50 pub fn code(&self) -> i32 {
52 *self as i32
53 }
54}
55
56impl std::fmt::Display for ProtocolErrorCode {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Self::ParseError => write!(f, "Parse error"),
60 Self::InvalidRequest => write!(f, "Invalid request"),
61 Self::MethodNotFound => write!(f, "Method not found"),
62 Self::InvalidParams => write!(f, "Invalid params"),
63 Self::InternalError => write!(f, "Internal error"),
64 Self::ToolNotFound => write!(f, "Tool not found"),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Error)]
78#[error("[{code}] {message}")]
79pub struct ProtocolError {
80 pub code: ProtocolErrorCode,
82
83 pub message: String,
85
86 pub data: Option<serde_json::Value>,
88}
89
90impl ProtocolError {
91 pub fn new(code: ProtocolErrorCode, message: impl Into<String>) -> Self {
93 Self {
94 code,
95 message: message.into(),
96 data: None,
97 }
98 }
99
100 pub fn with_data(mut self, data: serde_json::Value) -> Self {
102 self.data = Some(data);
103 self
104 }
105
106 pub fn tool_not_found(tool_name: &str) -> Self {
108 Self::new(
109 ProtocolErrorCode::ToolNotFound,
110 format!("Tool not found: {}", tool_name),
111 )
112 }
113
114 pub fn invalid_params(message: impl Into<String>) -> Self {
116 Self::new(ProtocolErrorCode::InvalidParams, message)
117 }
118
119 pub fn parse_error(message: impl Into<String>) -> Self {
121 Self::new(ProtocolErrorCode::ParseError, message)
122 }
123
124 pub fn method_not_found(method: &str) -> Self {
126 Self::new(
127 ProtocolErrorCode::MethodNotFound,
128 format!("Method not found: {}", method),
129 )
130 }
131
132 pub fn is_protocol_error(&self) -> bool {
134 true }
136
137 pub fn json_rpc_code(&self) -> i32 {
139 self.code.code()
140 }
141}
142
143#[derive(Debug, Clone, Error, Serialize, Deserialize)]
153#[error("[{code}] {message}")]
154pub struct SisterError {
155 pub code: ErrorCode,
157
158 pub severity: Severity,
160
161 pub message: String,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub context: Option<HashMap<String, serde_json::Value>>,
167
168 pub recoverable: bool,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub suggested_action: Option<SuggestedAction>,
174}
175
176impl SisterError {
177 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
179 let severity = code.default_severity();
180 let recoverable = code.is_typically_recoverable();
181
182 Self {
183 code,
184 severity,
185 message: message.into(),
186 context: None,
187 recoverable,
188 suggested_action: None,
189 }
190 }
191
192 pub fn with_context(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
194 let context = self.context.get_or_insert_with(HashMap::new);
195 if let Ok(v) = serde_json::to_value(value) {
196 context.insert(key.into(), v);
197 }
198 self
199 }
200
201 pub fn recoverable(mut self, recoverable: bool) -> Self {
203 self.recoverable = recoverable;
204 self
205 }
206
207 pub fn with_suggestion(mut self, action: SuggestedAction) -> Self {
209 self.suggested_action = Some(action);
210 self
211 }
212
213 pub fn with_severity(mut self, severity: Severity) -> Self {
215 self.severity = severity;
216 self
217 }
218
219 pub fn to_mcp_message(&self) -> String {
224 let mut msg = format!("Error: {}", self.message);
225 if let Some(ref action) = self.suggested_action {
226 match action {
227 SuggestedAction::Retry { after_ms } => {
228 msg.push_str(&format!(". Retry after {}ms", after_ms));
229 }
230 SuggestedAction::Alternative { description } => {
231 msg.push_str(&format!(". Try: {}", description));
232 }
233 SuggestedAction::UserAction { description } => {
234 msg.push_str(&format!(". User action needed: {}", description));
235 }
236 SuggestedAction::Restart => {
237 msg.push_str(". Try restarting the sister");
238 }
239 SuggestedAction::CheckConfig { key } => {
240 msg.push_str(&format!(". Check config key: {}", key));
241 }
242 SuggestedAction::ReportBug => {
243 msg.push_str(". This may be a bug — please report it");
244 }
245 }
246 }
247 msg
248 }
249
250 pub fn not_found(resource: impl Into<String>) -> Self {
256 let resource = resource.into();
257 Self::new(ErrorCode::NotFound, format!("{} not found", resource)).with_suggestion(
258 SuggestedAction::Alternative {
259 description: "Check the ID or use a query/list tool to find available items".into(),
260 },
261 )
262 }
263
264 pub fn invalid_input(message: impl Into<String>) -> Self {
266 Self::new(ErrorCode::InvalidInput, message)
267 }
268
269 pub fn permission_denied(message: impl Into<String>) -> Self {
271 Self::new(ErrorCode::PermissionDenied, message).recoverable(false)
272 }
273
274 pub fn internal(message: impl Into<String>) -> Self {
276 Self::new(ErrorCode::Internal, message)
277 .with_severity(Severity::Fatal)
278 .recoverable(false)
279 .with_suggestion(SuggestedAction::ReportBug)
280 }
281
282 pub fn storage(message: impl Into<String>) -> Self {
284 Self::new(ErrorCode::StorageError, message)
285 .with_suggestion(SuggestedAction::Retry { after_ms: 1000 })
286 }
287
288 pub fn context_not_found(context_id: impl Into<String>) -> Self {
290 Self::new(
291 ErrorCode::ContextNotFound,
292 format!("Context {} not found", context_id.into()),
293 )
294 .with_suggestion(SuggestedAction::Alternative {
295 description: "List available contexts/sessions or create a new one".into(),
296 })
297 }
298
299 pub fn evidence_not_found(evidence_id: impl Into<String>) -> Self {
301 Self::new(
302 ErrorCode::EvidenceNotFound,
303 format!("Evidence {} not found", evidence_id.into()),
304 )
305 .recoverable(false)
306 }
307}
308
309impl Default for SisterError {
310 fn default() -> Self {
311 Self::new(ErrorCode::Internal, "Unknown error")
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
317#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
318pub enum ErrorCode {
319 NotFound,
324
325 InvalidInput,
327
328 PermissionDenied,
330
331 StorageError,
333
334 NetworkError,
336
337 Timeout,
339
340 ResourceExhausted,
342
343 Internal,
345
346 NotImplemented,
348
349 ContextNotFound,
351
352 EvidenceNotFound,
354
355 GroundingFailed,
357
358 VersionMismatch,
360
361 ChecksumMismatch,
363
364 AlreadyExists,
366
367 InvalidState,
369
370 MemoryError,
375
376 VisionError,
378
379 CodebaseError,
381
382 IdentityError,
384
385 TimeError,
387
388 ContractError,
390}
391
392impl ErrorCode {
393 pub fn default_severity(&self) -> Severity {
395 match self {
396 Self::Internal | Self::ChecksumMismatch => Severity::Fatal,
397 Self::PermissionDenied | Self::VersionMismatch => Severity::Error,
398 Self::NotFound | Self::InvalidInput | Self::AlreadyExists => Severity::Error,
399 Self::Timeout | Self::NetworkError | Self::StorageError => Severity::Error,
400 Self::ResourceExhausted => Severity::Warning,
401 _ => Severity::Error,
402 }
403 }
404
405 pub fn is_typically_recoverable(&self) -> bool {
407 match self {
408 Self::Internal | Self::ChecksumMismatch | Self::VersionMismatch => false,
409 Self::NotFound | Self::EvidenceNotFound => true, Self::Timeout | Self::NetworkError | Self::StorageError => true, Self::ResourceExhausted => true, Self::InvalidInput | Self::InvalidState => true, Self::AlreadyExists => true, _ => true,
415 }
416 }
417}
418
419impl std::fmt::Display for ErrorCode {
420 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 let s = match self {
422 Self::NotFound => "NOT_FOUND",
423 Self::InvalidInput => "INVALID_INPUT",
424 Self::PermissionDenied => "PERMISSION_DENIED",
425 Self::StorageError => "STORAGE_ERROR",
426 Self::NetworkError => "NETWORK_ERROR",
427 Self::Timeout => "TIMEOUT",
428 Self::ResourceExhausted => "RESOURCE_EXHAUSTED",
429 Self::Internal => "INTERNAL",
430 Self::NotImplemented => "NOT_IMPLEMENTED",
431 Self::ContextNotFound => "CONTEXT_NOT_FOUND",
432 Self::EvidenceNotFound => "EVIDENCE_NOT_FOUND",
433 Self::GroundingFailed => "GROUNDING_FAILED",
434 Self::VersionMismatch => "VERSION_MISMATCH",
435 Self::ChecksumMismatch => "CHECKSUM_MISMATCH",
436 Self::AlreadyExists => "ALREADY_EXISTS",
437 Self::InvalidState => "INVALID_STATE",
438 Self::MemoryError => "MEMORY_ERROR",
439 Self::VisionError => "VISION_ERROR",
440 Self::CodebaseError => "CODEBASE_ERROR",
441 Self::IdentityError => "IDENTITY_ERROR",
442 Self::TimeError => "TIME_ERROR",
443 Self::ContractError => "CONTRACT_ERROR",
444 };
445 write!(f, "{}", s)
446 }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
451#[serde(rename_all = "snake_case")]
452pub enum Severity {
453 Info,
455
456 Warning,
458
459 Error,
461
462 Fatal,
464}
465
466impl std::fmt::Display for Severity {
467 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 match self {
469 Self::Info => write!(f, "info"),
470 Self::Warning => write!(f, "warning"),
471 Self::Error => write!(f, "error"),
472 Self::Fatal => write!(f, "fatal"),
473 }
474 }
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479#[serde(tag = "type", rename_all = "snake_case")]
480pub enum SuggestedAction {
481 Retry {
483 after_ms: u64,
485 },
486
487 Alternative {
489 description: String,
491 },
492
493 UserAction {
495 description: String,
497 },
498
499 Restart,
501
502 CheckConfig {
504 key: String,
506 },
507
508 ReportBug,
510}
511
512impl From<std::io::Error> for SisterError {
515 fn from(e: std::io::Error) -> Self {
516 SisterError::new(ErrorCode::StorageError, format!("I/O error: {}", e))
517 .with_context("io_error_kind", format!("{:?}", e.kind()))
518 .with_suggestion(SuggestedAction::Retry { after_ms: 1000 })
519 }
520}
521
522impl From<serde_json::Error> for SisterError {
523 fn from(e: serde_json::Error) -> Self {
524 SisterError::new(ErrorCode::InvalidInput, format!("JSON error: {}", e))
525 }
526}
527
528pub type SisterResult<T> = Result<T, SisterError>;
530
531pub type ProtocolResult<T> = Result<T, ProtocolError>;
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 #[test]
539 fn test_error_creation() {
540 let err = SisterError::not_found("node_123");
541 assert_eq!(err.code, ErrorCode::NotFound);
542 assert!(err.recoverable);
543 assert!(err.message.contains("node_123"));
544 }
545
546 #[test]
547 fn test_error_with_context() {
548 let err = SisterError::invalid_input("bad param")
549 .with_context("field", "name")
550 .with_context("provided", "");
551
552 assert!(err.context.is_some());
553 let ctx = err.context.unwrap();
554 assert_eq!(ctx.get("field").unwrap(), "name");
555 }
556
557 #[test]
558 fn test_error_serialization() {
559 let err = SisterError::not_found("test");
560 let json = serde_json::to_string(&err).unwrap();
561 assert!(json.contains("NOT_FOUND"));
562
563 let recovered: SisterError = serde_json::from_str(&json).unwrap();
564 assert_eq!(recovered.code, ErrorCode::NotFound);
565 }
566
567 #[test]
568 fn test_protocol_error_codes() {
569 let err = ProtocolError::tool_not_found("memory_foo");
570 assert_eq!(err.json_rpc_code(), -32803);
571 assert!(err.is_protocol_error());
572 assert!(err.message.contains("memory_foo"));
573
574 let err2 = ProtocolError::invalid_params("missing field: claim");
575 assert_eq!(err2.json_rpc_code(), -32602);
576
577 let err3 = ProtocolError::method_not_found("tools/unknown");
578 assert_eq!(err3.json_rpc_code(), -32601);
579 }
580
581 #[test]
582 fn test_mcp_message_formatting() {
583 let err = SisterError::not_found("node 42");
584 let msg = err.to_mcp_message();
585 assert!(msg.contains("node 42 not found"));
586 assert!(msg.contains("Try:"));
587
588 let err2 = SisterError::storage("disk full");
589 let msg2 = err2.to_mcp_message();
590 assert!(msg2.contains("Retry after"));
591 }
592
593 #[test]
594 fn test_protocol_error_code_values() {
595 assert_eq!(ProtocolErrorCode::ParseError.code(), -32700);
597 assert_eq!(ProtocolErrorCode::InvalidRequest.code(), -32600);
598 assert_eq!(ProtocolErrorCode::MethodNotFound.code(), -32601);
599 assert_eq!(ProtocolErrorCode::InvalidParams.code(), -32602);
600 assert_eq!(ProtocolErrorCode::InternalError.code(), -32603);
601 assert_eq!(ProtocolErrorCode::ToolNotFound.code(), -32803);
602 }
603}