Skip to main content

agent_client_protocol/jsonrpc/
termination.rs

1//! Terminal lifecycle event for driven connections.
2
3use std::sync::{Arc, Mutex};
4
5use crate::error::ErrorOrigin;
6
7/// Why a driven connection stopped.
8///
9/// A connection driven by [`Builder::connect_to`](crate::Builder::connect_to)
10/// or [`Builder::connect_with`](crate::Builder::connect_with) publishes
11/// exactly one termination, observable through
12/// [`ConnectionTo::terminated`](crate::ConnectionTo::terminated) and
13/// [`ConnectionTo::termination`](crate::ConnectionTo::termination).
14///
15/// Unlike [`ConnectionTo::incoming_closed`](crate::ConnectionTo::incoming_closed),
16/// which only reports clean incoming EOF, termination covers every way the
17/// connection driver can end.
18#[derive(Clone, Debug, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum ConnectionTermination {
21    /// The incoming transport reached clean EOF and the driver finished
22    /// successfully.
23    ///
24    /// This is also reported when a `connect_with` foreground returns `Ok`
25    /// after observing clean EOF.
26    IncomingClosed,
27    /// The `connect_with` foreground returned `Ok` before incoming EOF,
28    /// initiating a local shutdown that completed successfully.
29    LocalShutdown,
30    /// The `connect_with` foreground returned an error.
31    ForegroundError(crate::Error),
32    /// The transport driver failed to read, write, or drain.
33    TransportError(crate::Error),
34    /// A message handler, close callback, runner, or spawned task failed.
35    HandlerError(crate::Error),
36    /// The driver future was dropped before it completed.
37    ///
38    /// Only synchronous bookkeeping happens on drop: pending requests are
39    /// failed and this termination is published. No asynchronous cleanup,
40    /// close callbacks, or output draining run after an arbitrary drop.
41    Dropped,
42}
43
44impl ConnectionTermination {
45    /// The error that ended the connection, if any.
46    #[must_use]
47    pub fn error(&self) -> Option<&crate::Error> {
48        match self {
49            Self::ForegroundError(error)
50            | Self::TransportError(error)
51            | Self::HandlerError(error) => Some(error),
52            Self::IncomingClosed | Self::LocalShutdown | Self::Dropped => None,
53        }
54    }
55
56    /// Whether the driver completed without error.
57    #[must_use]
58    pub fn is_clean(&self) -> bool {
59        matches!(self, Self::IncomingClosed | Self::LocalShutdown)
60    }
61
62    /// Payload-free discriminator stored in pending-request errors.
63    pub(super) fn kind(&self) -> &'static str {
64        match self {
65            Self::IncomingClosed => "incoming_closed",
66            Self::LocalShutdown => "local_shutdown",
67            Self::ForegroundError(_) => "foreground_error",
68            Self::TransportError(_) => "transport_error",
69            Self::HandlerError(_) => "handler_error",
70            Self::Dropped => "dropped",
71        }
72    }
73}
74
75/// Stable discriminator stored in the `data.reason` field of errors produced
76/// when a connection terminates for a reason other than clean incoming EOF
77/// before a request receives its response.
78///
79/// The `data` field is informational. Use [`is_connection_terminated`] to
80/// detect these errors: a peer can send this value in its own error data.
81pub const CONNECTION_TERMINATED_REASON: &str = "connection_terminated";
82
83/// Return whether `error` reports that the connection terminated before a
84/// request received its response.
85///
86/// Only errors the SDK produced for its own connection match. An error
87/// response received from a peer never does, even when its `data.reason` is
88/// [`CONNECTION_TERMINATED_REASON`], and neither does an error built from the
89/// same code, message, and data.
90///
91/// Requests failed by clean incoming EOF instead satisfy
92/// [`is_incoming_transport_closed`](crate::is_incoming_transport_closed).
93#[must_use]
94pub fn is_connection_terminated(error: &crate::Error) -> bool {
95    error.origin == ErrorOrigin::ConnectionTerminated
96}
97
98pub(super) fn connection_terminated_error(
99    method: &str,
100    termination: &ConnectionTermination,
101) -> crate::Error {
102    let mut error = crate::Error::internal_error();
103    error.message = "Connection terminated".to_string();
104    error.origin = ErrorOrigin::ConnectionTerminated;
105    error.data(serde_json::json!({
106        "reason": CONNECTION_TERMINATED_REASON,
107        "method": method,
108        "termination": termination.kind(),
109    }))
110}
111
112/// Which part of the driver produced an error.
113#[derive(Clone, Copy, Debug)]
114pub(super) enum TerminationSource {
115    Transport,
116    Handler,
117    Foreground,
118}
119
120/// Records errors by origin so the driver's final result can be classified.
121#[derive(Clone, Default)]
122pub(super) struct TerminationSources {
123    errors: Arc<Mutex<Vec<(TerminationSource, crate::Error)>>>,
124}
125
126impl TerminationSources {
127    pub(super) fn record<T>(&self, source: TerminationSource, result: &Result<T, crate::Error>) {
128        if let Err(error) = result {
129            self.errors
130                .lock()
131                .expect("termination sources mutex poisoned")
132                .push((source, error.clone()));
133        }
134    }
135
136    /// Classify the driver's returned error. Several actors may fail during
137    /// one shutdown; the source of the error actually returned wins.
138    pub(super) fn classify(&self, error: crate::Error) -> ConnectionTermination {
139        let source = {
140            let errors = self
141                .errors
142                .lock()
143                .expect("termination sources mutex poisoned");
144            errors
145                .iter()
146                .find(|(_, recorded)| *recorded == error)
147                .or_else(|| errors.first())
148                .map_or(TerminationSource::Handler, |(source, _)| *source)
149        };
150        match source {
151            TerminationSource::Transport => ConnectionTermination::TransportError(error),
152            TerminationSource::Handler => ConnectionTermination::HandlerError(error),
153            TerminationSource::Foreground => ConnectionTermination::ForegroundError(error),
154        }
155    }
156}