Skip to main content

providers_kit/
error.rs

1//! Structured failures and explicitly accessed, potentially sensitive native diagnostics.
2//!
3//! [`KitError`] formats only classification, schema labels, and numeric context. Native
4//! messages, identifiers and bodies are available through [`KitError::native_details`]
5//! for deliberate inspection; they must not be copied into ordinary logs.
6
7use std::fmt;
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12const DETAIL_STRING_LIMIT: usize = 65_536;
13const DETAIL_TOTAL_LIMIT: usize = 1_048_576;
14const DETAIL_RECORD_LIMIT: usize = 256;
15const ERROR_DEPTH_LIMIT: usize = 8;
16const ERROR_NODE_LIMIT: usize = 32;
17
18/// Evidence used to choose a portable classification; it does not imply retry safety.
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "source", rename_all = "snake_case", deny_unknown_fields)]
21pub enum ClassificationBasis {
22    /// A caller implementation explicitly selected the classification.
23    Explicit,
24    /// The status used for classification, independently of any status in the body.
25    HttpStatus {
26        /// Actual HTTP status used for this classification.
27        status: u16,
28    },
29    /// A recognized native service code.
30    NativeCode,
31    /// A recognized inner native error type, not the envelope or event type.
32    NativeErrorType,
33    /// A recognized canonical status name.
34    CanonicalStatus,
35    /// A recognized numeric RPC status.
36    RpcCode,
37    /// A recognized reason and domain in the first retained reason record.
38    NativeReason,
39    /// A native hosted outcome or an actual nonzero execution exit code.
40    NativeOutcome,
41    /// A remote failure without a more specific supported classification.
42    Unclassified,
43}
44
45/// A Kit-owned input invariant identified without disclosing caller values.
46///
47/// Labels are trusted static schema names and rules, never native diagnostic text.
48/// Caller implementations can attach one with [`KitError::with_validation_violation`].
49///
50/// ```
51/// use providers_kit::error::ValidationViolation;
52/// let violation = ValidationViolation::OutOfRange {
53///     field: "generation.temperature", rule: "finite and non-negative",
54/// };
55/// assert!(format!("{violation:?}").contains("generation.temperature"));
56/// ```
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum ValidationViolation {
59    /// A required field was not supplied.
60    MissingField {
61        /// Trusted field name.
62        field: &'static str,
63    },
64    /// A required value or collection was empty.
65    Empty {
66        /// Trusted field name.
67        field: &'static str,
68    },
69    /// A value did not satisfy its required syntax.
70    InvalidFormat {
71        /// Trusted field name.
72        field: &'static str,
73        /// Trusted syntax rule, without the rejected value.
74        rule: &'static str,
75    },
76    /// A value violated a non-range invariant.
77    InvalidValue {
78        /// Trusted field name.
79        field: &'static str,
80        /// Trusted value constraint.
81        rule: &'static str,
82    },
83    /// A value was outside its permitted range.
84    OutOfRange {
85        /// Trusted field name.
86        field: &'static str,
87        /// Trusted range description.
88        rule: &'static str,
89    },
90    /// A value was repeated where uniqueness is required.
91    Duplicate {
92        /// Trusted field name.
93        field: &'static str,
94    },
95    /// A value or collection exceeded a known limit.
96    LimitExceeded {
97        /// Trusted field name.
98        field: &'static str,
99        /// Maximum permitted count or size.
100        limit: usize,
101    },
102    /// Two fields cannot be used together.
103    Incompatible {
104        /// Trusted field name.
105        field: &'static str,
106        /// Trusted name of the incompatible field.
107        other: &'static str,
108    },
109    /// The selected implementation cannot support the requested input feature.
110    Unsupported {
111        /// Trusted field name.
112        field: &'static str,
113        /// Trusted capability constraint.
114        rule: &'static str,
115    },
116}
117
118/// JSON decoding category, retained independently of a formatted parser message.
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
120pub enum JsonErrorCategory {
121    /// Input/output failed during decoding.
122    Io,
123    /// Invalid JSON syntax.
124    Syntax,
125    /// Valid JSON did not match the expected schema.
126    Data,
127    /// Input ended before a JSON value was complete.
128    Eof,
129}
130
131/// Observed JSON value shape, including the absence of a required field.
132#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
133pub enum JsonValueKind {
134    /// The field is absent.
135    Absent,
136    /// JSON null.
137    Null,
138    /// A JSON boolean.
139    Boolean,
140    /// A JSON number.
141    Number,
142    /// A JSON string.
143    String,
144    /// A JSON array.
145    Array,
146    /// A JSON object.
147    Object,
148}
149
150impl JsonValueKind {
151    pub(crate) fn of(value: Option<&serde_json::Value>) -> Self {
152        match value {
153            None => Self::Absent,
154            Some(serde_json::Value::Null) => Self::Null,
155            Some(serde_json::Value::Bool(_)) => Self::Boolean,
156            Some(serde_json::Value::Number(_)) => Self::Number,
157            Some(serde_json::Value::String(_)) => Self::String,
158            Some(serde_json::Value::Array(_)) => Self::Array,
159            Some(serde_json::Value::Object(_)) => Self::Object,
160        }
161    }
162}
163
164/// Exact response observations, accessed deliberately because values may be sensitive.
165///
166/// The corresponding trusted field name remains in [`ResponseViolation`].
167/// Formatting this value never prints observed strings.
168#[derive(Clone, Eq, PartialEq, Serialize)]
169#[serde(tag = "type", rename_all = "snake_case")]
170pub enum ResponseDetails {
171    /// Parser location and category; line and column use the parser's numbering.
172    Json {
173        /// Parser error category.
174        category: JsonErrorCategory,
175        /// One-based line, when available from the parser.
176        line: usize,
177        /// Parser column, which may be zero for an immediate input failure.
178        column: usize,
179    },
180    /// A field was absent or had the wrong JSON shape.
181    FieldType {
182        /// Required shape.
183        expected: JsonValueKind,
184        /// Observed shape.
185        actual: JsonValueKind,
186    },
187    /// Unrecognized native discriminator text.
188    Discriminator {
189        /// Exact observed discriminator, subject to documented bounds.
190        actual: String,
191    },
192    /// Two identifiers did not agree.
193    Identity {
194        /// Expected identifier.
195        expected: String,
196        /// Received identifier.
197        actual: String,
198    },
199    /// An index was outside the available collection.
200    Index {
201        /// Received index.
202        index: usize,
203        /// Number of available entries.
204        available: usize,
205    },
206    /// Observed bytes when a response limit was exceeded.
207    Size {
208        /// Observed byte count; this may be a lower bound after early termination.
209        observed: usize,
210    },
211}
212
213impl ResponseDetails {
214    pub(crate) fn json(error: &serde_json::Error) -> Self {
215        Self::Json {
216            category: match error.classify() {
217                serde_json::error::Category::Io => JsonErrorCategory::Io,
218                serde_json::error::Category::Syntax => JsonErrorCategory::Syntax,
219                serde_json::error::Category::Data => JsonErrorCategory::Data,
220                serde_json::error::Category::Eof => JsonErrorCategory::Eof,
221            },
222            line: error.line(),
223            column: error.column(),
224        }
225    }
226
227    fn bound(&mut self, budget: &mut DetailBudget) {
228        match self {
229            Self::Discriminator { actual } => budget.string(actual),
230            Self::Identity { expected, actual } => {
231                budget.string(expected);
232                budget.string(actual);
233            }
234            Self::Json { .. } | Self::FieldType { .. } | Self::Index { .. } | Self::Size { .. } => {
235            }
236        }
237    }
238}
239
240impl fmt::Debug for ResponseDetails {
241    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242        match self {
243            Self::Json {
244                category,
245                line,
246                column,
247            } => formatter
248                .debug_struct("Json")
249                .field("category", category)
250                .field("line", line)
251                .field("column", column)
252                .finish(),
253            Self::FieldType { expected, actual } => formatter
254                .debug_struct("FieldType")
255                .field("expected", expected)
256                .field("actual", actual)
257                .finish(),
258            Self::Index { index, available } => formatter
259                .debug_struct("Index")
260                .field("index", index)
261                .field("available", available)
262                .finish(),
263            Self::Size { observed } => formatter
264                .debug_struct("Size")
265                .field("observed", observed)
266                .finish(),
267            Self::Discriminator { .. } => {
268                formatter.write_str("Discriminator { actual: <redacted> }")
269            }
270            Self::Identity { .. } => {
271                formatter.write_str("Identity { expected: <redacted>, actual: <redacted> }")
272            }
273        }
274    }
275}
276
277/// Portable failure classification. This classification does not promise retry safety.
278#[derive(Clone, Copy, Debug, Eq, PartialEq)]
279pub enum ErrorKind {
280    /// Invalid provider configuration.
281    Configuration,
282    /// Invalid input detected before provider execution.
283    InvalidInput,
284    /// The implementation cannot perform the requested operation.
285    Unsupported,
286    /// The connection or transport failed without a more authoritative rejection.
287    Transport,
288    /// A local operation deadline elapsed.
289    Timeout,
290    /// The service reported that its deadline elapsed.
291    DeadlineExceeded,
292    /// Lifecycle admission is closed.
293    Closed,
294    /// The operation is incompatible with the current local state.
295    InvalidState,
296    /// The response violates a known schema or protocol invariant.
297    InvalidResponse(ResponseViolation),
298    /// The service rejected the request as invalid.
299    InvalidRequest,
300    /// Authentication was rejected.
301    Authentication,
302    /// The authenticated caller lacks permission.
303    PermissionDenied,
304    /// A requested model or resource was not found.
305    NotFound,
306    /// The request conflicts with current service state.
307    Conflict,
308    /// The request exceeds the service's size limit.
309    RequestTooLarge,
310    /// A service precondition is not satisfied.
311    FailedPrecondition,
312    /// Capacity or quota was exhausted; the service did not distinguish which.
313    ResourceExhausted,
314    /// The service explicitly identified a rate limit.
315    RateLimited,
316    /// The service explicitly identified exhausted quota.
317    QuotaExceeded,
318    /// Billing prevents execution.
319    Billing,
320    /// The service is unavailable or overloaded.
321    ServiceUnavailable,
322    /// The service reported an internal error.
323    Internal,
324    /// The service reported cancellation.
325    Cancelled,
326    /// A remote rejection without a recognized portable category.
327    ProviderRejected,
328}
329
330impl ErrorKind {
331    pub(crate) fn from_http_status(status: u16) -> Self {
332        match status {
333            400 | 422 => Self::InvalidRequest,
334            401 => Self::Authentication,
335            402 => Self::Billing,
336            403 => Self::PermissionDenied,
337            404 => Self::NotFound,
338            408 | 504 => Self::DeadlineExceeded,
339            409 => Self::Conflict,
340            412 => Self::FailedPrecondition,
341            413 => Self::RequestTooLarge,
342            429 => Self::ResourceExhausted,
343            500 => Self::Internal,
344            502 | 503 | 529 => Self::ServiceUnavailable,
345            _ => Self::ProviderRejected,
346        }
347    }
348    pub(crate) fn from_canonical_status(status: Option<&str>) -> ErrorKind {
349        match status {
350            Some("INVALID_ARGUMENT" | "OUT_OF_RANGE") => Self::InvalidRequest,
351            Some("UNAUTHENTICATED") => Self::Authentication,
352            Some("PERMISSION_DENIED") => Self::PermissionDenied,
353            Some("NOT_FOUND") => Self::NotFound,
354            Some("ALREADY_EXISTS" | "ABORTED") => Self::Conflict,
355            Some("RESOURCE_EXHAUSTED") => Self::ResourceExhausted,
356            Some("FAILED_PRECONDITION") => Self::FailedPrecondition,
357            Some("DEADLINE_EXCEEDED") => Self::DeadlineExceeded,
358            Some("CANCELLED") => Self::Cancelled,
359            Some("UNAVAILABLE") => Self::ServiceUnavailable,
360            Some("INTERNAL" | "DATA_LOSS" | "UNKNOWN") => Self::Internal,
361            Some("UNIMPLEMENTED") => Self::Unsupported,
362            _ => Self::ProviderRejected,
363        }
364    }
365
366    pub(crate) fn from_rpc_code(code: Option<i64>) -> ErrorKind {
367        match code {
368            Some(1) => Self::Cancelled,
369            Some(2 | 13 | 15) => Self::Internal,
370            Some(3 | 11) => Self::InvalidRequest,
371            Some(4) => Self::DeadlineExceeded,
372            Some(5) => Self::NotFound,
373            Some(6 | 10) => Self::Conflict,
374            Some(7) => Self::PermissionDenied,
375            Some(8) => Self::ResourceExhausted,
376            Some(9) => Self::FailedPrecondition,
377            Some(12) => Self::Unsupported,
378            Some(14) => Self::ServiceUnavailable,
379            Some(16) => Self::Authentication,
380            _ => Self::ProviderRejected,
381        }
382    }
383}
384
385/// A response invariant, assigned at its detection site without interpreting prose.
386///
387/// Field and rule labels are trusted, static schema descriptions. Implementors must
388/// never place credentials, payloads, URLs or native messages in these labels.
389#[derive(Clone, Copy, Debug, Eq, PartialEq)]
390pub enum ResponseViolation {
391    /// JSON could not be decoded into the expected response schema.
392    MalformedJson,
393    /// Text bytes were not valid UTF-8.
394    InvalidUtf8,
395    /// A required response field was absent.
396    MissingField {
397        /// Static schema field name, never the field's value.
398        field: &'static str,
399    },
400    /// A present response field had an invalid value.
401    InvalidField {
402        /// Static schema field name, never the field's value.
403        field: &'static str,
404    },
405    /// A response discriminator was not recognized.
406    UnknownVariant {
407        /// Static discriminator field name.
408        field: &'static str,
409    },
410    /// Response identifiers did not agree.
411    IdentityMismatch {
412        /// Static identity field name.
413        field: &'static str,
414    },
415    /// The response violated an ordering or semantic invariant.
416    ProtocolViolation {
417        /// Trusted, static description of the invariant.
418        rule: &'static str,
419    },
420    /// A response stream ended before its terminal event.
421    UnexpectedEnd,
422    /// Response data exceeded a byte limit.
423    LimitExceeded {
424        /// Maximum permitted bytes.
425        limit: usize,
426    },
427}
428
429impl ResponseViolation {
430    pub(crate) fn from_json_error(error: &serde_json::Error, field: &'static str) -> Self {
431        match error.classify() {
432            serde_json::error::Category::Data => Self::InvalidField { field },
433            serde_json::error::Category::Syntax | serde_json::error::Category::Eof => {
434                Self::MalformedJson
435            }
436            serde_json::error::Category::Io => Self::ProtocolViolation {
437                rule: "JSON input read failure",
438            },
439        }
440    }
441}
442
443/// Caller-visible operation associated with a failure.
444#[derive(Clone, Copy, Debug, Eq, PartialEq)]
445pub enum Operation {
446    /// No enclosing behavioral call is available (for example, a standalone DTO check).
447    Unspecified,
448    /// Provider construction.
449    Configure,
450    /// Standalone input validation.
451    ValidateInput,
452    /// Exact model selection.
453    SelectModel,
454    /// Model discovery.
455    ListModels,
456    /// Model metadata lookup.
457    ModelInfo,
458    /// File upload.
459    UploadFile,
460    /// File listing.
461    ListFiles,
462    /// File metadata lookup.
463    FileInfo,
464    /// File deletion.
465    DeleteFile,
466    /// Bounded file download.
467    DownloadFile,
468    /// Batch status lookup.
469    BatchStatus,
470    /// Batch cancellation.
471    CancelBatch,
472    /// Bounded batch result retrieval and normalization.
473    BatchResults,
474    /// Batch listing.
475    ListBatches,
476    /// Runtime health check.
477    Health,
478    /// Shared provider shutdown.
479    Shutdown,
480    /// Unary generation.
481    Generate,
482    /// Streaming generation, including stream consumption.
483    GenerateStream,
484    /// Embedding generation.
485    Embed,
486    /// Batch submission.
487    SubmitBatch,
488    /// Stateful session opening.
489    OpenSession,
490    /// Sending a session command.
491    SessionSend,
492    /// Receiving a session event.
493    SessionReceive,
494    /// Session closure.
495    SessionClose,
496}
497
498/// Stage at which the failure was observed; it does not assert that execution is safe to retry.
499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
500pub enum ErrorPhase {
501    /// A caller implementation supplied no finer phase.
502    Unspecified,
503    /// Provider configuration validation.
504    Configuration,
505    /// Local request validation.
506    InputValidation,
507    /// Request encoding or construction.
508    RequestBuild,
509    /// Sending a request or opening its transport.
510    RequestSend,
511    /// Reading or validating the response status and headers.
512    ResponseHeaders,
513    /// Reading response bytes.
514    ReadBody,
515    /// Decoding a response or frame.
516    Decode,
517    /// Converting decoded native data to portable values.
518    Normalize,
519    /// A service error or protocol failure during a generation stream.
520    Stream,
521    /// Session connection negotiation.
522    SessionHandshake,
523    /// Sending a session command.
524    SessionSend,
525    /// Receiving a session event.
526    SessionReceive,
527    /// Closing a session connection.
528    SessionClose,
529    /// Lifecycle admission or shutdown.
530    Lifecycle,
531    /// Releasing owned resources after an operation.
532    Cleanup,
533}
534
535/// Safe operation context, including the location of a failed batch result row.
536///
537/// ```
538/// use providers_kit::error::{ErrorContext, ErrorPhase, Operation};
539/// let context = ErrorContext {
540///     operation: Operation::BatchResults, phase: ErrorPhase::Normalize, row: Some(0),
541/// };
542/// assert_eq!(context.row, Some(0));
543/// ```
544#[derive(Clone, Copy, Debug, Eq, PartialEq)]
545pub struct ErrorContext {
546    /// The enclosing public operation.
547    pub operation: Operation,
548    /// The stage at which this error was detected.
549    pub phase: ErrorPhase,
550    /// Zero-based logical batch result row, excluding blank JSONL lines.
551    pub row: Option<usize>,
552}
553
554/// One structured native fact with its meaning preserved across providers.
555///
556/// Unknown native fields remain in the bounded body instead of an untyped public map.
557#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
558#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
559pub enum NativeDetail {
560    /// A reason is meaningful within its reported domain.
561    Reason {
562        /// Native reason domain.
563        domain: String,
564        /// Native reason value.
565        reason: String,
566    },
567    /// A resource explicitly identified by the service, not a caller correlation ID.
568    Resource {
569        /// Native resource type, if supplied.
570        resource_type: Option<String>,
571        /// Exact native resource name.
572        name: String,
573        /// Native resource owner, if supplied.
574        owner: Option<String>,
575        /// Native description, if supplied.
576        description: Option<String>,
577    },
578    /// Native request field validation failure.
579    FieldViolation {
580        /// Native field path.
581        field: String,
582        /// Native reason, when supplied.
583        reason: Option<String>,
584        /// Native diagnostic prose, when supplied.
585        description: Option<String>,
586    },
587    /// Native retry guidance; it does not authorize an automatic retry.
588    RetryDelay {
589        /// Whole nonnegative seconds.
590        seconds: u64,
591        /// Reported fractional nanoseconds.
592        nanos: u32,
593    },
594    /// Native quota violation.
595    QuotaViolation {
596        /// Native quota subject, when supplied.
597        subject: Option<String>,
598        /// Native diagnostic prose, when supplied.
599        description: Option<String>,
600    },
601    /// Native precondition failure.
602    PreconditionViolation {
603        /// Native precondition kind, when supplied.
604        kind: Option<String>,
605        /// Native subject, when supplied.
606        subject: Option<String>,
607        /// Native diagnostic prose, when supplied.
608        description: Option<String>,
609    },
610}
611
612impl NativeDetail {
613    fn strings(&self) -> [Option<&str>; 4] {
614        match self {
615            Self::Reason { domain, reason } => [Some(domain), Some(reason), None, None],
616            Self::Resource {
617                resource_type,
618                name,
619                owner,
620                description,
621            } => [
622                resource_type.as_deref(),
623                Some(name),
624                owner.as_deref(),
625                description.as_deref(),
626            ],
627            Self::FieldViolation {
628                field,
629                reason,
630                description,
631            } => [Some(field), reason.as_deref(), description.as_deref(), None],
632            Self::QuotaViolation {
633                subject,
634                description,
635            } => [subject.as_deref(), description.as_deref(), None, None],
636            Self::PreconditionViolation {
637                kind,
638                subject,
639                description,
640            } => [
641                kind.as_deref(),
642                subject.as_deref(),
643                description.as_deref(),
644                None,
645            ],
646            Self::RetryDelay { .. } => [None, None, None, None],
647        }
648    }
649
650    fn bound(&mut self, budget: &mut DetailBudget) {
651        match self {
652            Self::Reason { domain, reason } => {
653                budget.string(domain);
654                budget.string(reason);
655            }
656            Self::Resource {
657                resource_type,
658                name,
659                owner,
660                description,
661            } => {
662                budget.string(name);
663                for value in [resource_type, owner, description].into_iter().flatten() {
664                    budget.string(value);
665                }
666            }
667            Self::FieldViolation {
668                field,
669                reason,
670                description,
671            } => {
672                budget.string(field);
673                for value in [reason, description].into_iter().flatten() {
674                    budget.string(value);
675                }
676            }
677            Self::QuotaViolation {
678                subject,
679                description,
680            } => {
681                for value in [subject, description].into_iter().flatten() {
682                    budget.string(value);
683                }
684            }
685            Self::PreconditionViolation {
686                kind,
687                subject,
688                description,
689            } => {
690                for value in [kind, subject, description].into_iter().flatten() {
691                    budget.string(value);
692                }
693            }
694            Self::RetryDelay { .. } => {}
695        }
696    }
697}
698
699impl fmt::Debug for NativeDetail {
700    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
701        let name = match self {
702            Self::Reason { .. } => "Reason",
703            Self::Resource { .. } => "Resource",
704            Self::FieldViolation { .. } => "FieldViolation",
705            Self::RetryDelay { .. } => "RetryDelay",
706            Self::QuotaViolation { .. } => "QuotaViolation",
707            Self::PreconditionViolation { .. } => "PreconditionViolation",
708        };
709        formatter.write_str(name)?;
710        formatter.write_str(" { details: <redacted> }")
711    }
712}
713
714struct DetailBudget {
715    remaining: usize,
716    remaining_nodes: usize,
717    truncated: bool,
718}
719
720impl DetailBudget {
721    fn new() -> Self {
722        Self {
723            remaining: DETAIL_TOTAL_LIMIT,
724            remaining_nodes: ERROR_NODE_LIMIT,
725            truncated: false,
726        }
727    }
728
729    fn string(&mut self, value: &mut String) {
730        let limit = DETAIL_STRING_LIMIT.min(self.remaining);
731        if value.len() > limit {
732            let mut end = limit;
733            while !value.is_char_boundary(end) {
734                end -= 1;
735            }
736            value.truncate(end);
737            self.truncated = true;
738        }
739        self.remaining -= value.len();
740    }
741}
742
743/// Potentially sensitive native details, available only through explicit inspection.
744///
745/// ```
746/// use providers_kit::error::NativeErrorDetails;
747/// let details = NativeErrorDetails { message: Some("private payload".into()), ..Default::default() };
748/// assert!(!format!("{details:?}").contains("private payload"));
749/// ```
750///
751/// All string fields are bounded when attached to [`KitError`]. `Debug` deliberately
752/// hides them, including native codes and request IDs, because a service or proxy can
753/// return arbitrary text in any of them. Raw bodies retain provider-specific fields
754/// not covered by the portable metadata vocabulary.
755#[derive(Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
756#[serde(default, deny_unknown_fields)]
757pub struct NativeErrorDetails {
758    /// Native or Kit diagnostic prose, potentially containing caller input.
759    pub message: Option<String>,
760    /// Bounded native response or frame body, potentially containing secrets.
761    pub body: Option<String>,
762    /// Native error code; its meaning depends on the wire protocol.
763    pub code: Option<String>,
764    /// Native error type, distinct from its code.
765    pub error_type: Option<String>,
766    /// Native event discriminator, distinct from the inner error type.
767    pub event_type: Option<String>,
768    /// Native hosted execution outcome.
769    pub outcome: Option<String>,
770    /// Native parameter associated with the failure.
771    pub parameter: Option<String>,
772    /// Native request correlation ID.
773    pub request_id: Option<String>,
774    /// ID of the server event reporting the failure.
775    pub event_id: Option<String>,
776    /// ID of the client event that caused the failure.
777    pub related_event_id: Option<String>,
778    /// Native tool-call correlation, never a missing-resource identifier.
779    pub tool_call_id: Option<String>,
780    /// Native stream event sequence number, when reported.
781    pub sequence_number: Option<u64>,
782    /// Native status name (for example, a Google RPC canonical status).
783    pub native_status: Option<String>,
784    /// HTTP status reported by a native body or embedded result, not an invented outer status.
785    pub reported_http_status: Option<u16>,
786    /// Native RPC canonical numeric code, distinct from HTTP status.
787    pub rpc_code: Option<i64>,
788    /// Native execution exit code, distinct from HTTP and RPC codes.
789    pub execution_exit_code: Option<i64>,
790    /// Native Retry-After header; no automatic retry is implied.
791    pub retry_after: Option<String>,
792    /// Request URL, potentially containing credentials or private resource names.
793    pub url: Option<String>,
794    /// Repeated structured native evidence; at most 256 records are retained.
795    pub details: Vec<NativeDetail>,
796    /// At least one detail was truncated or the body was known to be incomplete.
797    pub truncated: bool,
798    /// Number of structured native records omitted during bounded capture.
799    pub omitted_details: usize,
800}
801
802impl NativeErrorDetails {
803    fn bound(&mut self) {
804        let mut budget = DetailBudget::new();
805        self.bound_with_budget(&mut budget);
806    }
807
808    fn bound_with_budget(&mut self, budget: &mut DetailBudget) {
809        // Reserve the named classification evidence before optional diagnostics.
810        for value in [
811            &mut self.code,
812            &mut self.error_type,
813            &mut self.native_status,
814            &mut self.outcome,
815        ]
816        .into_iter()
817        .flatten()
818        {
819            budget.string(value);
820        }
821        // The first reason is the one used by a NativeReason classifier. Retain
822        // that record even when it occurs after the general record limit.
823        let reason_index = self
824            .details
825            .iter()
826            .position(|detail| matches!(detail, NativeDetail::Reason { .. }));
827        if let Some(index) = reason_index {
828            self.details[index].bound(budget);
829            if index >= DETAIL_RECORD_LIMIT {
830                let reason = self.details.remove(index);
831                self.details.insert(0, reason);
832            }
833        }
834        let omitted = self.details.len().saturating_sub(DETAIL_RECORD_LIMIT);
835        self.omitted_details = self.omitted_details.saturating_add(omitted);
836        self.details.truncate(DETAIL_RECORD_LIMIT);
837        let retained_reason = self
838            .details
839            .iter()
840            .position(|detail| matches!(detail, NativeDetail::Reason { .. }));
841        for (index, detail) in self.details.iter_mut().enumerate() {
842            if Some(index) != retained_reason {
843                detail.bound(budget);
844            }
845        }
846        for value in [
847            &mut self.event_type,
848            &mut self.parameter,
849            &mut self.request_id,
850            &mut self.event_id,
851            &mut self.related_event_id,
852            &mut self.tool_call_id,
853            &mut self.retry_after,
854            &mut self.url,
855            &mut self.message,
856            &mut self.body,
857        ]
858        .into_iter()
859        .flatten()
860        {
861            budget.string(value);
862        }
863        self.truncated |= budget.truncated || self.omitted_details != 0;
864    }
865
866    fn validate(&self) -> Result<(), KitError> {
867        if self.details.len() > DETAIL_RECORD_LIMIT {
868            return Err(KitError::invalid_input(
869                ValidationViolation::LimitExceeded {
870                    field: "native_details.details",
871                    limit: DETAIL_RECORD_LIMIT,
872                },
873                "native details exceed 256 records",
874            ));
875        }
876        if self.omitted_details != 0 && !self.truncated {
877            return Err(KitError::invalid_input(
878                ValidationViolation::InvalidValue {
879                    field: "native_details.truncated",
880                    rule: "required when detail records were omitted",
881                },
882                "omitted native details require a truncation marker",
883            ));
884        }
885        let named = [
886            self.message.as_deref(),
887            self.body.as_deref(),
888            self.code.as_deref(),
889            self.error_type.as_deref(),
890            self.event_type.as_deref(),
891            self.outcome.as_deref(),
892            self.parameter.as_deref(),
893            self.request_id.as_deref(),
894            self.event_id.as_deref(),
895            self.related_event_id.as_deref(),
896            self.tool_call_id.as_deref(),
897            self.native_status.as_deref(),
898            self.retry_after.as_deref(),
899            self.url.as_deref(),
900        ];
901        let strings = named.into_iter().flatten().chain(
902            self.details
903                .iter()
904                .flat_map(|detail| detail.strings().into_iter().flatten()),
905        );
906        let mut total = 0_usize;
907        for value in strings {
908            if value.len() > DETAIL_STRING_LIMIT {
909                return Err(KitError::invalid_input(
910                    ValidationViolation::LimitExceeded {
911                        field: "native_details.string",
912                        limit: DETAIL_STRING_LIMIT,
913                    },
914                    "native detail exceeds 64 KiB",
915                ));
916            }
917            total = total.saturating_add(value.len());
918        }
919        if total > DETAIL_TOTAL_LIMIT {
920            return Err(KitError::invalid_input(
921                ValidationViolation::LimitExceeded {
922                    field: "native_details",
923                    limit: DETAIL_TOTAL_LIMIT,
924                },
925                "native details exceed 1 MiB in total",
926            ));
927        }
928        Ok(())
929    }
930}
931
932impl fmt::Debug for NativeErrorDetails {
933    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
934        formatter
935            .debug_struct("NativeErrorDetails")
936            .field("details", &"<redacted>")
937            .field("truncated", &self.truncated)
938            .field("omitted_details", &self.omitted_details)
939            .finish()
940    }
941}
942
943#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
944#[serde(rename_all = "snake_case")]
945enum RemoteCategory {
946    Unsupported,
947    InvalidRequest,
948    Authentication,
949    PermissionDenied,
950    NotFound,
951    Conflict,
952    RequestTooLarge,
953    FailedPrecondition,
954    ResourceExhausted,
955    RateLimited,
956    QuotaExceeded,
957    Billing,
958    ServiceUnavailable,
959    Internal,
960    DeadlineExceeded,
961    Cancelled,
962    ProviderRejected,
963}
964
965impl RemoteCategory {
966    fn from_kind(kind: ErrorKind) -> Result<Self, KitError> {
967        match kind {
968            ErrorKind::Unsupported => Ok(Self::Unsupported),
969            ErrorKind::InvalidRequest => Ok(Self::InvalidRequest),
970            ErrorKind::Authentication => Ok(Self::Authentication),
971            ErrorKind::PermissionDenied => Ok(Self::PermissionDenied),
972            ErrorKind::NotFound => Ok(Self::NotFound),
973            ErrorKind::Conflict => Ok(Self::Conflict),
974            ErrorKind::RequestTooLarge => Ok(Self::RequestTooLarge),
975            ErrorKind::FailedPrecondition => Ok(Self::FailedPrecondition),
976            ErrorKind::ResourceExhausted => Ok(Self::ResourceExhausted),
977            ErrorKind::RateLimited => Ok(Self::RateLimited),
978            ErrorKind::QuotaExceeded => Ok(Self::QuotaExceeded),
979            ErrorKind::Billing => Ok(Self::Billing),
980            ErrorKind::ServiceUnavailable => Ok(Self::ServiceUnavailable),
981            ErrorKind::Internal => Ok(Self::Internal),
982            ErrorKind::DeadlineExceeded => Ok(Self::DeadlineExceeded),
983            ErrorKind::Cancelled => Ok(Self::Cancelled),
984            ErrorKind::ProviderRejected => Ok(Self::ProviderRejected),
985            ErrorKind::Configuration
986            | ErrorKind::InvalidInput
987            | ErrorKind::Transport
988            | ErrorKind::Timeout
989            | ErrorKind::Closed
990            | ErrorKind::InvalidState
991            | ErrorKind::InvalidResponse(_) => Err(KitError::invalid_input(
992                ValidationViolation::InvalidValue {
993                    field: "remote_failure.kind",
994                    rule: "must classify a remote outcome",
995                },
996                "remote failure cannot use a local or response-schema classification",
997            )),
998        }
999    }
1000
1001    fn kind(self) -> ErrorKind {
1002        match self {
1003            Self::Unsupported => ErrorKind::Unsupported,
1004            Self::InvalidRequest => ErrorKind::InvalidRequest,
1005            Self::Authentication => ErrorKind::Authentication,
1006            Self::PermissionDenied => ErrorKind::PermissionDenied,
1007            Self::NotFound => ErrorKind::NotFound,
1008            Self::Conflict => ErrorKind::Conflict,
1009            Self::RequestTooLarge => ErrorKind::RequestTooLarge,
1010            Self::FailedPrecondition => ErrorKind::FailedPrecondition,
1011            Self::ResourceExhausted => ErrorKind::ResourceExhausted,
1012            Self::RateLimited => ErrorKind::RateLimited,
1013            Self::QuotaExceeded => ErrorKind::QuotaExceeded,
1014            Self::Billing => ErrorKind::Billing,
1015            Self::ServiceUnavailable => ErrorKind::ServiceUnavailable,
1016            Self::Internal => ErrorKind::Internal,
1017            Self::DeadlineExceeded => ErrorKind::DeadlineExceeded,
1018            Self::Cancelled => ErrorKind::Cancelled,
1019            Self::ProviderRejected => ErrorKind::ProviderRejected,
1020        }
1021    }
1022}
1023
1024/// An opaque remote failure, including failures inside successful batch or hosted results.
1025///
1026/// Serialization deliberately exports all retained native evidence, including the
1027/// bounded body. It is not a redacted logging format. Deserialization rejects invalid
1028/// categories, inconsistent evidence and values exceeding the documented bounds.
1029///
1030/// ```
1031/// use providers_kit::error::{ErrorKind, NativeErrorDetails, RemoteFailure};
1032/// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails::default())?;
1033/// assert_eq!(failure.kind(), ErrorKind::NotFound);
1034///
1035/// # Ok::<(), providers_kit::KitError>(())
1036/// ```
1037#[derive(Clone, Eq, PartialEq)]
1038pub struct RemoteFailure {
1039    category: RemoteCategory,
1040    basis: ClassificationBasis,
1041    details: Box<NativeErrorDetails>,
1042}
1043
1044impl RemoteFailure {
1045    /// Constructs an explicitly classified caller failure without claiming native inference.
1046    ///
1047    /// Details must already fit 64 KiB per string, 1 MiB total string bytes, and 256
1048    /// repeated records. Oversized caller evidence is rejected rather than discarded.
1049    ///
1050    /// # Errors
1051    ///
1052    /// Returns an input error for local-only categories, invalid numeric/detail fields, or evidence exceeding the documented capture bounds. Explicit classification does not authenticate origin or establish retry safety.
1053    ///
1054    /// # Examples
1055    ///
1056    /// ```
1057    /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1058    /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1059    /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1060    /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1061    /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1062    /// assert!(!format!("{failure:?}").contains("private detail"));
1063    ///
1064    /// # Ok::<(), providers_kit::KitError>(())
1065    /// ```
1066    ///
1067    pub fn new(kind: ErrorKind, details: NativeErrorDetails) -> Result<Self, KitError> {
1068        let category = RemoteCategory::from_kind(kind)?;
1069        details.validate()?;
1070        Ok(Self {
1071            category,
1072            basis: ClassificationBasis::Explicit,
1073            details: Box::new(details),
1074        })
1075    }
1076
1077    /// Returns the same portable classification vocabulary used by [`KitError`].
1078    ///
1079    /// # Examples
1080    ///
1081    /// ```
1082    /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1083    /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1084    /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1085    /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1086    /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1087    /// assert!(!format!("{failure:?}").contains("private detail"));
1088    ///
1089    /// # Ok::<(), providers_kit::KitError>(())
1090    /// ```
1091    ///
1092    pub fn kind(&self) -> ErrorKind {
1093        self.category.kind()
1094    }
1095
1096    /// Returns the evidence used for classification, independently of the retained body.
1097    ///
1098    /// # Examples
1099    ///
1100    /// ```
1101    /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1102    /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1103    /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1104    /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1105    /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1106    /// assert!(!format!("{failure:?}").contains("private detail"));
1107    ///
1108    /// # Ok::<(), providers_kit::KitError>(())
1109    /// ```
1110    ///
1111    pub fn classification_basis(&self) -> ClassificationBasis {
1112        self.basis
1113    }
1114
1115    /// Explicitly exposes potentially sensitive native evidence.
1116    ///
1117    /// # Examples
1118    ///
1119    /// ```
1120    /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1121    /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1122    /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1123    /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1124    /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1125    /// assert!(!format!("{failure:?}").contains("private detail"));
1126    ///
1127    /// # Ok::<(), providers_kit::KitError>(())
1128    /// ```
1129    ///
1130    pub fn native_details(&self) -> &NativeErrorDetails {
1131        &self.details
1132    }
1133
1134    pub(crate) fn from_native(
1135        kind: ErrorKind,
1136        basis: ClassificationBasis,
1137        mut details: NativeErrorDetails,
1138    ) -> Result<Self, KitError> {
1139        let category = RemoteCategory::from_kind(kind)?;
1140        details.bound();
1141        let failure = Self {
1142            category,
1143            basis,
1144            details: Box::new(details),
1145        };
1146        failure.validate()?;
1147        Ok(failure)
1148    }
1149
1150    fn validate(&self) -> Result<(), KitError> {
1151        self.details.validate()?;
1152        let present = match self.basis {
1153            ClassificationBasis::Explicit => true,
1154            ClassificationBasis::HttpStatus { status } => {
1155                (100..=599).contains(&status)
1156                    && !(200..300).contains(&status)
1157                    && self.kind() == ErrorKind::from_http_status(status)
1158            }
1159            ClassificationBasis::NativeCode => self
1160                .details
1161                .code
1162                .as_deref()
1163                .is_some_and(|value| !value.trim().is_empty()),
1164            ClassificationBasis::NativeErrorType => self
1165                .details
1166                .error_type
1167                .as_deref()
1168                .is_some_and(|value| !value.trim().is_empty()),
1169            ClassificationBasis::CanonicalStatus => {
1170                self.details.native_status.as_deref().is_some_and(|value| {
1171                    let kind = ErrorKind::from_canonical_status(Some(value));
1172                    kind != ErrorKind::ProviderRejected && self.kind() == kind
1173                })
1174            }
1175            ClassificationBasis::RpcCode => self.details.rpc_code.is_some_and(|code| {
1176                let kind = ErrorKind::from_rpc_code(Some(code));
1177                kind != ErrorKind::ProviderRejected && self.kind() == kind
1178            }),
1179            ClassificationBasis::NativeReason => self
1180                .details
1181                .details
1182                .iter()
1183                .find_map(|detail| match detail {
1184                    NativeDetail::Reason { domain, reason } => {
1185                        Some(!domain.trim().is_empty() && !reason.trim().is_empty())
1186                    }
1187                    _ => None,
1188                })
1189                .unwrap_or(false),
1190            ClassificationBasis::NativeOutcome => match self.details.outcome.as_deref() {
1191                Some("OUTCOME_FAILED" | "URL_RETRIEVAL_STATUS_ERROR") => {
1192                    self.kind() == ErrorKind::ProviderRejected
1193                }
1194                Some("OUTCOME_DEADLINE_EXCEEDED") => self.kind() == ErrorKind::DeadlineExceeded,
1195                Some("URL_RETRIEVAL_STATUS_PAYWALL" | "URL_RETRIEVAL_STATUS_UNSAFE") => {
1196                    self.kind() == ErrorKind::PermissionDenied
1197                }
1198                None => {
1199                    self.details
1200                        .execution_exit_code
1201                        .is_some_and(|code| code != 0)
1202                        && self.kind() == ErrorKind::ProviderRejected
1203                }
1204                Some(_) => false,
1205            },
1206            ClassificationBasis::Unclassified => self.category == RemoteCategory::ProviderRejected,
1207        };
1208        if !present {
1209            return Err(KitError::invalid_input(
1210                ValidationViolation::InvalidValue {
1211                    field: "remote_failure.basis",
1212                    rule: "requires corresponding classification evidence",
1213                },
1214                "remote classification basis does not have corresponding evidence",
1215            ));
1216        }
1217        Ok(())
1218    }
1219}
1220
1221impl Serialize for RemoteFailure {
1222    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1223    where
1224        S: serde::Serializer,
1225    {
1226        #[derive(Serialize)]
1227        struct Wire<'a> {
1228            kind: RemoteCategory,
1229            basis: ClassificationBasis,
1230            details: &'a NativeErrorDetails,
1231        }
1232        Wire {
1233            kind: self.category,
1234            basis: self.basis,
1235            details: &self.details,
1236        }
1237        .serialize(serializer)
1238    }
1239}
1240
1241impl<'de> Deserialize<'de> for RemoteFailure {
1242    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1243    where
1244        D: serde::Deserializer<'de>,
1245    {
1246        #[derive(Deserialize)]
1247        #[serde(deny_unknown_fields)]
1248        struct Wire {
1249            kind: RemoteCategory,
1250            basis: ClassificationBasis,
1251            details: NativeErrorDetails,
1252        }
1253        let wire = Wire::deserialize(deserializer)?;
1254        let failure = Self {
1255            category: wire.kind,
1256            basis: wire.basis,
1257            details: Box::new(wire.details),
1258        };
1259        failure.validate().map_err(serde::de::Error::custom)?;
1260        Ok(failure)
1261    }
1262}
1263
1264impl fmt::Display for RemoteFailure {
1265    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1266        write!(formatter, "remote {:?} ({:?})", self.kind(), self.basis)
1267    }
1268}
1269
1270impl fmt::Debug for RemoteFailure {
1271    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1272        formatter
1273            .debug_struct("RemoteFailure")
1274            .field("kind", &self.kind())
1275            .field("basis", &self.basis)
1276            .field("details", &self.details)
1277            .finish()
1278    }
1279}
1280
1281impl std::error::Error for RemoteFailure {}
1282
1283#[derive(Clone)]
1284enum ErrorFailure {
1285    Classified {
1286        kind: ErrorKind,
1287        details: Box<NativeErrorDetails>,
1288    },
1289    Remote(RemoteFailure),
1290}
1291
1292impl ErrorFailure {
1293    fn kind(&self) -> ErrorKind {
1294        match self {
1295            Self::Classified { kind, .. } => *kind,
1296            Self::Remote(failure) => failure.kind(),
1297        }
1298    }
1299
1300    fn details(&self) -> &NativeErrorDetails {
1301        match self {
1302            Self::Classified { details, .. } => details,
1303            Self::Remote(failure) => &failure.details,
1304        }
1305    }
1306
1307    fn details_mut(&mut self) -> &mut NativeErrorDetails {
1308        match self {
1309            Self::Classified { details, .. } => details,
1310            Self::Remote(failure) => &mut failure.details,
1311        }
1312    }
1313}
1314
1315#[derive(Clone)]
1316struct ErrorData {
1317    failure: ErrorFailure,
1318    validation: Option<ValidationViolation>,
1319    truncated: bool,
1320    status: Option<u16>,
1321    timeout: Option<Duration>,
1322    response: Option<ResponseDetails>,
1323    request_target: Option<String>,
1324    correlation_id: Option<String>,
1325    cause: Option<Box<KitError>>,
1326    supplementary: Vec<KitError>,
1327}
1328
1329impl ErrorData {
1330    fn bound(&mut self) {
1331        self.bound_tree(&mut DetailBudget::new(), 1);
1332    }
1333
1334    fn bound_tree(&mut self, budget: &mut DetailBudget, depth: usize) {
1335        budget.remaining_nodes -= 1;
1336        let previous_truncation = budget.truncated;
1337        budget.truncated = false;
1338        // Retain this node's observations and local correlation before its native body.
1339        if let Some(response) = self.response.as_mut() {
1340            response.bound(budget);
1341        }
1342        for value in [&mut self.request_target, &mut self.correlation_id]
1343            .into_iter()
1344            .flatten()
1345        {
1346            budget.string(value);
1347        }
1348        self.failure.details_mut().bound_with_budget(budget);
1349        self.truncated |= self.failure.details().truncated;
1350        if depth == ERROR_DEPTH_LIMIT {
1351            self.truncated |= self.cause.is_some() || !self.supplementary.is_empty();
1352            self.cause = None;
1353            self.supplementary.clear();
1354        } else {
1355            if self.cause.as_ref().is_some_and(|cause| {
1356                budget.remaining_nodes == 0 || !cause.data.retains_remote_evidence(budget)
1357            }) {
1358                self.cause = None;
1359                self.truncated = true;
1360            } else if let Some(cause) = self.cause.as_mut() {
1361                cause.data.bound_tree(budget, depth + 1);
1362            }
1363            let mut omitted = false;
1364            self.supplementary.retain_mut(|child| {
1365                if budget.remaining_nodes == 0 || !child.data.retains_remote_evidence(budget) {
1366                    omitted = true;
1367                    false
1368                } else {
1369                    child.data.bound_tree(budget, depth + 1);
1370                    true
1371                }
1372            });
1373            self.truncated |= omitted;
1374        }
1375        self.truncated |= budget.truncated;
1376        budget.truncated |= previous_truncation || self.truncated;
1377    }
1378
1379    fn retains_remote_evidence(&self, available: &DetailBudget) -> bool {
1380        let ErrorFailure::Remote(failure) = &self.failure else {
1381            return true;
1382        };
1383        let mut budget = DetailBudget {
1384            remaining: available.remaining,
1385            remaining_nodes: available.remaining_nodes,
1386            truncated: false,
1387        };
1388        if let Some(mut response) = self.response.clone() {
1389            response.bound(&mut budget);
1390        }
1391        for value in [&self.request_target, &self.correlation_id]
1392            .into_iter()
1393            .flatten()
1394        {
1395            budget.string(&mut value.clone());
1396        }
1397        let mut retained = failure.clone();
1398        retained.details.bound_with_budget(&mut budget);
1399        // A child is optional diagnostic context. Omit its subtree instead of
1400        // exposing an invalid serializable RemoteFailure or changing its basis.
1401        retained.validate().is_ok()
1402    }
1403}
1404
1405/// Structured error returned by the portable provider API.
1406///
1407/// ```
1408/// use providers_kit::KitError;
1409/// use providers_kit::error::{ErrorKind, ErrorPhase, Operation, ResponseViolation};
1410/// let error = KitError::new(
1411///     ErrorKind::InvalidResponse(ResponseViolation::MissingField { field: "modelVersion" }),
1412///     Operation::BatchResults,
1413///     ErrorPhase::Normalize,
1414/// ).with_row(0);
1415/// assert_eq!(error.context().row, Some(0));
1416/// assert!(error.to_string().contains("modelVersion"));
1417/// ```
1418///
1419/// Runtime errors are deliberately not serializable. Accepted remote outcomes use
1420/// [`RemoteFailure`]; runtime errors may contain trusted static schema labels and
1421/// causal Kit failures that must not be reconstructed from untrusted JSON.
1422///
1423/// ```compile_fail
1424/// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1425/// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::RequestSend);
1426/// let _ = serde_json::to_string(&error);
1427/// ```
1428#[derive(Clone)]
1429pub struct KitError {
1430    context: ErrorContext,
1431    data: Box<ErrorData>,
1432}
1433
1434impl KitError {
1435    /// Constructs a classified failure without native details.
1436    ///
1437    /// ```
1438    /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1439    /// let error = KitError::new(ErrorKind::Unsupported, Operation::Embed, ErrorPhase::InputValidation);
1440    /// assert_eq!(error.kind(), ErrorKind::Unsupported);
1441    /// ```
1442    pub fn new(kind: ErrorKind, operation: Operation, phase: ErrorPhase) -> Self {
1443        Self {
1444            context: ErrorContext {
1445                operation,
1446                phase,
1447                row: None,
1448            },
1449            data: Box::new(ErrorData {
1450                failure: ErrorFailure::Classified {
1451                    kind,
1452                    details: Box::default(),
1453                },
1454                validation: None,
1455                truncated: false,
1456                status: None,
1457                timeout: None,
1458                response: None,
1459                request_target: None,
1460                correlation_id: None,
1461                cause: None,
1462                supplementary: Vec::new(),
1463            }),
1464        }
1465    }
1466
1467    /// Attaches a remote failure to the operation that observed it.
1468    ///
1469    /// This does not invent an outer HTTP status from an embedded result or stream.
1470    ///
1471    /// # Examples
1472    ///
1473    /// ```
1474    /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1475    /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1476    /// use providers_kit::error::{KitError, Operation, ErrorPhase};
1477    /// let error = KitError::from_remote(failure, Operation::FileInfo, ErrorPhase::ReadBody);
1478    /// assert_eq!(error.kind(), ErrorKind::NotFound);
1479    ///
1480    /// # Ok::<(), providers_kit::KitError>(())
1481    /// ```
1482    ///
1483    pub fn from_remote(failure: RemoteFailure, operation: Operation, phase: ErrorPhase) -> Self {
1484        Self::new(failure.kind(), operation, phase).with_remote_failure(failure)
1485    }
1486
1487    pub(crate) fn with_remote_failure(mut self, failure: RemoteFailure) -> Self {
1488        self.data.failure = ErrorFailure::Remote(failure);
1489        self.data.bound();
1490        self
1491    }
1492
1493    /// Returns the basis for classification; caller construction remains explicit.
1494    ///
1495    /// # Examples
1496    ///
1497    /// ```
1498    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1499    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1500    /// let _basis = error.classification_basis();
1501    /// ```
1502    ///
1503    pub fn classification_basis(&self) -> ClassificationBasis {
1504        match &self.data.failure {
1505            ErrorFailure::Classified { .. } => ClassificationBasis::Explicit,
1506            ErrorFailure::Remote(failure) => failure.classification_basis(),
1507        }
1508    }
1509
1510    /// Returns an attached remote failure, if one was represented explicitly.
1511    ///
1512    /// Absence does not establish that the implementation made no service request.
1513    ///
1514    /// # Examples
1515    ///
1516    /// ```
1517    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1518    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1519    /// assert!(error.remote_failure().is_none());
1520    /// ```
1521    ///
1522    pub fn remote_failure(&self) -> Option<&RemoteFailure> {
1523        match &self.data.failure {
1524            ErrorFailure::Classified { .. } => None,
1525            ErrorFailure::Remote(failure) => Some(failure),
1526        }
1527    }
1528
1529    /// Explicitly exposes potentially sensitive observations for a response violation.
1530    ///
1531    /// # Examples
1532    ///
1533    /// ```
1534    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1535    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1536    /// assert!(error.response_details().is_none());
1537    /// ```
1538    ///
1539    pub fn response_details(&self) -> Option<&ResponseDetails> {
1540        self.data.response.as_ref()
1541    }
1542
1543    /// Returns the safe, detector-assigned local input invariant, when applicable.
1544    ///
1545    /// ```
1546    /// use providers_kit::{generation::GenerationConfig, error::ValidationViolation};
1547    /// let error = GenerationConfig::default().with_temperature(-1.0).unwrap_err();
1548    /// assert!(matches!(error.validation_violation(), Some(ValidationViolation::OutOfRange {
1549    ///     field: "generation.temperature", ..
1550    /// })));
1551    /// ```
1552    pub fn validation_violation(&self) -> Option<&ValidationViolation> {
1553        self.data.validation.as_ref()
1554    }
1555
1556    /// Attaches trusted field/rule labels without exposing rejected caller values.
1557    ///
1558    /// ```
1559    /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation, ValidationViolation}};
1560    /// let error = KitError::new(ErrorKind::InvalidInput, Operation::ValidateInput, ErrorPhase::InputValidation)
1561    ///     .with_validation_violation(ValidationViolation::Empty { field: "messages" });
1562    /// assert_eq!(error.validation_violation(), Some(&ValidationViolation::Empty { field: "messages" }));
1563    /// ```
1564    pub fn with_validation_violation(mut self, violation: ValidationViolation) -> Self {
1565        self.data.validation = Some(violation);
1566        self
1567    }
1568
1569    /// Reports whether this tree lost diagnostics to string, record, depth, or node bounds.
1570    ///
1571    /// A tree retains at most eight levels, 32 nodes, and 1 MiB of diagnostic strings.
1572    /// The primary error is retained before its cause subtree and supplementary errors.
1573    ///
1574    /// ```
1575    /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, NativeErrorDetails, Operation}};
1576    /// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody)
1577    ///     .with_native_details(NativeErrorDetails {
1578    ///         message: Some("x".repeat(65_537)), ..Default::default()
1579    ///     });
1580    /// assert!(error.diagnostics_truncated());
1581    /// ```
1582    pub fn diagnostics_truncated(&self) -> bool {
1583        self.data.truncated
1584    }
1585
1586    /// Returns the cause of this failure, separately from cleanup or diagnostic failures.
1587    ///
1588    /// # Examples
1589    ///
1590    /// ```
1591    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1592    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1593    /// assert!(error.cause().is_none());
1594    /// ```
1595    ///
1596    pub fn cause(&self) -> Option<&Self> {
1597        self.data.cause.as_deref()
1598    }
1599
1600    /// Explicitly exposes the local requested target; this is not native attribution.
1601    ///
1602    /// # Examples
1603    ///
1604    /// ```
1605    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1606    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1607    /// assert!(error.request_target().is_none());
1608    /// ```
1609    ///
1610    pub fn request_target(&self) -> Option<&str> {
1611        self.data.request_target.as_deref()
1612    }
1613
1614    /// Explicitly exposes caller correlation, such as a batch custom ID.
1615    ///
1616    /// # Examples
1617    ///
1618    /// ```
1619    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1620    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1621    /// assert!(error.correlation_id().is_none());
1622    /// ```
1623    ///
1624    pub fn correlation_id(&self) -> Option<&str> {
1625        self.data.correlation_id.as_deref()
1626    }
1627
1628    /// Retains response observations without placing native values in trusted labels.
1629    ///
1630    /// # Examples
1631    ///
1632    /// ```
1633    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1634    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1635    /// let details = providers_kit::error::ResponseDetails::Json { category: providers_kit::error::JsonErrorCategory::Syntax, line: 1, column: 2 };
1636    /// let error = error.with_response_details(details);
1637    /// assert!(error.response_details().is_some());
1638    /// ```
1639    ///
1640    pub fn with_response_details(mut self, details: ResponseDetails) -> Self {
1641        self.data.response = Some(details);
1642        self.data.bound();
1643        self
1644    }
1645
1646    /// Retains an actual causal failure. Independent cleanup belongs in supplementary errors.
1647    ///
1648    /// # Examples
1649    ///
1650    /// ```
1651    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1652    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1653    /// let cause = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1654    /// let error = error.with_cause(cause);
1655    /// assert_eq!(error.cause().unwrap().kind(), ErrorKind::Timeout);
1656    /// ```
1657    ///
1658    pub fn with_cause(mut self, cause: Self) -> Self {
1659        self.data.cause = Some(Box::new(cause));
1660        self.data.bound();
1661        self
1662    }
1663
1664    /// Retains a bounded local target without asserting that the provider identified it.
1665    ///
1666    /// # Examples
1667    ///
1668    /// ```
1669    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1670    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1671    /// let error = error.with_request_target("private-resource");
1672    /// assert_eq!(error.request_target(), Some("private-resource"));
1673    /// assert!(!format!("{error}").contains("private-resource"));
1674    /// ```
1675    ///
1676    pub fn with_request_target(mut self, target: impl Into<String>) -> Self {
1677        self.data.request_target = Some(target.into());
1678        self.data.bound();
1679        self
1680    }
1681
1682    /// Retains bounded caller correlation without treating it as a missing-resource identity.
1683    ///
1684    /// # Examples
1685    ///
1686    /// ```
1687    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1688    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1689    /// let error = error.with_correlation_id("row-1");
1690    /// assert_eq!(error.correlation_id(), Some("row-1"));
1691    /// ```
1692    ///
1693    pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
1694        self.data.correlation_id = Some(id.into());
1695        self.data.bound();
1696        self
1697    }
1698
1699    /// Returns the portable classification without interpreting native prose.
1700    ///
1701    /// ```
1702    ///
1703    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1704    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1705    /// assert_eq!(error.kind(), ErrorKind::Transport);
1706    /// ```
1707    pub fn kind(&self) -> ErrorKind {
1708        self.data.failure.kind()
1709    }
1710    /// Returns safe operation, phase and batch row context.
1711    ///
1712    /// ```
1713    ///
1714    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1715    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1716    /// assert_eq!(error.context().operation, Operation::Generate);
1717    /// ```
1718    pub fn context(&self) -> &ErrorContext {
1719        &self.context
1720    }
1721    /// Returns the actual unsuccessful HTTP status, if one was received.
1722    ///
1723    /// ```
1724    ///
1725    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1726    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1727    /// assert_eq!(error.http_status(), None);
1728    /// ```
1729    pub fn http_status(&self) -> Option<u16> {
1730        self.data.status
1731    }
1732    /// Returns the local deadline that expired, if available.
1733    ///
1734    /// ```
1735    ///
1736    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1737    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1738    /// assert_eq!(error.timeout(), None);
1739    /// ```
1740    pub fn timeout(&self) -> Option<Duration> {
1741        self.data.timeout
1742    }
1743    /// Explicitly exposes potentially sensitive native details; do not log them by default.
1744    ///
1745    /// ```
1746    ///
1747    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1748    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1749    /// assert!(error.native_details().message.is_none());
1750    /// ```
1751    pub fn native_details(&self) -> &NativeErrorDetails {
1752        self.data.failure.details()
1753    }
1754    /// Returns failures collecting diagnostics or releasing resources after the primary failure.
1755    ///
1756    /// ```
1757    ///
1758    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1759    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1760    /// assert!(error.supplementary().is_empty());
1761    /// ```
1762    pub fn supplementary(&self) -> &[KitError] {
1763        &self.data.supplementary
1764    }
1765
1766    /// Attaches an observed HTTP status, never a synthetic status inferred from an event code.
1767    ///
1768    /// ```
1769    ///
1770    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1771    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1772    /// let error = error.with_http_status(503);
1773    /// assert_eq!(error.http_status(), Some(503));
1774    /// ```
1775    pub fn with_http_status(mut self, status: u16) -> Self {
1776        self.data.status = Some(status);
1777        self
1778    }
1779    /// Attaches a local deadline; service timeout responses retain their native classification.
1780    ///
1781    /// ```
1782    ///
1783    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1784    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1785    /// let error = error.with_timeout(std::time::Duration::from_secs(10));
1786    /// assert_eq!(error.timeout(), Some(std::time::Duration::from_secs(10)));
1787    /// ```
1788    pub fn with_timeout(mut self, timeout: Duration) -> Self {
1789        self.data.timeout = Some(timeout);
1790        self
1791    }
1792    /// Attaches the zero-based logical batch row, excluding blank JSONL lines.
1793    ///
1794    /// ```
1795    ///
1796    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1797    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1798    /// let error = error.with_row(2);
1799    /// assert_eq!(error.context().row, Some(2));
1800    /// ```
1801    pub fn with_row(mut self, row: usize) -> Self {
1802        self.context.row = Some(row);
1803        self
1804    }
1805    /// Attaches explicit native details, bounding each string to 64 KiB on UTF-8 boundaries.
1806    ///
1807    /// ```
1808    ///
1809    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1810    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1811    /// let details = providers_kit::error::NativeErrorDetails {
1812    ///     request_id: Some("sensitive-correlation-id".into()),
1813    ///     ..Default::default()
1814    /// };
1815    /// let error = error.with_native_details(details);
1816    /// assert!(!error.to_string().contains("sensitive-correlation-id"));
1817    /// ```
1818    pub fn with_native_details(mut self, mut details: NativeErrorDetails) -> Self {
1819        details.bound();
1820        // Replacing classification evidence is explicit caller construction; it
1821        // must not preserve a stale adapter-derived basis over unrelated details.
1822        match &mut self.data.failure {
1823            ErrorFailure::Classified {
1824                details: current, ..
1825            } => **current = details,
1826            ErrorFailure::Remote(failure) => {
1827                *failure.details = details;
1828                failure.basis = ClassificationBasis::Explicit;
1829            }
1830        }
1831        self.data.bound();
1832        self
1833    }
1834    /// Preserves a secondary failure without replacing this failure's classification or context.
1835    ///
1836    /// ```
1837    ///
1838    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1839    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1840    /// let secondary = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1841    /// let error = error.with_supplementary(secondary);
1842    /// assert_eq!(error.kind(), ErrorKind::Transport);
1843    /// assert_eq!(error.supplementary()[0].kind(), ErrorKind::Timeout);
1844    /// ```
1845    pub fn with_supplementary(mut self, error: Self) -> Self {
1846        self.data.supplementary.push(error);
1847        self.data.bound();
1848        self
1849    }
1850
1851    pub(crate) fn at_operation(mut self, operation: Operation) -> Self {
1852        self.context.operation = operation;
1853        if let Some(cause) = self.data.cause.take() {
1854            self.data.cause = Some(Box::new(
1855                if cause.context.operation == Operation::Unspecified {
1856                    cause.at_operation(operation)
1857                } else {
1858                    *cause
1859                },
1860            ));
1861        }
1862        self.data.supplementary = self
1863            .data
1864            .supplementary
1865            .into_iter()
1866            .map(|error| {
1867                if error.context.operation == Operation::Unspecified {
1868                    error.at_operation(operation)
1869                } else {
1870                    error
1871                }
1872            })
1873            .collect();
1874        self
1875    }
1876    pub(crate) fn at_phase(mut self, phase: ErrorPhase) -> Self {
1877        self.context.phase = phase;
1878        self
1879    }
1880    pub(crate) fn with_message(mut self, message: impl Into<String>) -> Self {
1881        self.data.failure.details_mut().message = Some(message.into());
1882        self.data.bound();
1883        self
1884    }
1885    pub(crate) fn with_body(mut self, body: Option<String>) -> Self {
1886        self.data.failure.details_mut().body = body;
1887        self.data.bound();
1888        self
1889    }
1890    pub(crate) fn with_diagnostic(mut self, details: Option<NativeErrorDetails>) -> Self {
1891        if let Some(details) = details {
1892            self.data.failure.details_mut().body = details.body;
1893            self.data.failure.details_mut().truncated |= details.truncated;
1894        }
1895        self.data.bound();
1896        self
1897    }
1898    // Adapter-only enrichment preserves the already-validated native basis.
1899    pub(crate) fn with_native_capture(mut self, details: NativeErrorDetails) -> Self {
1900        *self.data.failure.details_mut() = details;
1901        self.data.bound();
1902        self
1903    }
1904    pub(crate) fn config(violation: ValidationViolation, message: impl Into<String>) -> Self {
1905        Self::new(
1906            ErrorKind::Configuration,
1907            Operation::Configure,
1908            ErrorPhase::Configuration,
1909        )
1910        .with_validation_violation(violation)
1911        .with_message(message)
1912    }
1913    pub(crate) fn invalid_input(
1914        violation: ValidationViolation,
1915        message: impl Into<String>,
1916    ) -> Self {
1917        Self::new(
1918            ErrorKind::InvalidInput,
1919            Operation::ValidateInput,
1920            ErrorPhase::InputValidation,
1921        )
1922        .with_validation_violation(violation)
1923        .with_message(message)
1924    }
1925    pub(crate) fn invalid_response(reason: ResponseViolation) -> Self {
1926        Self::new(
1927            ErrorKind::InvalidResponse(reason),
1928            Operation::Unspecified,
1929            ErrorPhase::Normalize,
1930        )
1931    }
1932    pub(crate) fn unsupported(violation: ValidationViolation, message: impl Into<String>) -> Self {
1933        Self::new(
1934            ErrorKind::Unsupported,
1935            Operation::Unspecified,
1936            ErrorPhase::InputValidation,
1937        )
1938        .with_validation_violation(violation)
1939        .with_message(message)
1940    }
1941    pub(crate) fn invalid_state(message: impl Into<String>) -> Self {
1942        Self::new(
1943            ErrorKind::InvalidState,
1944            Operation::Unspecified,
1945            ErrorPhase::Lifecycle,
1946        )
1947        .with_message(message)
1948    }
1949    pub(crate) fn closed(message: impl Into<String>) -> Self {
1950        Self::new(
1951            ErrorKind::Closed,
1952            Operation::Unspecified,
1953            ErrorPhase::Lifecycle,
1954        )
1955        .with_message(message)
1956    }
1957}
1958
1959impl fmt::Display for KitError {
1960    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1961        write!(
1962            formatter,
1963            "{:?} during {:?}/{:?}",
1964            self.kind(),
1965            self.context.operation,
1966            self.context.phase
1967        )?;
1968        if let Some(violation) = self.validation_violation() {
1969            write!(formatter, " ({violation:?})")?;
1970        }
1971        if self.diagnostics_truncated() {
1972            formatter.write_str(" (diagnostics truncated)")?;
1973        }
1974        if let Some(status) = self.http_status() {
1975            write!(formatter, " (HTTP {status})")?;
1976        }
1977        if let Some(row) = self.context.row {
1978            write!(formatter, " (row {row})")?;
1979        }
1980        if let Some(timeout) = self.timeout() {
1981            write!(formatter, " (deadline {timeout:?})")?;
1982        }
1983        if !self.supplementary().is_empty() {
1984            write!(
1985                formatter,
1986                " ({} supplementary failures)",
1987                self.supplementary().len()
1988            )?;
1989        }
1990        Ok(())
1991    }
1992}
1993
1994impl fmt::Debug for KitError {
1995    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1996        formatter
1997            .debug_struct("KitError")
1998            .field("kind", &self.kind())
1999            .field("basis", &self.classification_basis())
2000            .field("context", &self.context)
2001            .field("validation", &self.validation_violation())
2002            .field("diagnostics_truncated", &self.diagnostics_truncated())
2003            .field("http_status", &self.http_status())
2004            .field("timeout", &self.timeout())
2005            .field("native", &self.native_details())
2006            .field("response", &self.response_details())
2007            .field("cause", &self.cause())
2008            .field("supplementary", &self.supplementary())
2009            .finish()
2010    }
2011}
2012
2013impl std::error::Error for KitError {
2014    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2015        if let Some(cause) = self.cause() {
2016            return Some(cause);
2017        }
2018        self.remote_failure()
2019            .map(|failure| failure as &(dyn std::error::Error + 'static))
2020    }
2021}