Skip to main content

ag_agent/app_server/
error.rs

1use std::error::Error;
2
3use crate::app_server_transport::AppServerTransportError;
4
5/// Typed error returned by app-server infrastructure operations.
6///
7/// Wraps transport failures, prompt rendering issues, lock errors, and
8/// provider-specific failures so callers can distinguish error categories
9/// without parsing opaque strings.
10#[derive(Debug, thiserror::Error)]
11pub enum AppServerError {
12    /// The user interrupted an in-flight app-server turn.
13    #[error("{0}")]
14    InterruptedByUser(String),
15
16    /// The session registry mutex is poisoned.
17    #[error("Failed to lock {provider} app-server session map")]
18    LockPoisoned {
19        /// Provider label for diagnostics.
20        provider: &'static str,
21    },
22
23    /// A prompt template or protocol instruction rendering failed.
24    #[error("{0}")]
25    PromptRender(String),
26
27    /// A provider-specific runtime startup or turn execution failure.
28    #[error("{0}")]
29    Provider(String),
30
31    /// Both the initial attempt and one retry after restart failed.
32    #[error(
33        "{provider} app-server failed, then retry failed after restart: first error: \
34         {first_error}; retry error: {retry_error}"
35    )]
36    RetryExhausted {
37        /// Provider label for diagnostics.
38        provider: &'static str,
39        /// Error message from the first failed attempt.
40        first_error: String,
41        /// Error message from the retry attempt after restart.
42        retry_error: String,
43    },
44
45    /// An stdio transport or process communication failure.
46    #[error(transparent)]
47    Transport(Box<dyn Error + Send + Sync>),
48}
49
50impl From<AppServerTransportError> for AppServerError {
51    fn from(error: AppServerTransportError) -> Self {
52        Self::Transport(Box::new(error))
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn lock_poisoned_display_includes_provider_name() {
62        // Arrange
63        let error = AppServerError::LockPoisoned { provider: "Codex" };
64
65        // Act
66        let display = error.to_string();
67
68        // Assert
69        assert_eq!(display, "Failed to lock Codex app-server session map");
70    }
71
72    #[test]
73    fn interrupted_by_user_display_shows_message() {
74        // Arrange
75        let error = AppServerError::InterruptedByUser("[Stopped]".to_string());
76
77        // Act / Assert
78        assert_eq!(error.to_string(), "[Stopped]");
79    }
80
81    #[test]
82    fn prompt_render_display_shows_message() {
83        // Arrange
84        let error = AppServerError::PromptRender("template syntax error".to_string());
85
86        // Act / Assert
87        assert_eq!(error.to_string(), "template syntax error");
88    }
89
90    #[test]
91    fn provider_display_shows_message() {
92        // Arrange
93        let error = AppServerError::Provider("runtime crashed".to_string());
94
95        // Act / Assert
96        assert_eq!(error.to_string(), "runtime crashed");
97    }
98
99    #[test]
100    fn retry_exhausted_display_includes_both_errors() {
101        // Arrange
102        let error = AppServerError::RetryExhausted {
103            provider: "Codex",
104            first_error: "connection reset".to_string(),
105            retry_error: "timeout".to_string(),
106        };
107
108        // Act
109        let display = error.to_string();
110
111        // Assert
112        assert_eq!(
113            display,
114            "Codex app-server failed, then retry failed after restart: first error: connection \
115             reset; retry error: timeout"
116        );
117    }
118
119    #[test]
120    fn transport_display_delegates_to_inner_error() {
121        // Arrange
122        let error = AppServerError::from(AppServerTransportError::ProcessTerminated);
123
124        // Act / Assert
125        assert_eq!(
126            error.to_string(),
127            "App-server terminated before sending expected response"
128        );
129    }
130
131    #[test]
132    fn transport_from_conversion_wraps_transport_error() {
133        // Arrange
134        let transport_error = AppServerTransportError::Timeout {
135            response_id: "init-1".to_string(),
136            timeout_seconds: 300,
137        };
138
139        // Act
140        let error: AppServerError = transport_error.into();
141
142        // Assert
143        assert!(matches!(error, AppServerError::Transport(_)));
144    }
145}