Skip to main content

agentic_contracts/
errors.rs

1//! Standard error types for all sisters.
2//!
3//! Two error layers:
4//!
5//! 1. **ProtocolError** — MCP/JSON-RPC protocol errors (wrong method, bad params, unknown tool).
6//!    These become JSON-RPC error responses.
7//!
8//! 2. **SisterError** — Domain/business logic errors (node not found, invalid state).
9//!    These become `{isError: true}` tool results per MCP spec.
10//!
11//! # MCP Error Handling Rule
12//!
13//! If the tool was found and invoked, errors go through `isError: true`.
14//! JSON-RPC errors are only for protocol/routing failures.
15
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use thiserror::Error;
19
20// ═══════════════════════════════════════════════════════════════════
21// LAYER 1: MCP Protocol Errors (JSON-RPC error responses)
22// ═══════════════════════════════════════════════════════════════════
23
24/// Standard JSON-RPC / MCP protocol error codes.
25///
26/// These are used ONLY for protocol-level failures.
27/// Tool execution errors should use `SisterError` + `isError: true`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum ProtocolErrorCode {
30    /// JSON parse error (-32700)
31    ParseError = -32700,
32
33    /// Invalid JSON-RPC request (-32600)
34    InvalidRequest = -32600,
35
36    /// Method not found (-32601)
37    MethodNotFound = -32601,
38
39    /// Invalid method parameters (-32602)
40    InvalidParams = -32602,
41
42    /// Internal JSON-RPC error (-32603)
43    InternalError = -32603,
44
45    /// Tool not found (-32803) — MCP extension
46    ToolNotFound = -32803,
47}
48
49impl ProtocolErrorCode {
50    /// Get the numeric JSON-RPC error code
51    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/// MCP protocol error — becomes a JSON-RPC error response.
70///
71/// Use this for:
72/// - Parse errors
73/// - Invalid requests
74/// - Unknown methods
75/// - Unknown tools (code -32803, NOT -32602)
76/// - Invalid parameters (before the tool is invoked)
77#[derive(Debug, Clone, Error)]
78#[error("[{code}] {message}")]
79pub struct ProtocolError {
80    /// JSON-RPC error code
81    pub code: ProtocolErrorCode,
82
83    /// Human-readable error message
84    pub message: String,
85
86    /// Optional structured data
87    pub data: Option<serde_json::Value>,
88}
89
90impl ProtocolError {
91    /// Create a new protocol error
92    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    /// Add structured data to the error
101    pub fn with_data(mut self, data: serde_json::Value) -> Self {
102        self.data = Some(data);
103        self
104    }
105
106    /// Create a "tool not found" error (code -32803)
107    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    /// Create an "invalid params" error (code -32602)
115    pub fn invalid_params(message: impl Into<String>) -> Self {
116        Self::new(ProtocolErrorCode::InvalidParams, message)
117    }
118
119    /// Create a "parse error" (code -32700)
120    pub fn parse_error(message: impl Into<String>) -> Self {
121        Self::new(ProtocolErrorCode::ParseError, message)
122    }
123
124    /// Create a "method not found" error (code -32601)
125    pub fn method_not_found(method: &str) -> Self {
126        Self::new(
127            ProtocolErrorCode::MethodNotFound,
128            format!("Method not found: {}", method),
129        )
130    }
131
132    /// Check if this is a protocol-level error (should be JSON-RPC error)
133    pub fn is_protocol_error(&self) -> bool {
134        true // All ProtocolErrors are protocol-level by definition
135    }
136
137    /// Get the numeric error code for JSON-RPC response
138    pub fn json_rpc_code(&self) -> i32 {
139        self.code.code()
140    }
141}
142
143// ═══════════════════════════════════════════════════════════════════
144// LAYER 2: Domain/Business Logic Errors (isError: true in MCP)
145// ═══════════════════════════════════════════════════════════════════
146
147/// Standard error type for ALL sisters — domain/business logic errors.
148///
149/// These errors occur AFTER a tool is found and invoked.
150/// In MCP, they become `{isError: true}` in the tool result,
151/// NOT JSON-RPC error responses.
152#[derive(Debug, Clone, Error, Serialize, Deserialize)]
153#[error("[{code}] {message}")]
154pub struct SisterError {
155    /// Error code (machine-readable)
156    pub code: ErrorCode,
157
158    /// Severity level
159    pub severity: Severity,
160
161    /// Human-readable message (should be actionable for LLMs)
162    pub message: String,
163
164    /// Additional context (for debugging)
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub context: Option<HashMap<String, serde_json::Value>>,
167
168    /// Is this recoverable?
169    pub recoverable: bool,
170
171    /// Suggested action for recovery
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub suggested_action: Option<SuggestedAction>,
174}
175
176impl SisterError {
177    /// Create a new error
178    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    /// Add context to the error
193    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    /// Set recoverable flag
202    pub fn recoverable(mut self, recoverable: bool) -> Self {
203        self.recoverable = recoverable;
204        self
205    }
206
207    /// Set suggested action
208    pub fn with_suggestion(mut self, action: SuggestedAction) -> Self {
209        self.suggested_action = Some(action);
210        self
211    }
212
213    /// Set severity
214    pub fn with_severity(mut self, severity: Severity) -> Self {
215        self.severity = severity;
216        self
217    }
218
219    /// Format as an MCP-friendly error message.
220    ///
221    /// Includes what went wrong AND what to try instead,
222    /// so the LLM can reason about recovery.
223    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    // ═══════════════════════════════════════════════════════════
251    // Common error constructors
252    // ═══════════════════════════════════════════════════════════
253
254    /// Not found error
255    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    /// Invalid input error
265    pub fn invalid_input(message: impl Into<String>) -> Self {
266        Self::new(ErrorCode::InvalidInput, message)
267    }
268
269    /// Permission denied error
270    pub fn permission_denied(message: impl Into<String>) -> Self {
271        Self::new(ErrorCode::PermissionDenied, message).recoverable(false)
272    }
273
274    /// Internal error (bug)
275    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    /// Storage error
283    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    /// Context/session not found error
289    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    /// Evidence not found error
300    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/// Standard error codes across ALL sisters.
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
317#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
318pub enum ErrorCode {
319    // ═══════════════════════════════════════════════════════
320    // COMMON ERRORS (All sisters use these)
321    // ═══════════════════════════════════════════════════════
322    /// Resource not found
323    NotFound,
324
325    /// Invalid input provided
326    InvalidInput,
327
328    /// Operation not permitted
329    PermissionDenied,
330
331    /// Storage error (read/write failed)
332    StorageError,
333
334    /// Network error
335    NetworkError,
336
337    /// Operation timed out
338    Timeout,
339
340    /// Resource limits exceeded
341    ResourceExhausted,
342
343    /// Internal error (bug)
344    Internal,
345
346    /// Not implemented yet
347    NotImplemented,
348
349    /// Context/session not found
350    ContextNotFound,
351
352    /// Evidence not found
353    EvidenceNotFound,
354
355    /// Grounding failed
356    GroundingFailed,
357
358    /// Version mismatch
359    VersionMismatch,
360
361    /// Checksum mismatch (corruption)
362    ChecksumMismatch,
363
364    /// Already exists
365    AlreadyExists,
366
367    /// Invalid state for operation
368    InvalidState,
369
370    // ═══════════════════════════════════════════════════════
371    // SISTER-SPECIFIC ERROR PREFIXES
372    // ═══════════════════════════════════════════════════════
373    /// Memory-specific error
374    MemoryError,
375
376    /// Vision-specific error
377    VisionError,
378
379    /// Codebase-specific error
380    CodebaseError,
381
382    /// Identity-specific error
383    IdentityError,
384
385    /// Time-specific error
386    TimeError,
387
388    /// Contract-specific error
389    ContractError,
390}
391
392impl ErrorCode {
393    /// Get default severity for this error code
394    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    /// Check if this error is typically recoverable
406    pub fn is_typically_recoverable(&self) -> bool {
407        match self {
408            Self::Internal | Self::ChecksumMismatch | Self::VersionMismatch => false,
409            Self::NotFound | Self::EvidenceNotFound => true, // Can try different ID
410            Self::Timeout | Self::NetworkError | Self::StorageError => true, // Can retry
411            Self::ResourceExhausted => true,                 // Can wait
412            Self::InvalidInput | Self::InvalidState => true, // Can fix input
413            Self::AlreadyExists => true,                     // Can use existing
414            _ => 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/// Severity levels
450#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
451#[serde(rename_all = "snake_case")]
452pub enum Severity {
453    /// Informational, not really an error
454    Info,
455
456    /// Warning, operation succeeded but with issues
457    Warning,
458
459    /// Error, operation failed but recoverable
460    Error,
461
462    /// Fatal, sister is in bad state
463    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/// Suggested actions for error recovery
478#[derive(Debug, Clone, Serialize, Deserialize)]
479#[serde(tag = "type", rename_all = "snake_case")]
480pub enum SuggestedAction {
481    /// Retry the operation
482    Retry {
483        /// Milliseconds to wait before retry
484        after_ms: u64,
485    },
486
487    /// Use a different approach
488    Alternative {
489        /// Description of the alternative
490        description: String,
491    },
492
493    /// User intervention needed
494    UserAction {
495        /// Description of what the user should do
496        description: String,
497    },
498
499    /// Restart the sister
500    Restart,
501
502    /// Check configuration
503    CheckConfig {
504        /// Configuration key to check
505        key: String,
506    },
507
508    /// Contact support / report bug
509    ReportBug,
510}
511
512// Implement From for common error types
513
514impl 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
528/// Result type alias for sister operations (domain errors)
529pub type SisterResult<T> = Result<T, SisterError>;
530
531/// Result type alias for protocol operations
532pub 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        // Verify exact JSON-RPC error codes per spec
596        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}