aion_integrations/error.rs
1//! The harness-neutral error taxonomy for the integration seam.
2//!
3//! [`HarnessError`] is the single error type every [`crate::AgentHarness`] /
4//! [`crate::AgentSession`] method returns. It is **harness-neutral**: no variant names a
5//! concrete harness, and only the transport/protocol variants reference the notion of a wire
6//! at all (as generic descriptions, never a specific protocol type). An adapter maps its own
7//! failures onto these variants; callers above the adapter branch on the variant alone.
8
9/// The neutral error taxonomy for the harness-integration seam.
10///
11/// Every arm is harness-neutral. [`Self::CapabilityNotSupported`] is the first-class outcome an
12/// observability-only harness returns from [`crate::AgentSession::intervene`] for any command —
13/// it is a legitimate, gated rejection, not an internal failure.
14#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum HarnessError {
17 /// The requested intervention primitive is not in the harness's advertised capability set.
18 ///
19 /// This is the first-class rejection an observability-only harness (empty capability set)
20 /// returns for *every* command, and the rejection any harness returns for a primitive it did
21 /// not advertise. It is a normal, expected outcome of capability gating — not a fault.
22 #[error("capability not supported: {primitive}")]
23 CapabilityNotSupported {
24 /// A neutral label naming the unsupported primitive (e.g. `"pause_resume"`).
25 primitive: String,
26 },
27 /// The command targets a stale or unknown activity attempt and is a no-op.
28 ///
29 /// A command addressed to a superseded attempt (a later attempt is now live) or to a session
30 /// that has already reached its terminal result is dropped without effect.
31 #[error("stale target: {detail}")]
32 StaleTarget {
33 /// Human-readable detail describing why the target is stale.
34 detail: String,
35 },
36 /// The underlying transport failed (spawn/connect failure, broken pipe, EOF, I/O error).
37 ///
38 /// Neutral: it describes *that* the transport failed and carries the detail, never *which*
39 /// transport. An adapter maps its own I/O failures here.
40 #[error("transport error: {detail}")]
41 Transport {
42 /// Human-readable description of the transport failure.
43 detail: String,
44 },
45 /// A message was received that violates the wire protocol contract.
46 ///
47 /// Malformed framing, an undecodable envelope, a response that correlates to no outstanding
48 /// request, or a terminal result delivered on the wrong message kind. This signals a bug in
49 /// the peer or the adapter, distinct from an ordinary transport outage.
50 #[error("protocol error: {detail}")]
51 Protocol {
52 /// Human-readable description of the protocol violation.
53 detail: String,
54 },
55 /// The harness reported an application-level failure while running the agent.
56 ///
57 /// The agent ran but ended in failure (a non-success exit, an error result, a rejected run).
58 /// Distinct from [`Self::Transport`] (the channel broke) and [`Self::Protocol`] (a malformed
59 /// message): here the channel and framing were sound and the harness *reported* failure.
60 #[error("harness reported failure: {detail}")]
61 Harness {
62 /// Human-readable description of the reported failure.
63 detail: String,
64 },
65}
66
67impl HarnessError {
68 /// Builds a [`Self::CapabilityNotSupported`] naming the unsupported primitive.
69 #[must_use]
70 pub fn capability_not_supported(primitive: impl Into<String>) -> Self {
71 Self::CapabilityNotSupported {
72 primitive: primitive.into(),
73 }
74 }
75
76 /// Builds a [`Self::StaleTarget`] with a detail message.
77 #[must_use]
78 pub fn stale_target(detail: impl Into<String>) -> Self {
79 Self::StaleTarget {
80 detail: detail.into(),
81 }
82 }
83
84 /// Builds a [`Self::Transport`] with a detail message.
85 #[must_use]
86 pub fn transport(detail: impl Into<String>) -> Self {
87 Self::Transport {
88 detail: detail.into(),
89 }
90 }
91
92 /// Builds a [`Self::Protocol`] with a detail message.
93 #[must_use]
94 pub fn protocol(detail: impl Into<String>) -> Self {
95 Self::Protocol {
96 detail: detail.into(),
97 }
98 }
99
100 /// Builds a [`Self::Harness`] with a detail message.
101 #[must_use]
102 pub fn harness(detail: impl Into<String>) -> Self {
103 Self::Harness {
104 detail: detail.into(),
105 }
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::HarnessError;
112
113 fn assert_send_sync_static<T: Send + Sync + 'static>() {}
114
115 #[test]
116 fn harness_error_is_send_sync_static() {
117 assert_send_sync_static::<HarnessError>();
118 }
119
120 #[test]
121 fn capability_not_supported_names_the_primitive() {
122 let error = HarnessError::capability_not_supported("pause_resume");
123 assert_eq!(error.to_string(), "capability not supported: pause_resume");
124 assert!(matches!(error, HarnessError::CapabilityNotSupported { .. }));
125 }
126
127 #[test]
128 fn each_constructor_renders_its_class() {
129 assert_eq!(
130 HarnessError::stale_target("attempt 2 superseded").to_string(),
131 "stale target: attempt 2 superseded"
132 );
133 assert_eq!(
134 HarnessError::transport("broken pipe").to_string(),
135 "transport error: broken pipe"
136 );
137 assert_eq!(
138 HarnessError::protocol("no matching id").to_string(),
139 "protocol error: no matching id"
140 );
141 assert_eq!(
142 HarnessError::harness("exit code 1").to_string(),
143 "harness reported failure: exit code 1"
144 );
145 }
146}