Skip to main content

agent_client_protocol/
util.rs

1// Types re-exported from crate root
2
3use futures::{
4    future::BoxFuture,
5    stream::{Stream, StreamExt},
6};
7
8mod typed;
9pub use typed::{MatchDispatch, MatchDispatchFrom, TypeNotification};
10
11/// Format only code-derived metadata at ordinary log levels.
12///
13/// Error messages, data, and even Serde parse reasons can contain private input.
14/// Do not add fields from the original error here, including purported paths
15/// extracted from its text. Full diagnostics belong in opt-in protocol tracing.
16pub(crate) fn redacted_error(code: impl Into<i32>) -> impl std::fmt::Debug {
17    struct RedactedError {
18        code: i32,
19        reason: &'static str,
20    }
21
22    impl std::fmt::Debug for RedactedError {
23        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24            f.debug_struct("Error")
25                .field("code", &self.code)
26                .field("reason", &self.reason)
27                .finish()
28        }
29    }
30
31    let code = code.into();
32    let reason = match code {
33        -32700 => "Parse error",
34        -32600 => "Invalid request",
35        -32601 => "Method not found",
36        -32602 => "Invalid params",
37        -32603 => "Internal error",
38        _ => "Protocol or application error",
39    };
40    RedactedError { code, reason }
41}
42
43/// Cast from `N` to `M` by serializing/deserialization to/from JSON.
44///
45/// Returned diagnostics can contain the input JSON and payload-bearing parse
46/// reasons. Treat them as sensitive, as with [`json_cast_params`].
47pub fn json_cast<N, M>(params: N) -> Result<M, crate::Error>
48where
49    N: serde::Serialize,
50    M: serde::de::DeserializeOwned,
51{
52    let json = serde_json::to_value(params).map_err(|e| {
53        crate::Error::parse_error().data(serde_json::json!({
54            "error": e.to_string(),
55            "phase": "serialization"
56        }))
57    })?;
58    let m = serde_json::from_value(json.clone()).map_err(|e| {
59        crate::Error::parse_error().data(serde_json::json!({
60            "error": e.to_string(),
61            "json": json,
62            "phase": "deserialization"
63        }))
64    })?;
65    Ok(m)
66}
67
68/// Cast incoming request/notification params into a typed payload.
69///
70/// Like [`json_cast`], but deserialization failures become
71/// [`Error::invalid_params`](`crate::Error::invalid_params`) (`-32602`)
72/// instead of a parse error, which is the correct JSON-RPC error code for
73/// malformed method parameters.
74///
75/// Returned diagnostics include the input JSON and may include input values in
76/// the parse reason. Treat them as sensitive; the SDK redacts errors at WARN.
77pub fn json_cast_params<N, M>(params: N) -> Result<M, crate::Error>
78where
79    N: serde::Serialize,
80    M: serde::de::DeserializeOwned,
81{
82    let json = serde_json::to_value(params).map_err(|e| {
83        crate::Error::internal_error().data(serde_json::json!({
84            "error": e.to_string(),
85            "phase": "serialization"
86        }))
87    })?;
88    let m = serde_json::from_value(json.clone()).map_err(|e| {
89        crate::Error::invalid_params().data(serde_json::json!({
90            "error": e.to_string(),
91            "json": json,
92            "phase": "deserialization"
93        }))
94    })?;
95    Ok(m)
96}
97
98/// Creates an internal error with the given message
99pub fn internal_error(message: impl ToString) -> crate::Error {
100    crate::Error::internal_error().data(message.to_string())
101}
102
103/// Creates a parse error with the given message
104pub fn parse_error(message: impl ToString) -> crate::Error {
105    crate::Error::parse_error().data(message.to_string())
106}
107
108/// No version supported by a v2 endpoint is at or below the requested version.
109///
110/// Uses the protocol wire code so legacy v1 error models can carry the v2
111/// diagnostic as an unknown error code without changing the v1 contract.
112#[must_use]
113pub fn unsupported_protocol_version(
114    requested: crate::schema::ProtocolVersion,
115    supported: &[crate::schema::ProtocolVersion],
116) -> crate::Error {
117    crate::Error::new(-32022, "Unsupported protocol version")
118        .data(serde_json::json!({ "requested": requested, "supported": supported }))
119}
120
121pub(crate) fn instrumented_with_connection_name<F>(
122    name: String,
123    task: F,
124) -> tracing::instrument::Instrumented<F> {
125    use tracing::Instrument;
126
127    task.instrument(tracing::info_span!("connection", name = name))
128}
129
130pub(crate) async fn instrument_with_connection_name<R>(
131    name: Option<String>,
132    task: impl Future<Output = R>,
133) -> R {
134    if let Some(name) = name {
135        instrumented_with_connection_name(name.clone(), task).await
136    } else {
137        task.await
138    }
139}
140
141/// Run `background` until `foreground` completes.
142///
143/// Returns the result of `foreground`. If `background` errors before
144/// `foreground` completes, the error is propagated. If `background`
145/// completes with `Ok(())`, we continue waiting for `foreground`.
146pub fn run_until<T, E>(
147    background: impl Future<Output = Result<(), E>>,
148    foreground: impl Future<Output = Result<T, E>>,
149) -> impl Future<Output = Result<T, E>> {
150    use futures::future::{Either, select};
151    use std::pin::pin;
152
153    Box::pin(async move {
154        match select(pin!(background), pin!(foreground)).await {
155            Either::Left((bg_result, fg_future)) => {
156                // Background finished first
157                bg_result?; // propagate error, or if Ok(()), keep waiting
158                fg_future.await
159            }
160            Either::Right((fg_result, _bg_future)) => {
161                // Foreground finished first, drop background
162                fg_result
163            }
164        }
165    })
166}
167
168/// Process items from a stream concurrently.
169///
170/// For each item received from `stream`, calls `process_fn` to create a future,
171/// then runs all futures concurrently. If any future returns an error,
172/// stops processing and returns that error.
173///
174/// This is useful for patterns where you receive work items from a channel
175/// and want to process them concurrently while respecting backpressure.
176pub(crate) async fn process_stream_concurrently<T, F>(
177    stream: impl Stream<Item = T>,
178    process_fn: F,
179    process_fn_hack: impl for<'a> Fn(&'a F, T) -> BoxFuture<'a, Result<(), crate::Error>>,
180) -> Result<(), crate::Error>
181where
182    F: AsyncFn(T) -> Result<(), crate::Error>,
183{
184    use std::pin::pin;
185
186    use futures::stream::{FusedStream, FuturesUnordered};
187    use futures_concurrency::future::Race;
188
189    enum Event<T> {
190        NewItem(Option<T>),
191        FutureCompleted(Option<Result<(), crate::Error>>),
192    }
193
194    let mut stream = pin!(stream.fuse());
195    let mut futures: FuturesUnordered<_> = FuturesUnordered::new();
196
197    loop {
198        // If we have no futures to run, wait until we do.
199        if futures.is_empty() {
200            match stream.next().await {
201                Some(item) => futures.push(process_fn_hack(&process_fn, item)),
202                None => return Ok(()),
203            }
204            continue;
205        }
206
207        // If there are no more items coming in, just drain our queue and return.
208        if stream.is_terminated() {
209            while let Some(result) = futures.next().await {
210                result?;
211            }
212            return Ok(());
213        }
214
215        // Otherwise, race between getting a new item and completing a future.
216        let event = (async { Event::NewItem(stream.next().await) }, async {
217            Event::FutureCompleted(futures.next().await)
218        })
219            .race()
220            .await;
221
222        match event {
223            Event::NewItem(Some(item)) => {
224                futures.push(process_fn_hack(&process_fn, item));
225            }
226            Event::FutureCompleted(Some(result)) => {
227                result?;
228            }
229            Event::NewItem(None) | Event::FutureCompleted(None) => {
230                // Stream closed, loop will catch is_terminated
231                // No futures were pending, shouldn't happen since we checked is_empty
232            }
233        }
234    }
235}