use agent_client_protocol as acp; use tokio::sync::{mpsc, oneshot}; use crate::message::{AcpAgentMessage, AcpClientMessage}; pub type AcpResult = Result; pub type AcpRxo = oneshot::Receiver>; pub type AcpTxo = oneshot::Sender>; pub type AcpClientRx = mpsc::UnboundedReceiver; pub type AcpClientTx = mpsc::UnboundedSender; pub type AcpAgentRx = mpsc::UnboundedReceiver; pub type AcpAgentTx = mpsc::UnboundedSender; pub fn acp_internal_error(message: impl Into) -> acp::Error { acp::Error::new(acp::ErrorCode::InternalError.into(), message) } /// The two distinct ways an [`acp_send`](crate::acp_send) round-trip can fail /// when the underlying channel is closed. Both surface as a JSON-RPC /// `INTERNAL_ERROR` (so existing callers and the wire format are unaffected); /// this typed discriminant — carried in the error's `data` — lets callers tell /// them apart WITHOUT substring-matching the human-readable `message`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AcpChannelFailure { /// The request could not be ENQUEUED: the receiver half (the peer's /// connection task) is already gone, so no peer is listening — e.g. a /// headless run with no client wired. SendFailed, /// The request was enqueued but the RESPONSE channel was dropped before a /// reply arrived: a peer received the request, then went away (disconnect / /// process exit) without answering. RecvFailed, } impl AcpChannelFailure { /// `data` object key under which [`acp_send`](crate::acp_send) records the /// kind. Namespaced so it can never collide with other `with_data` payloads. const DATA_KEY: &'static str = "xaiAcpChannelFailure"; const fn tag(self) -> &'static str { match self { Self::SendFailed => "send_failed", Self::RecvFailed => "recv_failed", } } fn from_tag(tag: &str) -> Option { match tag { "send_failed" => Some(Self::SendFailed), "recv_failed" => Some(Self::RecvFailed), _ => None, } } } /// Build the channel-closed error for [`acp_send`](crate::acp_send), tagging it /// with a typed [`AcpChannelFailure`] discriminant in `data`. The error `code` /// stays `INTERNAL_ERROR`, so this is purely additive for callers that just /// propagate the error. pub(crate) fn acp_channel_failure_error( message: impl Into, kind: AcpChannelFailure, ) -> acp::Error { acp_internal_error(message).data(serde_json::json!({ AcpChannelFailure::DATA_KEY: kind.tag() })) } /// Recover the [`AcpChannelFailure`] kind from an error, or `None` if the error /// did not originate from [`acp_send`](crate::acp_send)'s channel-closed paths /// (or predates the tag). Consumers use this instead of inspecting `message`. pub fn acp_channel_failure(err: &acp::Error) -> Option { err.data .as_ref() .and_then(|data| data.get(AcpChannelFailure::DATA_KEY)) .and_then(|value| value.as_str()) .and_then(AcpChannelFailure::from_tag) } /// Compact single-line JSON for gateway debug traces. Plain (uncolored) /// output: this feeds `tracing::debug!`, which typically lands in log files /// where ANSI colors are noise. Replaces the former `colored_json`-backed /// `color_json` (dropped to shrink the shipped dependency tree). #[doc(hidden)] pub fn compact_json(value: &T) -> String { serde_json::to_string(value).unwrap_or_default() } #[cfg(test)] mod channel_failure_tests { use super::{ AcpChannelFailure, acp, acp_channel_failure, acp_channel_failure_error, acp_internal_error, }; #[test] fn classifier_round_trips_both_kinds() { for kind in [AcpChannelFailure::SendFailed, AcpChannelFailure::RecvFailed] { let err = acp_channel_failure_error("boom", kind); // Code stays INTERNAL_ERROR for backward compatibility. assert_eq!(err.code, acp::ErrorCode::InternalError); assert_eq!(acp_channel_failure(&err), Some(kind)); } } #[test] fn classifier_none_for_untagged_errors() { assert_eq!(acp_channel_failure(&acp_internal_error("plain")), None); // A different `with_data` payload must not be misread as a channel kind. assert_eq!( acp_channel_failure(&acp::Error::invalid_params().data("unknown session id")), None ); } }