agents_kit/error.rs
1//! Portable, safely formatted failures for Agent operations.
2
3use std::fmt;
4use std::time::Duration;
5
6/// Broad portable failure category. Categories do not establish retry safety.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum ErrorKind {
9 /// Invalid passive configuration.
10 InvalidConfig,
11 /// Invalid caller request.
12 InvalidInput,
13 /// A portable resource was proven absent.
14 NotFound,
15 /// Caller input conflicts with durable facts.
16 Conflict,
17 /// The owning lifecycle is closed.
18 Closed,
19 /// Existing work prevents admission.
20 Busy,
21 /// Accepted work durably reached cancellation.
22 Cancelled,
23 /// A local deadline elapsed.
24 Timeout,
25 /// The selected implementation cannot preserve requested semantics.
26 Unsupported,
27 /// The selected adapter does not yet implement the capability.
28 NotYetImplemented,
29 /// External data contradicts its protocol.
30 Protocol,
31 /// Kit-owned facts contradict their invariants.
32 InvalidState,
33 /// A dependency operation failed.
34 Integration,
35}
36
37/// Caller-visible operation. Accepted streamed and unary work both use `Prompt`.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum Operation {
40 /// No operation was supplied by the constructor.
41 Unspecified,
42 /// Construct passive configuration.
43 Configure,
44 /// Validate passive request data.
45 ValidateInput,
46 /// Construct a new Agent.
47 Build,
48 /// Reopen an existing Agent.
49 Reopen,
50 /// List stored Agents.
51 ListAgents,
52 /// Read Agent information.
53 AgentInfo,
54 /// Create a workspace.
55 NewWorkspace,
56 /// List workspaces.
57 ListWorkspaces,
58 /// Read workspace information.
59 WorkspaceInfo,
60 /// Open a workspace handle.
61 GetWorkspace,
62 /// Check workspace update readiness.
63 CheckWorkspaceUpdate,
64 /// Update a workspace.
65 UpdateWorkspace,
66 /// Delete a workspace.
67 DeleteWorkspace,
68 /// List advertised models.
69 ListModels,
70 /// List executable tools.
71 ListTools,
72 /// Ingest managed knowledge.
73 IngestKnowledge,
74 /// Close a workspace handle.
75 CloseWorkspace,
76 /// Create a conversation.
77 NewConversation,
78 /// List conversations.
79 ListConversations,
80 /// Read conversation information.
81 ConversationInfo,
82 /// Open a conversation handle.
83 GetConversation,
84 /// Execute or observe an accepted prompt.
85 Prompt,
86 /// Abort accepted prompt work.
87 Abort,
88 /// Read a workspace-relative file.
89 ReadFile,
90 /// Receive durable notifications.
91 NotificationStream,
92 /// Resolve a pending interaction.
93 ResolveNotification,
94 /// Close a conversation.
95 CloseConversation,
96 /// Shut down an Agent.
97 Shutdown,
98}
99
100/// Semantic step at which the failure was detected.
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum ErrorPhase {
103 /// The detecting step was not specified.
104 Unspecified,
105 /// Passive configuration validation.
106 Configuration,
107 /// Request validation.
108 InputValidation,
109 /// Operation admission.
110 Admission,
111 /// Client construction.
112 ClientSetup,
113 /// Process startup.
114 ProcessStart,
115 /// Server health and protocol verification.
116 ServerVerification,
117 /// Model or tool inventory verification.
118 InventoryVerification,
119 /// Waiting for the event owner to become ready.
120 EventReady,
121 /// Event synchronization.
122 EventSynchronization,
123 /// Selecting the configured model.
124 ModelSelection,
125 /// Model generation.
126 ModelGeneration,
127 /// Executable tool work.
128 ToolExecution,
129 /// Validating a completed model turn.
130 CompletionValidation,
131 /// Reading managed knowledge.
132 KnowledgeRead,
133 /// Writing managed knowledge.
134 KnowledgeWrite,
135 /// Building a native request.
136 RequestBuild,
137 /// Sending a native request.
138 RequestSend,
139 /// Reading a response.
140 ResponseRead,
141 /// Decoding a response.
142 ResponseDecode,
143 /// Reading control-plane state.
144 StateRead,
145 /// Writing control-plane state.
146 StateWrite,
147 /// Committing control-plane state.
148 StateCommit,
149 /// Reconciling existing work.
150 Reconciliation,
151 /// Resolving an interaction.
152 InteractionResolution,
153 /// Cancelling accepted work.
154 Cancellation,
155 /// Publishing an outcome to observers.
156 Publication,
157 /// Settling an owned operation.
158 Settlement,
159 /// Releasing owned resources.
160 Cleanup,
161 /// Lifecycle admission or closure.
162 Lifecycle,
163}
164
165/// Safe operation and detection context.
166///
167/// ```
168/// use agents_kit::error::{ErrorContext, ErrorPhase, Operation};
169/// let context = ErrorContext { operation: Operation::Prompt, phase: ErrorPhase::ModelGeneration };
170/// assert_eq!(context.operation, Operation::Prompt);
171/// ```
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub struct ErrorContext {
174 /// Caller-visible operation.
175 pub operation: Operation,
176 /// Detecting semantic step.
177 pub phase: ErrorPhase,
178}
179
180/// The owner of a local deadline, without implying that side effects stopped.
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum DeadlineKind {
183 /// Construction, shutdown or another lifecycle bound.
184 Lifecycle,
185 /// One dependency request.
186 Request,
187 /// One logical operation.
188 Operation,
189 /// Waiting for an independently owned operation.
190 Observation,
191}
192
193/// Specific sourced evidence, independent of the broad recovery category.
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub enum ErrorDetail {
196 /// Authentication was rejected.
197 AuthenticationRejected,
198 /// Permission was denied.
199 PermissionDenied,
200 /// A remote request was rejected as invalid.
201 InvalidRequest,
202 /// A dependency resource was absent.
203 ResourceMissing,
204 /// A remote service reported conflict.
205 RemoteConflict,
206 /// A request exceeded an accepted size.
207 RequestTooLarge,
208 /// A remote precondition failed.
209 PreconditionFailed,
210 /// Resources were exhausted without a more precise classification.
211 ResourceExhausted,
212 /// An authoritative rate limit was reported.
213 RateLimited,
214 /// An authoritative quota limit was reported.
215 QuotaExceeded,
216 /// Billing requirements prevented execution.
217 BillingRequired,
218 /// A service was unavailable.
219 ServiceUnavailable,
220 /// A remote internal failure was reported.
221 RemoteInternal,
222 /// A remote service reported cancellation, not Agent cancellation.
223 RemoteCancelled,
224 /// A remote service reported its deadline elapsed.
225 RemoteDeadlineExceeded,
226 /// A remote rejection had no narrower supported classification.
227 RemoteRejected,
228 /// Dependency transport failed.
229 TransportFailure,
230 /// A dependency was closed, not necessarily the Agent.
231 DependencyClosed,
232 /// A dependency cancelled its work, not necessarily the Agent prompt.
233 DependencyCancelled,
234 /// An owned task terminated unexpectedly.
235 TaskTerminated,
236 /// Generation reached its output limit.
237 OutputLimit,
238 /// Generation exceeded a context limit.
239 ContextLimit,
240 /// A session budget was exhausted.
241 SessionLimit,
242 /// Generated content was filtered.
243 ContentFiltered,
244 /// A policy rejected execution.
245 PolicyRejected,
246 /// Provider-managed continuation is required.
247 ContinuationRequired,
248 /// Received data could not be decoded.
249 MalformedData,
250 /// Required response data was absent.
251 MissingData,
252 /// Received data had an invalid value or shape.
253 InvalidData,
254 /// An unknown protocol discriminator was received.
255 UnknownDiscriminator,
256 /// Response correlation did not match its owner.
257 CorrelationMismatch,
258 /// Protocol steps or fields contradicted each other.
259 InvalidSequence,
260 /// A stream ended without its required terminal.
261 IncompleteStream,
262 /// Output did not satisfy the requested schema.
263 StructuredOutputMismatch,
264 /// A completed tool result violated its output contract.
265 InvalidToolOutput,
266 /// A supplied revision was stale.
267 StaleRevision,
268 /// Reopened attachments disagreed with durable configuration.
269 AttachmentMismatch,
270 /// The StateStore reported an exact transaction conflict.
271 TransactionConflict,
272 /// Non-idempotent acceptance could not be established.
273 AcceptanceUnknown,
274 /// Exclusive runtime ownership could not be established.
275 OwnershipConflict,
276 /// A response exceeded a known configured limit.
277 ResponseLimit {
278 /// Maximum accepted size or count.
279 limit: usize,
280 },
281 /// A local deadline actually elapsed.
282 DeadlineElapsed {
283 /// The owner of the deadline.
284 kind: DeadlineKind,
285 /// Configured duration when known, never inferred from elapsed time.
286 timeout: Option<Duration>,
287 },
288}
289
290/// An opaque, safely formatted failure with bounded normalized evidence.
291///
292/// Messages supplied by callers must already be redacted. Never include native
293/// identities, routes, credentials, storage keys, payloads or foreign prose.
294///
295/// ```
296/// use agents_kit::{KitError, error::{ErrorKind, Operation, ErrorPhase, ErrorDetail}};
297/// let error = KitError::integration("model authentication was rejected")
298/// .with_context(Operation::Prompt, ErrorPhase::ModelGeneration)
299/// .with_detail(ErrorDetail::AuthenticationRejected)
300/// .with_http_status(401);
301/// assert_eq!(error.kind(), ErrorKind::Integration);
302/// assert_eq!(error.http_status(), Some(401));
303/// ```
304///
305/// Runtime failures cannot be reconstructed from untrusted serialized messages.
306/// ```compile_fail
307/// let error = agents_kit::KitError::integration("safe failure");
308/// let _ = serde_json::to_string(&error);
309/// ```
310#[derive(Clone)]
311pub struct KitError {
312 data: Box<ErrorData>,
313}
314
315#[derive(Clone)]
316struct ErrorData {
317 kind: ErrorKind,
318 context: ErrorContext,
319 message: String,
320 detail: Option<ErrorDetail>,
321 http_status: Option<u16>,
322 cause: Option<KitError>,
323 supplementary: Vec<KitError>,
324 truncated: bool,
325}
326
327impl KitError {
328 /// Constructs a classified failure with already-redacted caller context.
329 ///
330 /// ```
331 /// use agents_kit::KitError;
332 /// use agents_kit::error::{ErrorKind};
333 /// let error = KitError::new(ErrorKind::Integration, "safe dependency failure");
334 /// assert_eq!(error.kind(), ErrorKind::Integration);
335 /// ```
336 pub fn new(kind: ErrorKind, safe_message: impl Into<String>) -> Self {
337 let mut error = Self {
338 data: Box::new(ErrorData {
339 kind,
340 context: ErrorContext {
341 operation: Operation::Unspecified,
342 phase: ErrorPhase::Unspecified,
343 },
344 message: safe_message.into(),
345 detail: None,
346 http_status: None,
347 cause: None,
348 supplementary: Vec::new(),
349 truncated: false,
350 }),
351 };
352 error.bound();
353 error
354 }
355
356 /// Creates an invalid-configuration failure with safe context.
357 ///
358 /// ```
359 /// use agents_kit::KitError;
360 /// use agents_kit::error::{ErrorKind};
361 /// let error = KitError::invalid_config("safe failure context");
362 /// assert_eq!(error.kind(), ErrorKind::InvalidConfig);
363 /// ```
364 pub fn invalid_config(message: impl Into<String>) -> Self {
365 Self::new(ErrorKind::InvalidConfig, message)
366 .with_context(Operation::Configure, ErrorPhase::Configuration)
367 }
368 /// Creates an invalid-input failure with safe context.
369 ///
370 /// ```
371 /// use agents_kit::KitError;
372 /// use agents_kit::error::{ErrorKind};
373 /// let error = KitError::invalid_input("safe failure context");
374 /// assert_eq!(error.kind(), ErrorKind::InvalidInput);
375 /// ```
376 pub fn invalid_input(message: impl Into<String>) -> Self {
377 Self::new(ErrorKind::InvalidInput, message)
378 .with_context(Operation::ValidateInput, ErrorPhase::InputValidation)
379 }
380 /// Creates a proven missing-portable-resource failure.
381 ///
382 /// ```
383 /// use agents_kit::KitError;
384 /// use agents_kit::error::{ErrorKind};
385 /// let error = KitError::not_found("safe failure context");
386 /// assert_eq!(error.kind(), ErrorKind::NotFound);
387 /// ```
388 pub fn not_found(message: impl Into<String>) -> Self {
389 Self::new(ErrorKind::NotFound, message)
390 }
391 /// Creates a durable-fact conflict.
392 ///
393 /// ```
394 /// use agents_kit::KitError;
395 /// use agents_kit::error::{ErrorKind};
396 /// let error = KitError::conflict("safe failure context");
397 /// assert_eq!(error.kind(), ErrorKind::Conflict);
398 /// ```
399 pub fn conflict(message: impl Into<String>) -> Self {
400 Self::new(ErrorKind::Conflict, message)
401 }
402 /// Creates an owning-lifecycle closure failure.
403 ///
404 /// ```
405 /// use agents_kit::KitError;
406 /// use agents_kit::error::{ErrorKind};
407 /// let error = KitError::closed("safe failure context");
408 /// assert_eq!(error.kind(), ErrorKind::Closed);
409 /// ```
410 pub fn closed(message: impl Into<String>) -> Self {
411 Self::new(ErrorKind::Closed, message).at_phase(ErrorPhase::Lifecycle)
412 }
413 /// Creates a busy-admission failure.
414 ///
415 /// ```
416 /// use agents_kit::KitError;
417 /// use agents_kit::error::{ErrorKind};
418 /// let error = KitError::busy("safe failure context");
419 /// assert_eq!(error.kind(), ErrorKind::Busy);
420 /// ```
421 pub fn busy(message: impl Into<String>) -> Self {
422 Self::new(ErrorKind::Busy, message).at_phase(ErrorPhase::Admission)
423 }
424 /// Creates a durable Agent cancellation failure.
425 ///
426 /// ```
427 /// use agents_kit::KitError;
428 /// use agents_kit::error::{ErrorKind};
429 /// let error = KitError::cancelled("safe failure context");
430 /// assert_eq!(error.kind(), ErrorKind::Cancelled);
431 /// ```
432 pub fn cancelled(message: impl Into<String>) -> Self {
433 Self::new(ErrorKind::Cancelled, message).at_phase(ErrorPhase::Cancellation)
434 }
435 /// Creates a local deadline failure; attach the known owner and duration.
436 ///
437 /// ```
438 /// use agents_kit::KitError;
439 /// use agents_kit::error::{ErrorKind};
440 /// let error = KitError::timeout("safe failure context");
441 /// assert_eq!(error.kind(), ErrorKind::Timeout);
442 /// ```
443 pub fn timeout(message: impl Into<String>) -> Self {
444 Self::new(ErrorKind::Timeout, message).with_detail(ErrorDetail::DeadlineElapsed {
445 kind: DeadlineKind::Operation,
446 timeout: None,
447 })
448 }
449 /// Creates an unsupported-semantics failure.
450 ///
451 /// ```
452 /// use agents_kit::KitError;
453 /// use agents_kit::error::{ErrorKind};
454 /// let error = KitError::unsupported("safe failure context");
455 /// assert_eq!(error.kind(), ErrorKind::Unsupported);
456 /// ```
457 pub fn unsupported(message: impl Into<String>) -> Self {
458 Self::new(ErrorKind::Unsupported, message)
459 }
460 /// Creates an incomplete-adapter failure.
461 ///
462 /// ```
463 /// use agents_kit::KitError;
464 /// use agents_kit::error::{ErrorKind};
465 /// let error = KitError::not_yet_implemented("safe failure context");
466 /// assert_eq!(error.kind(), ErrorKind::NotYetImplemented);
467 /// ```
468 pub fn not_yet_implemented(message: impl Into<String>) -> Self {
469 Self::new(ErrorKind::NotYetImplemented, message)
470 }
471 /// Creates a malformed or contradictory protocol failure.
472 ///
473 /// ```
474 /// use agents_kit::KitError;
475 /// use agents_kit::error::{ErrorKind};
476 /// let error = KitError::protocol("safe failure context");
477 /// assert_eq!(error.kind(), ErrorKind::Protocol);
478 /// ```
479 pub fn protocol(message: impl Into<String>) -> Self {
480 Self::new(ErrorKind::Protocol, message)
481 }
482 /// Creates an invalid-owned-state failure.
483 ///
484 /// ```
485 /// use agents_kit::KitError;
486 /// use agents_kit::error::{ErrorKind};
487 /// let error = KitError::invalid_state("safe failure context");
488 /// assert_eq!(error.kind(), ErrorKind::InvalidState);
489 /// ```
490 pub fn invalid_state(message: impl Into<String>) -> Self {
491 Self::new(ErrorKind::InvalidState, message)
492 }
493 /// Creates a dependency execution failure.
494 ///
495 /// ```
496 /// use agents_kit::KitError;
497 /// use agents_kit::error::{ErrorKind};
498 /// let error = KitError::integration("safe failure context");
499 /// assert_eq!(error.kind(), ErrorKind::Integration);
500 /// ```
501 pub fn integration(message: impl Into<String>) -> Self {
502 Self::new(ErrorKind::Integration, message)
503 }
504 /// Attaches the caller operation and detecting phase without changing evidence.
505 ///
506 /// ```
507 /// use agents_kit::KitError;
508 /// use agents_kit::error::{Operation, ErrorPhase};
509 /// let error = KitError::protocol("missing reply").with_context(Operation::Prompt, ErrorPhase::ResponseDecode);
510 /// assert_eq!(error.context().phase, ErrorPhase::ResponseDecode);
511 /// ```
512 pub fn with_context(mut self, operation: Operation, phase: ErrorPhase) -> Self {
513 self.data.context = ErrorContext { operation, phase };
514 self
515 }
516 /// Attaches sourced portable evidence without changing the broad category.
517 ///
518 /// ```
519 /// use agents_kit::KitError;
520 /// use agents_kit::error::{ErrorDetail};
521 /// let error = KitError::protocol("missing reply").with_detail(ErrorDetail::MissingData);
522 /// assert_eq!(error.detail(), Some(&ErrorDetail::MissingData));
523 /// ```
524 pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
525 self.data.detail = Some(detail);
526 self
527 }
528 /// Attaches an observed HTTP status, never an inferred status.
529 ///
530 /// ```
531 /// use agents_kit::KitError;
532 /// let error = KitError::integration("rejected request").with_http_status(403);
533 /// assert_eq!(error.http_status(), Some(403));
534 /// ```
535 pub fn with_http_status(mut self, status: u16) -> Self {
536 self.data.http_status = Some(status);
537 self
538 }
539 /// Attaches a normalized causal failure, separately from cleanup failures.
540 ///
541 /// ```
542 /// use agents_kit::KitError;
543 /// use agents_kit::error::{ErrorKind};
544 /// let error = KitError::integration("generation failed").with_cause(KitError::timeout("dependency deadline elapsed"));
545 /// assert_eq!(error.cause().unwrap().kind(), ErrorKind::Timeout);
546 /// ```
547 pub fn with_cause(mut self, cause: Self) -> Self {
548 self.data.cause = Some(cause);
549 self.bound();
550 self
551 }
552 /// Appends independent cleanup or earlier-attempt evidence in observation order.
553 ///
554 /// ```
555 /// use agents_kit::KitError;
556 /// let error = KitError::protocol("invalid reply").with_supplementary(KitError::integration("cleanup failed"));
557 /// assert_eq!(error.supplementary().len(), 1);
558 /// ```
559 pub fn with_supplementary(mut self, error: Self) -> Self {
560 self.data.supplementary.push(error);
561 self.bound();
562 self
563 }
564 /// Returns the broad portable category.
565 ///
566 /// ```
567 /// use agents_kit::KitError;
568 /// use agents_kit::error::{ErrorKind};
569 /// assert_eq!(KitError::closed("agent shut down").kind(), ErrorKind::Closed);
570 /// ```
571 pub fn kind(&self) -> ErrorKind {
572 self.data.kind
573 }
574 /// Returns the caller operation and detecting phase.
575 ///
576 /// ```
577 /// use agents_kit::KitError;
578 /// use agents_kit::error::{Operation, ErrorPhase};
579 /// let error = KitError::protocol("invalid reply").with_context(Operation::Prompt, ErrorPhase::ResponseDecode);
580 /// assert_eq!(error.context().operation, Operation::Prompt);
581 /// ```
582 pub fn context(&self) -> &ErrorContext {
583 &self.data.context
584 }
585 /// Returns narrower evidence when a detector established it.
586 ///
587 /// ```
588 /// use agents_kit::KitError;
589 /// use agents_kit::error::{ErrorDetail};
590 /// let error = KitError::integration("output truncated").with_detail(ErrorDetail::OutputLimit);
591 /// assert_eq!(error.detail(), Some(&ErrorDetail::OutputLimit));
592 /// ```
593 pub fn detail(&self) -> Option<&ErrorDetail> {
594 self.data.detail.as_ref()
595 }
596 /// Returns the actual observed HTTP status.
597 ///
598 /// ```
599 /// use agents_kit::KitError;
600 /// assert_eq!(KitError::integration("no HTTP response").http_status(), None);
601 /// ```
602 pub fn http_status(&self) -> Option<u16> {
603 self.data.http_status
604 }
605 /// Returns the normalized causal failure.
606 ///
607 /// ```
608 /// use agents_kit::KitError;
609 /// assert!(KitError::invalid_input("empty prompt").cause().is_none());
610 /// ```
611 pub fn cause(&self) -> Option<&Self> {
612 self.data.cause.as_ref()
613 }
614 /// Returns independent supplementary failures in observation order.
615 ///
616 /// ```
617 /// use agents_kit::KitError;
618 /// assert!(KitError::invalid_input("empty prompt").supplementary().is_empty());
619 /// ```
620 pub fn supplementary(&self) -> &[Self] {
621 &self.data.supplementary
622 }
623 /// Reports whether any retained diagnostic tree evidence was truncated.
624 ///
625 /// ```
626 /// use agents_kit::KitError;
627 /// let error = KitError::integration("x".repeat(5000));
628 /// assert!(error.diagnostics_truncated());
629 /// assert_eq!(error.message().len(), 4096);
630 /// ```
631 pub fn diagnostics_truncated(&self) -> bool {
632 self.data.truncated
633 }
634 /// Returns bounded, already-redacted caller context.
635 ///
636 /// ```
637 /// use agents_kit::KitError;
638 /// let error = KitError::invalid_input("prompt must be nonempty");
639 /// assert_eq!(error.message(), "prompt must be nonempty");
640 /// ```
641 pub fn message(&self) -> &str {
642 &self.data.message
643 }
644
645 pub(crate) fn at_operation(mut self, operation: Operation) -> Self {
646 self.data.context.operation = operation;
647 self.inherit_operation(operation);
648 self
649 }
650 fn inherit_operation(&mut self, operation: Operation) {
651 if self.data.context.operation == Operation::Unspecified {
652 self.data.context.operation = operation;
653 }
654 if let Some(cause) = &mut self.data.cause {
655 cause.inherit_operation(operation);
656 }
657 for error in &mut self.data.supplementary {
658 error.inherit_operation(operation);
659 }
660 }
661 pub(crate) fn at_phase(mut self, phase: ErrorPhase) -> Self {
662 self.data.context.phase = phase;
663 self
664 }
665 pub(crate) fn in_phase(self, phase: ErrorPhase) -> Self {
666 if self.context().phase == ErrorPhase::Unspecified {
667 self.at_phase(phase)
668 } else {
669 self
670 }
671 }
672 pub(crate) fn with_diagnostics_truncated(mut self) -> Self {
673 self.data.truncated = true;
674 self
675 }
676 pub(crate) fn with_cleanup_result(self, cleanup: Result<(), Self>) -> Self {
677 match cleanup {
678 Ok(()) => self,
679 Err(error) => self.with_supplementary(error.in_phase(ErrorPhase::Cleanup)),
680 }
681 }
682
683 pub(crate) fn record_cleanup(primary: &mut Option<Self>, error: Self) {
684 let error = error.in_phase(ErrorPhase::Cleanup);
685 *primary = Some(match primary.take() {
686 Some(primary) => primary.with_supplementary(error),
687 None => error,
688 });
689 }
690
691 pub(crate) fn from_state_store(
692 source: storage_kit::StateStoreError,
693 message: &'static str,
694 ) -> Self {
695 use storage_kit::StateStoreError;
696 match source {
697 StateStoreError::TransactionConflict { .. } => {
698 Self::conflict(message).with_detail(ErrorDetail::TransactionConflict)
699 }
700 StateStoreError::TimedOut { timeout_ms, .. } => {
701 Self::timeout(message).with_detail(ErrorDetail::DeadlineElapsed {
702 kind: DeadlineKind::Operation,
703 timeout: Some(Duration::from_millis(timeout_ms)),
704 })
705 }
706 StateStoreError::Unavailable { .. } => {
707 Self::integration(message).with_detail(ErrorDetail::ServiceUnavailable)
708 }
709 StateStoreError::InvalidConfig(_) | StateStoreError::InvalidRequest(_) => {
710 Self::integration(message).with_detail(ErrorDetail::InvalidData)
711 }
712 StateStoreError::Serialization(_) => {
713 Self::integration(message).with_detail(ErrorDetail::MalformedData)
714 }
715 StateStoreError::BackendOperation { .. } => Self::integration(message),
716 }
717 }
718 pub(crate) fn from_provider(source: &providers_kit::KitError) -> Self {
719 use providers_kit::error::{
720 ErrorKind as ProviderKind, ErrorPhase as ProviderPhase, ResponseViolation,
721 };
722 let (kind, detail) = match source.kind() {
723 ProviderKind::Configuration => {
724 (ErrorKind::InvalidConfig, Some(ErrorDetail::InvalidData))
725 }
726 ProviderKind::InvalidInput => (ErrorKind::InvalidInput, Some(ErrorDetail::InvalidData)),
727 ProviderKind::Unsupported => (ErrorKind::Unsupported, None),
728 ProviderKind::Transport => {
729 (ErrorKind::Integration, Some(ErrorDetail::TransportFailure))
730 }
731 ProviderKind::Timeout => (
732 ErrorKind::Timeout,
733 Some(ErrorDetail::DeadlineElapsed {
734 kind: DeadlineKind::Request,
735 timeout: source.timeout(),
736 }),
737 ),
738 ProviderKind::Closed => (ErrorKind::Integration, Some(ErrorDetail::DependencyClosed)),
739 ProviderKind::InvalidState => (ErrorKind::InvalidState, Some(ErrorDetail::InvalidData)),
740 ProviderKind::InvalidResponse(violation) => (
741 ErrorKind::Protocol,
742 Some(match violation {
743 ResponseViolation::MalformedJson | ResponseViolation::InvalidUtf8 => {
744 ErrorDetail::MalformedData
745 }
746 ResponseViolation::MissingField { .. } => ErrorDetail::MissingData,
747 ResponseViolation::InvalidField { .. } => ErrorDetail::InvalidData,
748 ResponseViolation::UnknownVariant { .. } => ErrorDetail::UnknownDiscriminator,
749 ResponseViolation::IdentityMismatch { .. } => ErrorDetail::CorrelationMismatch,
750 ResponseViolation::ProtocolViolation { .. } => ErrorDetail::InvalidSequence,
751 ResponseViolation::UnexpectedEnd => ErrorDetail::IncompleteStream,
752 ResponseViolation::LimitExceeded { limit } => {
753 ErrorDetail::ResponseLimit { limit }
754 }
755 }),
756 ),
757 ProviderKind::InvalidRequest => {
758 (ErrorKind::Integration, Some(ErrorDetail::InvalidRequest))
759 }
760 ProviderKind::Authentication => (
761 ErrorKind::Integration,
762 Some(ErrorDetail::AuthenticationRejected),
763 ),
764 ProviderKind::PermissionDenied => {
765 (ErrorKind::Integration, Some(ErrorDetail::PermissionDenied))
766 }
767 ProviderKind::NotFound => (ErrorKind::Integration, Some(ErrorDetail::ResourceMissing)),
768 ProviderKind::Conflict => (ErrorKind::Integration, Some(ErrorDetail::RemoteConflict)),
769 ProviderKind::RequestTooLarge => {
770 (ErrorKind::Integration, Some(ErrorDetail::RequestTooLarge))
771 }
772 ProviderKind::FailedPrecondition => (
773 ErrorKind::Integration,
774 Some(ErrorDetail::PreconditionFailed),
775 ),
776 ProviderKind::ResourceExhausted => {
777 (ErrorKind::Integration, Some(ErrorDetail::ResourceExhausted))
778 }
779 ProviderKind::RateLimited => (ErrorKind::Integration, Some(ErrorDetail::RateLimited)),
780 ProviderKind::QuotaExceeded => {
781 (ErrorKind::Integration, Some(ErrorDetail::QuotaExceeded))
782 }
783 ProviderKind::Billing => (ErrorKind::Integration, Some(ErrorDetail::BillingRequired)),
784 ProviderKind::ServiceUnavailable => (
785 ErrorKind::Integration,
786 Some(ErrorDetail::ServiceUnavailable),
787 ),
788 ProviderKind::Internal => (ErrorKind::Integration, Some(ErrorDetail::RemoteInternal)),
789 ProviderKind::Cancelled => (ErrorKind::Integration, Some(ErrorDetail::RemoteCancelled)),
790 ProviderKind::DeadlineExceeded => (
791 ErrorKind::Integration,
792 Some(ErrorDetail::RemoteDeadlineExceeded),
793 ),
794 ProviderKind::ProviderRejected => {
795 (ErrorKind::Integration, Some(ErrorDetail::RemoteRejected))
796 }
797 };
798 let phase = match source.context().phase {
799 ProviderPhase::Unspecified => ErrorPhase::Unspecified,
800 ProviderPhase::Configuration => ErrorPhase::Configuration,
801 ProviderPhase::InputValidation => ErrorPhase::InputValidation,
802 ProviderPhase::RequestBuild => ErrorPhase::RequestBuild,
803 ProviderPhase::RequestSend | ProviderPhase::SessionSend => ErrorPhase::RequestSend,
804 ProviderPhase::ResponseHeaders
805 | ProviderPhase::ReadBody
806 | ProviderPhase::Stream
807 | ProviderPhase::SessionReceive => ErrorPhase::ResponseRead,
808 ProviderPhase::Decode => ErrorPhase::ResponseDecode,
809 ProviderPhase::Normalize => ErrorPhase::CompletionValidation,
810 ProviderPhase::SessionHandshake => ErrorPhase::ClientSetup,
811 ProviderPhase::Lifecycle | ProviderPhase::SessionClose => ErrorPhase::Lifecycle,
812 ProviderPhase::Cleanup => ErrorPhase::Cleanup,
813 };
814 let mut error = Self::new(kind, "provider operation failed").at_phase(phase);
815 if let Some(detail) = detail {
816 error = error.with_detail(detail);
817 }
818 if let Some(status) = source.http_status() {
819 error = error.with_http_status(status);
820 }
821 if let Some(cause) = source.cause() {
822 error = error.with_cause(Self::from_provider(cause));
823 }
824 for supplementary in source.supplementary() {
825 error = error.with_supplementary(Self::from_provider(supplementary));
826 }
827 if source.diagnostics_truncated() {
828 error = error.with_diagnostics_truncated();
829 }
830 error
831 }
832 fn bound(&mut self) {
833 self.bound_tree(1, &mut 32, &mut (64 * 1024));
834 }
835 fn bound_tree(&mut self, depth: usize, nodes: &mut usize, bytes: &mut usize) {
836 *nodes -= 1;
837 let limit = (*bytes).min(4096);
838 if self.data.message.len() > limit {
839 let mut boundary = limit;
840 while !self.data.message.is_char_boundary(boundary) {
841 boundary -= 1;
842 }
843 self.data.message.truncate(boundary);
844 self.data.truncated = true;
845 }
846 *bytes -= self.data.message.len();
847 if depth == 8 || *nodes == 0 {
848 self.data.truncated |= self.data.cause.is_some() || !self.data.supplementary.is_empty();
849 self.data.cause = None;
850 self.data.supplementary.clear();
851 return;
852 }
853 if let Some(cause) = &mut self.data.cause {
854 cause.bound_tree(depth + 1, nodes, bytes);
855 self.data.truncated |= cause.diagnostics_truncated();
856 }
857 let mut retained = 0;
858 for error in &mut self.data.supplementary {
859 if *nodes == 0 {
860 break;
861 }
862 error.bound_tree(depth + 1, nodes, bytes);
863 self.data.truncated |= error.diagnostics_truncated();
864 retained += 1;
865 }
866 self.data.truncated |= retained < self.data.supplementary.len();
867 self.data.supplementary.truncate(retained);
868 }
869}
870
871impl fmt::Display for KitError {
872 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
873 write!(
874 f,
875 "{:?} during {:?}/{:?}",
876 self.kind(),
877 self.context().operation,
878 self.context().phase
879 )?;
880 if let Some(detail) = self.detail() {
881 write!(f, ": {detail:?}")?;
882 }
883 if !self.message().is_empty() {
884 write!(f, ": {}", self.message())?;
885 }
886 if let Some(status) = self.http_status() {
887 write!(f, " (HTTP {status})")?;
888 }
889 if !self.supplementary().is_empty() {
890 write!(
891 f,
892 " ({} supplementary failures)",
893 self.supplementary().len()
894 )?;
895 }
896 if self.diagnostics_truncated() {
897 f.write_str(" (diagnostics truncated)")?;
898 }
899 Ok(())
900 }
901}
902
903impl fmt::Debug for KitError {
904 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
905 f.debug_struct("KitError")
906 .field("kind", &self.kind())
907 .field("context", self.context())
908 .field("detail", &self.detail())
909 .field("message", &self.message())
910 .field("http_status", &self.http_status())
911 .field("cause", &self.cause())
912 .field("supplementary", &self.supplementary())
913 .field("diagnostics_truncated", &self.diagnostics_truncated())
914 .finish()
915 }
916}
917
918impl std::error::Error for KitError {
919 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
920 self.cause()
921 .map(|cause| cause as &(dyn std::error::Error + 'static))
922 }
923}
924
925impl From<tools_kit::ToolError> for KitError {
926 fn from(source: tools_kit::ToolError) -> Self {
927 use tools_kit::ToolError;
928 let error = match source {
929 ToolError::Timeout { timeout_secs, .. } => Self::timeout(
930 "tool operation deadline elapsed",
931 )
932 .with_detail(ErrorDetail::DeadlineElapsed {
933 kind: DeadlineKind::Request,
934 timeout: Some(Duration::from_secs(timeout_secs)),
935 }),
936 ToolError::Closed { .. } => Self::integration("tool runtime is closed")
937 .with_detail(ErrorDetail::DependencyClosed),
938 ToolError::Cancelled { .. } => Self::integration("tool operation was cancelled")
939 .with_detail(ErrorDetail::DependencyCancelled),
940 ToolError::InvalidOutput { .. } => Self::integration("tool output is invalid")
941 .with_detail(ErrorDetail::InvalidToolOutput),
942 ToolError::Protocol { .. } => Self::protocol("tool protocol data is invalid")
943 .with_detail(ErrorDetail::MalformedData),
944 ToolError::Transport { .. } => Self::integration("tool transport failed")
945 .with_detail(ErrorDetail::TransportFailure),
946 ToolError::UnknownTool { .. } => Self::integration("selected tool is absent")
947 .with_detail(ErrorDetail::ResourceMissing),
948 ToolError::InvalidConfiguration { .. } | ToolError::DuplicateName { .. } => {
949 Self::integration("tool configuration is invalid")
950 .with_detail(ErrorDetail::InvalidData)
951 }
952 ToolError::InvalidArguments { .. }
953 | ToolError::InvalidInteraction { .. }
954 | ToolError::UndeclaredInteraction { .. } => {
955 Self::integration("tool input is invalid").with_detail(ErrorDetail::InvalidData)
956 }
957 ToolError::InteractionDeclined { .. } => {
958 Self::integration("tool interaction was declined")
959 .with_detail(ErrorDetail::PermissionDenied)
960 }
961 ToolError::Unsupported { .. } => Self::unsupported("tool operation is unsupported"),
962 ToolError::BuildCleanup { primary, cleanup } => Self::from(*primary)
963 .with_supplementary(Self::from(*cleanup).at_phase(ErrorPhase::Cleanup)),
964 };
965 error.in_phase(ErrorPhase::ToolExecution)
966 }
967}
968
969impl From<reqwest::Error> for KitError {
970 fn from(source: reqwest::Error) -> Self {
971 if source.is_timeout() {
972 Self::timeout("HTTP request deadline elapsed")
973 .with_detail(ErrorDetail::DeadlineElapsed {
974 kind: DeadlineKind::Request,
975 timeout: None,
976 })
977 .at_phase(ErrorPhase::RequestSend)
978 } else {
979 let error = Self::integration("HTTP operation failed")
980 .with_detail(ErrorDetail::TransportFailure);
981 match source.status() {
982 Some(status) => error.with_http_status(status.as_u16()),
983 None => error,
984 }
985 }
986 }
987}
988
989impl From<std::io::Error> for KitError {
990 fn from(source: std::io::Error) -> Self {
991 match source.kind() {
992 std::io::ErrorKind::TimedOut => Self::timeout("IO operation deadline elapsed"),
993 std::io::ErrorKind::PermissionDenied => Self::integration("IO permission was denied")
994 .with_detail(ErrorDetail::PermissionDenied),
995 std::io::ErrorKind::NotFound => Self::integration("IO resource was absent")
996 .with_detail(ErrorDetail::ResourceMissing),
997 _ => {
998 Self::integration("IO operation failed").with_detail(ErrorDetail::TransportFailure)
999 }
1000 }
1001 }
1002}