agent_client_protocol/jsonrpc.rs
1//! Core JSON-RPC server support.
2
3use agent_client_protocol_schema::v1::{
4 JsonRpcMessage as VersionedJsonRpcMessage, Notification as RpcNotification,
5 Request as RpcRequest, RequestId, Response as RpcResponse, SessionId,
6};
7
8// Types re-exported from crate root
9use serde::ser::SerializeSeq as _;
10use serde::{Deserialize, Serialize};
11use std::any::TypeId;
12use std::collections::HashMap;
13use std::fmt::Debug;
14use std::panic::Location;
15use std::pin::pin;
16use std::sync::{
17 Arc, Mutex, Weak,
18 atomic::{AtomicBool, Ordering},
19};
20use uuid::Uuid;
21
22use futures::FutureExt;
23use futures::channel::{mpsc, oneshot};
24use futures::future::{self, BoxFuture, Either};
25use futures::{AsyncRead, AsyncWrite, StreamExt};
26
27pub(crate) mod close;
28mod dynamic_handler;
29pub(crate) mod handlers;
30mod incoming_actor;
31mod outgoing_actor;
32mod protocol_compat;
33pub(crate) mod run;
34mod task_actor;
35mod transport_actor;
36
37use crate::jsonrpc::close::{ChainedClose, CloseCallback};
38pub use crate::jsonrpc::close::{HandleConnectionClose, NullClose};
39use crate::jsonrpc::dynamic_handler::DynamicHandlerMessage;
40pub use crate::jsonrpc::handlers::NullHandler;
41use crate::jsonrpc::handlers::{ChainedHandler, NamedHandler};
42use crate::jsonrpc::handlers::{MessageHandler, NotificationHandler, RequestHandler};
43use crate::jsonrpc::outgoing_actor::{OutgoingMessageTx, send_raw_message};
44use crate::jsonrpc::protocol_compat::{ProtocolCompat, ProtocolMode};
45use crate::jsonrpc::run::SpawnedRun;
46use crate::jsonrpc::run::{ChainRun, NullRun, RunWithConnectionTo};
47use crate::jsonrpc::task_actor::{Task, TaskTx};
48#[cfg(feature = "unstable_mcp_over_acp")]
49use crate::mcp_server::McpServer;
50use crate::role::HasPeer;
51use crate::role::Role;
52use crate::{Agent, Client, ConnectTo, RoleId};
53
54/// One valid JSON-RPC message carried inside a [`TransportFrame`].
55///
56/// This uses the JSON-RPC envelope types from `agent-client-protocol-schema`
57/// while keeping method params as raw, JSON-RPC-valid params at the transport boundary.
58#[derive(Debug, Clone)]
59pub enum RawJsonRpcMessage {
60 /// A JSON-RPC request with an id and expected response.
61 Request(RpcRequest<RawJsonRpcParams>),
62 /// A JSON-RPC notification without a response.
63 Notification(RpcNotification<RawJsonRpcParams>),
64 /// A JSON-RPC response to a prior request.
65 Response(RpcResponse<serde_json::Value>),
66}
67
68/// A JSON-RPC frame exchanged between protocol components and transports.
69///
70/// A frame preserves the boundary between a single JSON-RPC value and a batch.
71/// Malformed wire input is represented explicitly; transport failures are
72/// reported by the future that drives the transport rather than sent through a
73/// [`Channel`].
74#[derive(Clone, Debug)]
75pub enum TransportFrame {
76 /// One valid JSON-RPC message.
77 Single(RawJsonRpcMessage),
78 /// One malformed or invalid wire value retained for relays.
79 Malformed {
80 /// The original wire representation.
81 raw: String,
82 /// The JSON-RPC error associated with the malformed value.
83 error: crate::Error,
84 },
85 /// Entries retained from one non-empty JSON-RPC batch, kept in source order.
86 Batch(TransportBatch),
87}
88
89/// A structurally non-empty JSON-RPC batch retained across framed relays.
90#[derive(Clone, Debug)]
91pub struct TransportBatch {
92 first: TransportBatchEntry,
93 rest: Vec<TransportBatchEntry>,
94}
95
96/// One entry in a [`TransportBatch`].
97#[derive(Clone, Debug)]
98pub enum TransportBatchEntry {
99 /// A valid JSON-RPC message.
100 Message(RawJsonRpcMessage),
101 /// A malformed or invalid JSON-RPC value retained for relays.
102 Malformed {
103 /// The original JSON value.
104 raw: serde_json::Value,
105 /// The JSON-RPC error associated with the malformed value.
106 error: crate::Error,
107 },
108}
109
110pub(crate) fn is_response_only_shape(value: &serde_json::Value) -> bool {
111 value.as_object().is_some_and(|object| {
112 !object.contains_key("method")
113 && (object.contains_key("result") || object.contains_key("error"))
114 })
115}
116
117pub(crate) fn raw_is_response_only_shape(raw: &str) -> bool {
118 serde_json::from_str(raw).is_ok_and(|value| is_response_only_shape(&value))
119}
120
121impl TransportBatchEntry {
122 /// Create a valid batch entry.
123 #[must_use]
124 pub fn message(message: RawJsonRpcMessage) -> Self {
125 Self::Message(message)
126 }
127
128 /// Create a malformed batch entry.
129 #[must_use]
130 pub fn malformed(raw: serde_json::Value, error: crate::Error) -> Self {
131 Self::Malformed { raw, error }
132 }
133
134 #[cfg(test)]
135 fn as_result(&self) -> Result<&RawJsonRpcMessage, &crate::Error> {
136 match self {
137 Self::Message(message) => Ok(message),
138 Self::Malformed { error, .. } => Err(error),
139 }
140 }
141
142 fn message_ref(&self) -> Option<&RawJsonRpcMessage> {
143 match self {
144 Self::Message(message) => Some(message),
145 Self::Malformed { .. } => None,
146 }
147 }
148}
149
150impl Serialize for TransportBatchEntry {
151 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
152 where
153 S: serde::Serializer,
154 {
155 match self {
156 Self::Message(message) => message.serialize(serializer),
157 Self::Malformed { raw, .. } => raw.serialize(serializer),
158 }
159 }
160}
161
162impl TransportBatch {
163 /// Create a non-empty batch from entries.
164 ///
165 /// Returns `None` when the iterator is empty.
166 pub fn from_entries(entries: impl IntoIterator<Item = TransportBatchEntry>) -> Option<Self> {
167 let mut entries = entries.into_iter();
168 Some(Self {
169 first: entries.next()?,
170 rest: entries.collect(),
171 })
172 }
173
174 /// Create a non-empty batch from valid messages.
175 ///
176 /// Returns `None` when the iterator is empty.
177 pub fn from_messages(messages: impl IntoIterator<Item = RawJsonRpcMessage>) -> Option<Self> {
178 Self::from_entries(messages.into_iter().map(TransportBatchEntry::message))
179 }
180
181 /// Iterate over entries in source order.
182 pub fn entries(&self) -> impl Iterator<Item = &TransportBatchEntry> {
183 std::iter::once(&self.first).chain(&self.rest)
184 }
185
186 /// Iterate mutably over entries in source order.
187 pub fn entries_mut(&mut self) -> impl Iterator<Item = &mut TransportBatchEntry> {
188 std::iter::once(&mut self.first).chain(&mut self.rest)
189 }
190
191 /// Consume this batch and iterate over its entries in source order.
192 pub fn into_entries(self) -> impl Iterator<Item = TransportBatchEntry> {
193 std::iter::once(self.first).chain(self.rest)
194 }
195
196 /// Return the number of entries in this non-empty batch.
197 #[must_use]
198 pub fn len(&self) -> usize {
199 1 + self.rest.len()
200 }
201
202 /// Return whether this batch is empty.
203 ///
204 /// A `TransportBatch` is structurally non-empty, so this always returns
205 /// `false`.
206 #[must_use]
207 pub const fn is_empty(&self) -> bool {
208 false
209 }
210
211 #[cfg(test)]
212 pub(crate) fn iter_results(
213 &self,
214 ) -> impl Iterator<Item = Result<&RawJsonRpcMessage, &crate::Error>> {
215 self.entries().map(TransportBatchEntry::as_result)
216 }
217
218 fn messages(&self) -> impl Iterator<Item = &RawJsonRpcMessage> {
219 self.entries().filter_map(TransportBatchEntry::message_ref)
220 }
221}
222
223impl Serialize for TransportBatch {
224 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
225 where
226 S: serde::Serializer,
227 {
228 let mut sequence = serializer.serialize_seq(Some(1 + self.rest.len()))?;
229 sequence.serialize_element(&self.first)?;
230 for entry in &self.rest {
231 sequence.serialize_element(entry)?;
232 }
233 sequence.end()
234 }
235}
236
237impl TransportFrame {
238 fn inspect_messages(
239 &self,
240 observer: &mut impl FnMut(&RawJsonRpcMessage) -> Result<(), crate::Error>,
241 ) -> Result<(), crate::Error> {
242 match self {
243 Self::Single(message) => observer(message),
244 Self::Malformed { .. } => Ok(()),
245 Self::Batch(batch) => {
246 for message in batch.messages() {
247 observer(message)?;
248 }
249 Ok(())
250 }
251 }
252 }
253}
254
255/// Raw JSON-RPC request or notification parameters.
256///
257/// JSON-RPC params, when present, must be either an array or an object.
258#[derive(Debug, Clone, PartialEq)]
259pub enum RawJsonRpcParams {
260 /// Positional JSON-RPC params.
261 Array(Vec<serde_json::Value>),
262 /// Named JSON-RPC params.
263 Object(serde_json::Map<String, serde_json::Value>),
264}
265
266impl RawJsonRpcParams {
267 /// Convert a JSON value into JSON-RPC params.
268 pub fn from_value(value: serde_json::Value) -> Result<Option<Self>, crate::Error> {
269 match value {
270 serde_json::Value::Null => Ok(None),
271 serde_json::Value::Array(array) => Ok(Some(Self::Array(array))),
272 serde_json::Value::Object(object) => Ok(Some(Self::Object(object))),
273 _ => {
274 Err(crate::Error::invalid_params()
275 .data("JSON-RPC params must be an object or array"))
276 }
277 }
278 }
279
280 /// Convert params back into a JSON value.
281 #[must_use]
282 pub fn into_value(self) -> serde_json::Value {
283 match self {
284 Self::Array(array) => serde_json::Value::Array(array),
285 Self::Object(object) => serde_json::Value::Object(object),
286 }
287 }
288}
289
290impl Serialize for RawJsonRpcParams {
291 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
292 where
293 S: serde::Serializer,
294 {
295 match self {
296 Self::Array(array) => array.serialize(serializer),
297 Self::Object(object) => object.serialize(serializer),
298 }
299 }
300}
301
302impl<'de> Deserialize<'de> for RawJsonRpcParams {
303 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
304 where
305 D: serde::Deserializer<'de>,
306 {
307 let value = serde_json::Value::deserialize(deserializer)?;
308 match value {
309 serde_json::Value::Array(array) => Ok(Self::Array(array)),
310 serde_json::Value::Object(object) => Ok(Self::Object(object)),
311 _ => Err(serde::de::Error::custom(
312 "JSON-RPC params must be an object or array",
313 )),
314 }
315 }
316}
317
318impl RawJsonRpcMessage {
319 /// Build a raw JSON-RPC request message.
320 pub fn request(
321 method: String,
322 params: serde_json::Value,
323 id: RequestId,
324 ) -> Result<Self, crate::Error> {
325 Ok(Self::Request(RpcRequest {
326 id,
327 method: Arc::from(method),
328 params: RawJsonRpcParams::from_value(params)?,
329 }))
330 }
331
332 /// Build a raw JSON-RPC notification message.
333 pub fn notification(method: String, params: serde_json::Value) -> Result<Self, crate::Error> {
334 Ok(Self::Notification(RpcNotification {
335 method: Arc::from(method),
336 params: RawJsonRpcParams::from_value(params)?,
337 }))
338 }
339
340 /// Build a raw JSON-RPC response message.
341 #[must_use]
342 pub fn response(id: RequestId, response: Result<serde_json::Value, crate::Error>) -> Self {
343 Self::Response(RpcResponse::new(id, response))
344 }
345
346 /// The response id, if this is a response.
347 #[must_use]
348 pub fn response_id(&self) -> Option<&RequestId> {
349 match self {
350 Self::Response(RpcResponse::Result { id, .. } | RpcResponse::Error { id, .. }) => {
351 Some(id)
352 }
353 Self::Request(_) | Self::Notification(_) => None,
354 }
355 }
356}
357
358impl Serialize for RawJsonRpcMessage {
359 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360 where
361 S: serde::Serializer,
362 {
363 match self {
364 Self::Request(request) => {
365 VersionedJsonRpcMessage::wrap(request.clone()).serialize(serializer)
366 }
367 Self::Notification(notification) => {
368 VersionedJsonRpcMessage::wrap(notification.clone()).serialize(serializer)
369 }
370 Self::Response(response) => {
371 VersionedJsonRpcMessage::wrap(response.clone()).serialize(serializer)
372 }
373 }
374 }
375}
376
377impl<'de> Deserialize<'de> for RawJsonRpcMessage {
378 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
379 where
380 D: serde::Deserializer<'de>,
381 {
382 let value = serde_json::Value::deserialize(deserializer)?;
383 let Some(object) = value.as_object() else {
384 return Err(serde::de::Error::custom("invalid JSON-RPC message"));
385 };
386
387 let has_method = object.contains_key("method");
388 let has_id = object.contains_key("id");
389 let has_result = object.contains_key("result");
390 let has_error = object.contains_key("error");
391
392 if has_method && !has_result && !has_error {
393 if has_id {
394 let request = serde_json::from_value::<
395 VersionedJsonRpcMessage<RpcRequest<RawJsonRpcParams>>,
396 >(value)
397 .map_err(serde::de::Error::custom)?
398 .into_inner();
399 Ok(Self::Request(request))
400 } else {
401 let notification = serde_json::from_value::<
402 VersionedJsonRpcMessage<RpcNotification<RawJsonRpcParams>>,
403 >(value)
404 .map_err(serde::de::Error::custom)?
405 .into_inner();
406 Ok(Self::Notification(notification))
407 }
408 } else if !has_method && has_id && has_result != has_error {
409 let response = serde_json::from_value::<
410 VersionedJsonRpcMessage<RpcResponse<serde_json::Value>>,
411 >(value)
412 .map_err(serde::de::Error::custom)?
413 .into_inner();
414 Ok(Self::Response(response))
415 } else {
416 Err(serde::de::Error::custom("invalid JSON-RPC message"))
417 }
418 }
419}
420
421fn params_from_transport(params: Option<RawJsonRpcParams>) -> serde_json::Value {
422 params.map_or(serde_json::Value::Null, RawJsonRpcParams::into_value)
423}
424
425/// Handlers process incoming JSON-RPC messages on a connection.
426///
427/// When messages arrive, they flow through a chain of handlers. Each handler can
428/// either **claim** the message (handle it) or **decline** it (pass to the next handler).
429///
430/// # Message Flow
431///
432/// Messages flow through three layers of handlers in order:
433///
434/// ```text
435/// ┌─────────────────────────────────────────────────────────────────┐
436/// │ Incoming Message │
437/// └─────────────────────────────────────────────────────────────────┘
438/// │
439/// ▼
440/// ┌─────────────────────────────────────────────────────────────────┐
441/// │ 1. User Handlers (registered via on_receive_request, etc.) │
442/// │ - Tried in registration order │
443/// │ - First handler to return Handled::Yes claims the message │
444/// └─────────────────────────────────────────────────────────────────┘
445/// │ Handled::No
446/// ▼
447/// ┌─────────────────────────────────────────────────────────────────┐
448/// │ 2. Dynamic Handlers (added at runtime) │
449/// │ - Used for session-specific message handling │
450/// │ - Added via ConnectionTo::add_dynamic_handler │
451/// └─────────────────────────────────────────────────────────────────┘
452/// │ Handled::No
453/// ▼
454/// ┌─────────────────────────────────────────────────────────────────┐
455/// │ 3. Role Default Handler │
456/// │ - Fallback based on the connection's Role │
457/// │ - Handles protocol-level messages (e.g., proxy forwarding) │
458/// └─────────────────────────────────────────────────────────────────┘
459/// │ Handled::No
460/// ▼
461/// ┌─────────────────────────────────────────────────────────────────┐
462/// │ Unhandled: requests error, notifications ignored │
463/// └─────────────────────────────────────────────────────────────────┘
464/// ```
465///
466/// # The `Handled` Return Value
467///
468/// Each handler returns [`Handled`] to indicate whether it processed the message:
469///
470/// - **`Handled::Yes`** - Message was handled. No further handlers are invoked.
471/// - **`Handled::No { message, retry }`** - Message was not handled. The message
472/// (possibly modified) is passed to the next handler in the chain.
473///
474/// For convenience, handlers can return `()` which is equivalent to `Handled::Yes`.
475///
476/// # The Retry Mechanism
477///
478/// The `retry` flag in `Handled::No` controls what happens when no handler claims a message:
479///
480/// - **`retry: false`** (default) - Send a "method not found" error
481/// response immediately for requests, or ignore notifications.
482/// - **`retry: true`** - Queue the message and retry it when new dynamic handlers are added.
483///
484/// This mechanism exists because of a timing issue with sessions: when a `session/new`
485/// response is being processed, the dynamic handler for that session hasn't been registered
486/// yet, but `session/update` notifications for that session may already be arriving.
487/// By setting `retry: true`, these early notifications are queued until the session's
488/// dynamic handler is added.
489///
490/// # Handler Registration
491///
492/// Most users register handlers using the builder methods on [`Builder`]:
493///
494/// ```
495/// # use agent_client_protocol::{Agent, Client, ConnectTo};
496/// # use agent_client_protocol::schema::v1::{AgentCapabilities, InitializeRequest, InitializeResponse};
497/// # use agent_client_protocol_test::StatusUpdate;
498/// # async fn example(transport: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
499/// Agent.builder()
500/// .on_receive_request(async |req: InitializeRequest, responder, cx| {
501/// responder.respond(
502/// InitializeResponse::new(req.protocol_version)
503/// .agent_capabilities(AgentCapabilities::new()),
504/// )
505/// }, agent_client_protocol::on_receive_request!())
506/// .on_receive_notification(async |notif: StatusUpdate, cx| {
507/// // Process notification
508/// Ok(())
509/// }, agent_client_protocol::on_receive_notification!())
510/// .connect_to(transport)
511/// .await?;
512/// # Ok(())
513/// # }
514/// ```
515///
516/// The type parameter on the closure determines which messages are dispatched to it.
517/// Messages that don't match the type are automatically passed to the next handler.
518///
519/// # Implementing Custom Handlers
520///
521/// For advanced use cases, you can implement [`HandleDispatchFrom`] directly:
522///
523/// ```no_run
524/// use agent_client_protocol::{
525/// Client, ConnectionTo, Dispatch, Error, HandleDispatchFrom, Handled,
526/// };
527///
528/// struct MyHandler;
529///
530/// impl HandleDispatchFrom<Client> for MyHandler {
531/// async fn handle_dispatch_from(
532/// &mut self,
533/// message: Dispatch,
534/// _connection: ConnectionTo<Client>,
535/// ) -> Result<Handled<Dispatch>, Error> {
536/// if message.method() == "my/custom/method" {
537/// // Handle it
538/// Ok(Handled::Yes)
539/// } else {
540/// // Pass to next handler
541/// Ok(Handled::No { message, retry: false })
542/// }
543/// }
544///
545/// fn describe_chain(&self) -> impl std::fmt::Debug {
546/// "MyHandler"
547/// }
548/// }
549/// ```
550///
551/// # Important: Handlers Must Not Block
552///
553/// The connection processes messages on a single async task. While a handler is running,
554/// no other messages can be processed. For expensive operations, use [`ConnectionTo::spawn`]
555/// to run work concurrently:
556///
557/// ```
558/// # use agent_client_protocol::{Client, Agent, ConnectTo};
559/// # use agent_client_protocol_test::{expensive_operation, ProcessComplete};
560/// # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
561/// # Client.builder().connect_with(transport, async |cx| {
562/// cx.spawn({
563/// let connection = cx.clone();
564/// async move {
565/// let result = expensive_operation("data").await?;
566/// connection.send_notification(ProcessComplete { result })?;
567/// Ok(())
568/// }
569/// })?;
570/// # Ok(())
571/// # }).await?;
572/// # Ok(())
573/// # }
574/// ```
575#[allow(async_fn_in_trait)]
576/// A handler for incoming JSON-RPC messages.
577///
578/// This trait is implemented by types that can process incoming messages on a connection.
579/// Handlers are registered with a [`Builder`] and are called in order until
580/// one claims the message.
581///
582/// The type parameter is the counterpart role that messages arrive from and
583/// that the supplied [`ConnectionTo`] addresses. An agent handler therefore
584/// implements `HandleDispatchFrom<Client>`, while a client handler implements
585/// `HandleDispatchFrom<Agent>`.
586pub trait HandleDispatchFrom<Counterpart: Role>: Send {
587 /// Attempt to claim an incoming dispatch (request, notification, or response).
588 ///
589 /// # Important: do not block
590 ///
591 /// The server will not process new messages until this handler returns.
592 /// You should avoid blocking in this callback unless you wish to block the server (e.g., for rate limiting).
593 /// The recommended approach to manage expensive operations is to the [`ConnectionTo::spawn`] method available on the message context.
594 ///
595 /// # Parameters
596 ///
597 /// * `message` - The incoming message to handle.
598 /// * `connection` - The connection, used to send messages and access connection state.
599 ///
600 /// # Returns
601 ///
602 /// * `Ok(Handled::Yes)` if the message was claimed. It will not be propagated further.
603 /// * `Ok(Handled::No(message))` if not; the (possibly changed) message will be passed to the remaining handlers.
604 /// * `Err` if processing fails. Requests receive an Error Response, response
605 /// errors are routed to the local request awaiter, and notification errors
606 /// are logged without a wire reply.
607 fn handle_dispatch_from(
608 &mut self,
609 message: Dispatch,
610 connection: ConnectionTo<Counterpart>,
611 ) -> impl Future<Output = Result<Handled<Dispatch>, crate::Error>> + Send;
612
613 /// Returns a debug description of the registered handlers for diagnostics.
614 fn describe_chain(&self) -> impl std::fmt::Debug;
615}
616
617impl<Counterpart: Role, H> HandleDispatchFrom<Counterpart> for &mut H
618where
619 H: HandleDispatchFrom<Counterpart>,
620{
621 fn handle_dispatch_from(
622 &mut self,
623 message: Dispatch,
624 cx: ConnectionTo<Counterpart>,
625 ) -> impl Future<Output = Result<Handled<Dispatch>, crate::Error>> + Send {
626 H::handle_dispatch_from(self, message, cx)
627 }
628
629 fn describe_chain(&self) -> impl std::fmt::Debug {
630 H::describe_chain(self)
631 }
632}
633
634/// A JSON-RPC connection that can act as either a server, client, or both.
635///
636/// [`Builder`] provides a builder-style API for creating JSON-RPC servers and clients.
637/// You start by calling `Role.builder()` (e.g., `Client.builder()`), then add message
638/// handlers, and finally drive the connection with either [`connect_to`](Builder::connect_to)
639/// or [`connect_with`](Builder::connect_with), providing a component implementation
640/// (e.g., [`ByteStreams`] for byte streams).
641///
642/// # JSON-RPC Primer
643///
644/// JSON-RPC 2.0 has two fundamental message types:
645///
646/// * **Requests** - Messages that expect a response. They have an `id` field that gets
647/// echoed back in the response so the sender can correlate them.
648/// * **Notifications** - Fire-and-forget messages with no `id` field. The sender doesn't
649/// expect or receive a response.
650///
651/// # Type-Driven Message Dispatch
652///
653/// The handler registration methods use Rust's type system to determine which messages
654/// to handle. The type parameter you provide controls what gets dispatched to your handler:
655///
656/// ## Single Message Types
657///
658/// The simplest case - handle one specific message type:
659///
660/// ```no_run
661/// # use agent_client_protocol_test::*;
662/// # use agent_client_protocol::schema::v1::{InitializeRequest, InitializeResponse, SessionNotification};
663/// # async fn example() -> Result<(), agent_client_protocol::Error> {
664/// # let connection = mock_connection();
665/// connection
666/// .on_receive_request(async |req: InitializeRequest, responder, cx| {
667/// // Handle only InitializeRequest messages
668/// responder.respond(InitializeResponse::make())
669/// }, agent_client_protocol::on_receive_request!())
670/// .on_receive_notification(async |notif: SessionNotification, cx| {
671/// // Handle only SessionUpdate notifications
672/// Ok(())
673/// }, agent_client_protocol::on_receive_notification!())
674/// # .connect_to(agent_client_protocol_test::MockTransport).await?;
675/// # Ok(())
676/// # }
677/// ```
678///
679/// ## Enum Message Types
680///
681/// You can also handle multiple related messages with a single handler by defining an enum
682/// that implements the appropriate trait ([`JsonRpcRequest`] or [`JsonRpcNotification`]):
683///
684/// ```no_run
685/// # use agent_client_protocol_test::*;
686/// # use agent_client_protocol::{JsonRpcRequest, JsonRpcMessage, UntypedMessage};
687/// # use agent_client_protocol::schema::v1::{InitializeRequest, InitializeResponse, PromptRequest, PromptResponse};
688/// # async fn example() -> Result<(), agent_client_protocol::Error> {
689/// # let connection = mock_connection();
690/// // Define an enum for multiple request types
691/// #[derive(Debug, Clone)]
692/// enum MyRequests {
693/// Initialize(InitializeRequest),
694/// Prompt(PromptRequest),
695/// }
696///
697/// // Implement JsonRpcRequest for your enum
698/// # impl JsonRpcMessage for MyRequests {
699/// # fn matches_method(_method: &str) -> bool { false }
700/// # fn method(&self) -> &str { "myRequests" }
701/// # fn to_untyped_message(&self) -> Result<UntypedMessage, agent_client_protocol::Error> { todo!() }
702/// # fn parse_message(_method: &str, _params: &impl serde::Serialize) -> Result<Self, agent_client_protocol::Error> { Err(agent_client_protocol::Error::method_not_found()) }
703/// # }
704/// impl JsonRpcRequest for MyRequests { type Response = serde_json::Value; }
705///
706/// // Handle all variants in one place
707/// connection.on_receive_request(async |req: MyRequests, responder, cx| {
708/// match req {
709/// MyRequests::Initialize(init) => { responder.respond(serde_json::json!({})) }
710/// MyRequests::Prompt(prompt) => { responder.respond(serde_json::json!({})) }
711/// }
712/// }, agent_client_protocol::on_receive_request!())
713/// # .connect_to(agent_client_protocol_test::MockTransport).await?;
714/// # Ok(())
715/// # }
716/// ```
717///
718/// ## Mixed Message Types
719///
720/// To handle requests, notifications, and responses in one callback, use
721/// [`on_receive_dispatch`](Self::on_receive_dispatch):
722///
723/// ```no_run
724/// # use agent_client_protocol_test::*;
725/// # use agent_client_protocol::Dispatch;
726/// # use agent_client_protocol::schema::v1::{InitializeRequest, InitializeResponse, SessionNotification};
727/// # async fn example() -> Result<(), agent_client_protocol::Error> {
728/// # let connection = mock_connection();
729/// // on_receive_dispatch receives requests, notifications, and responses
730/// connection.on_receive_dispatch(async |msg: Dispatch<InitializeRequest, SessionNotification>, _cx| {
731/// match msg {
732/// Dispatch::Request(req, responder) => {
733/// responder.respond(InitializeResponse::make())
734/// }
735/// Dispatch::Notification(notif) => {
736/// Ok(())
737/// }
738/// Dispatch::Response(result, router) => {
739/// // Forward response to its destination
740/// router.route_with_result(result)
741/// }
742/// }
743/// }, agent_client_protocol::on_receive_dispatch!())
744/// # .connect_to(agent_client_protocol_test::MockTransport).await?;
745/// # Ok(())
746/// # }
747/// ```
748///
749/// # Handler Registration
750///
751/// Register handlers using these methods (listed from most common to most flexible):
752///
753/// * [`on_receive_request`](Self::on_receive_request) - Handle JSON-RPC requests (messages expecting responses)
754/// * [`on_receive_notification`](Self::on_receive_notification) - Handle JSON-RPC notifications (fire-and-forget)
755/// * [`on_receive_dispatch`](Self::on_receive_dispatch) - Handle requests, notifications, and responses in one callback
756/// * [`with_handler`](Self::with_handler) - Low-level primitive for maximum flexibility
757///
758/// ## Handler Ordering
759///
760/// Handlers are tried in the order you register them. The first handler that claims a message
761/// (by matching its type) will process it. Subsequent handlers won't see that message:
762///
763/// ```no_run
764/// # use agent_client_protocol_test::*;
765/// # use agent_client_protocol::schema::v1::{InitializeRequest, InitializeResponse, PromptRequest, PromptResponse};
766/// # async fn example() -> Result<(), agent_client_protocol::Error> {
767/// # let connection = mock_connection();
768/// connection
769/// .on_receive_request(async |req: InitializeRequest, responder, cx| {
770/// // This runs first for InitializeRequest
771/// responder.respond(InitializeResponse::make())
772/// }, agent_client_protocol::on_receive_request!())
773/// .on_receive_request(async |req: PromptRequest, responder, cx| {
774/// // This runs first for PromptRequest
775/// responder.respond(PromptResponse::make())
776/// }, agent_client_protocol::on_receive_request!())
777/// // Unknown requests receive Method not found automatically; unhandled
778/// // notifications are ignored.
779/// # .connect_to(agent_client_protocol_test::MockTransport).await?;
780/// # Ok(())
781/// # }
782/// ```
783///
784/// # Event Loop and Concurrency
785///
786/// Understanding the event loop is critical for writing correct handlers.
787///
788/// ## The Event Loop
789///
790/// [`Builder`] runs all handler callbacks on a single async task - the event loop.
791/// While a handler is running, **the server cannot receive new messages**. This means
792/// any blocking or expensive work in your handlers will stall the entire connection.
793///
794/// To avoid blocking the event loop, use [`ConnectionTo::spawn`] to offload serious
795/// work to concurrent tasks:
796///
797/// ```no_run
798/// # use agent_client_protocol_test::*;
799/// # async fn example() -> Result<(), agent_client_protocol::Error> {
800/// # let connection = mock_connection();
801/// connection.on_receive_request(async |req: AnalyzeRequest, responder, cx| {
802/// // Clone cx for the spawned task
803/// cx.spawn({
804/// let connection = cx.clone();
805/// async move {
806/// let result = expensive_analysis(&req.data).await?;
807/// connection.send_notification(AnalysisComplete { result })?;
808/// Ok(())
809/// }
810/// })?;
811///
812/// // Respond immediately without blocking
813/// responder.respond(AnalysisStarted { job_id: 42 })
814/// }, agent_client_protocol::on_receive_request!())
815/// # .connect_to(agent_client_protocol_test::MockTransport).await?;
816/// # Ok(())
817/// # }
818/// ```
819///
820/// Note that the entire connection runs within one async task, so parallelism must be
821/// managed explicitly using [`spawn`](ConnectionTo::spawn).
822///
823/// ## The Connection Context
824///
825/// Handler callbacks receive a context object (`cx`) for interacting with the connection:
826///
827/// * **For request handlers** - [`Responder<R>`] provides [`respond`](Responder::respond)
828/// to send the response, plus methods to send other messages
829/// * **For notification handlers** - [`ConnectionTo`] provides methods to send messages
830/// and spawn tasks
831///
832/// Both context types support:
833/// * [`send_request`](ConnectionTo::send_request) - Send requests to the other side
834/// * [`send_notification`](ConnectionTo::send_notification) - Send notifications
835/// * [`spawn`](ConnectionTo::spawn) - Run tasks concurrently without blocking the event loop
836///
837/// The [`SentRequest`] returned by `send_request` provides methods like
838/// [`on_receiving_result`](SentRequest::on_receiving_result) that help you
839/// avoid accidentally blocking the event loop while waiting for responses.
840///
841/// # Driving the Connection
842///
843/// After adding handlers, you must drive the connection using one of two modes:
844///
845/// ## Server Mode: `connect_to()`
846///
847/// Use [`connect_to`](Self::connect_to) when you only need to respond to incoming messages:
848///
849/// ```no_run
850/// # use agent_client_protocol_test::*;
851/// # async fn example() -> Result<(), agent_client_protocol::Error> {
852/// # let connection = mock_connection();
853/// connection
854/// .on_receive_request(async |req: MyRequest, responder, cx| {
855/// responder.respond(MyResponse { status: "ok".into() })
856/// }, agent_client_protocol::on_receive_request!())
857/// .connect_to(MockTransport) // Runs until connection closes or error occurs
858/// .await?;
859/// # Ok(())
860/// # }
861/// ```
862///
863/// The connection will process incoming messages and invoke your handlers until the
864/// connection is closed or an error occurs.
865///
866/// ## Client Mode: `connect_with()`
867///
868/// Use [`connect_with`](Self::connect_with) when you need to both handle incoming messages
869/// AND send your own requests/notifications:
870///
871/// ```no_run
872/// # use agent_client_protocol_test::*;
873/// # use agent_client_protocol::schema::v1::InitializeRequest;
874/// # async fn example() -> Result<(), agent_client_protocol::Error> {
875/// # let connection = mock_connection();
876/// connection
877/// .on_receive_request(async |req: MyRequest, responder, cx| {
878/// responder.respond(MyResponse { status: "ok".into() })
879/// }, agent_client_protocol::on_receive_request!())
880/// .connect_with(MockTransport, async |cx| {
881/// // You can send requests to the other side
882/// let response = cx.send_request(InitializeRequest::make())
883/// .block_task()
884/// .await?;
885///
886/// // And send notifications
887/// cx.send_notification(StatusUpdate { message: "ready".into() })?;
888///
889/// Ok(())
890/// })
891/// .await?;
892/// # Ok(())
893/// # }
894/// ```
895///
896/// The connection will serve incoming messages in the background while your client closure
897/// runs. When the closure returns, the connection shuts down.
898///
899/// # Example: Complete Agent
900///
901/// ```no_run
902/// # use agent_client_protocol::UntypedRole;
903/// # use agent_client_protocol::{Builder};
904/// # use agent_client_protocol::Stdio;
905/// # use agent_client_protocol::schema::v1::{InitializeRequest, InitializeResponse, PromptRequest, PromptResponse, SessionNotification};
906/// # async fn example() -> Result<(), agent_client_protocol::Error> {
907/// let transport = Stdio::new();
908///
909/// UntypedRole.builder()
910/// .name("my-agent") // Optional: for debugging logs
911/// .on_receive_request(async |init: InitializeRequest, responder, cx| {
912/// let response: InitializeResponse = todo!();
913/// responder.respond(response)
914/// }, agent_client_protocol::on_receive_request!())
915/// .on_receive_request(async |prompt: PromptRequest, responder, cx| {
916/// // You can send notifications while processing a request
917/// let notif: SessionNotification = todo!();
918/// cx.send_notification(notif)?;
919///
920/// // Then respond to the request
921/// let response: PromptResponse = todo!();
922/// responder.respond(response)
923/// }, agent_client_protocol::on_receive_request!())
924/// .connect_to(transport)
925/// .await?;
926/// # Ok(())
927/// # }
928/// ```
929#[must_use]
930#[derive(Debug)]
931pub struct Builder<Host: Role, Handler = NullHandler, Runner = NullRun, Close = NullClose>
932where
933 Handler: HandleDispatchFrom<Host::Counterpart>,
934 Runner: RunWithConnectionTo<Host::Counterpart>,
935 Close: HandleConnectionClose<Host::Counterpart>,
936{
937 /// My role.
938 host: Host,
939
940 /// Name of the connection, used in tracing logs.
941 name: Option<String>,
942
943 /// Handler for incoming messages.
944 handler: Handler,
945
946 /// Runner for background connection tasks.
947 runner: Runner,
948
949 /// Protocol version mode for the public API and wire compatibility layer.
950 protocol_mode: ProtocolMode,
951
952 /// Handler run when the incoming transport reaches clean EOF.
953 on_close: Close,
954}
955
956fn default_protocol_mode<Host: Role>() -> ProtocolMode {
957 let role = TypeId::of::<Host>();
958
959 if role == TypeId::of::<Agent>() {
960 ProtocolMode::v1_agent()
961 } else if role == TypeId::of::<Client>() {
962 ProtocolMode::v1_client()
963 } else {
964 ProtocolMode::disabled()
965 }
966}
967
968impl<Host: Role> Builder<Host, NullHandler, NullRun, NullClose> {
969 /// Create a new connection builder for the given role.
970 /// This type follows a builder pattern; use other methods to configure and then invoke
971 /// [`Self::connect_to`] (to use as a server) or [`Self::connect_with`] to use as a client.
972 pub fn new(role: Host) -> Self {
973 Self {
974 host: role,
975 name: None,
976 handler: NullHandler,
977 runner: NullRun,
978 protocol_mode: default_protocol_mode::<Host>(),
979 on_close: NullClose,
980 }
981 }
982}
983
984impl<Host: Role, Handler> Builder<Host, Handler, NullRun, NullClose>
985where
986 Handler: HandleDispatchFrom<Host::Counterpart>,
987{
988 /// Create a new connection builder with the given handler.
989 pub fn new_with(role: Host, handler: Handler) -> Self {
990 Self {
991 host: role,
992 name: None,
993 handler,
994 runner: NullRun,
995 protocol_mode: default_protocol_mode::<Host>(),
996 on_close: NullClose,
997 }
998 }
999}
1000
1001impl<
1002 Host: Role,
1003 Handler: HandleDispatchFrom<Host::Counterpart>,
1004 Runner: RunWithConnectionTo<Host::Counterpart>,
1005 Close: HandleConnectionClose<Host::Counterpart>,
1006> Builder<Host, Handler, Runner, Close>
1007{
1008 /// Set the "name" of this connection -- used only for debugging logs.
1009 pub fn name(mut self, name: impl ToString) -> Self {
1010 self.name = Some(name.to_string());
1011 self
1012 }
1013
1014 pub(crate) fn v1_agent(mut self) -> Self {
1015 self.protocol_mode = ProtocolMode::v1_agent();
1016 self
1017 }
1018
1019 pub(crate) fn v1_client(mut self) -> Self {
1020 self.protocol_mode = ProtocolMode::v1_client();
1021 self
1022 }
1023
1024 #[cfg(feature = "unstable_protocol_v2")]
1025 pub(crate) fn v2_agent(mut self) -> Self {
1026 self.protocol_mode = ProtocolMode::v2_agent();
1027 self
1028 }
1029
1030 #[cfg(feature = "unstable_protocol_v2")]
1031 pub(crate) fn v2_client(mut self) -> Self {
1032 self.protocol_mode = ProtocolMode::v2_client();
1033 self
1034 }
1035
1036 /// Merge another [`Builder`] into this one.
1037 ///
1038 /// Prefer [`Self::on_receive_request`] or [`Self::on_receive_notification`].
1039 /// This is a low-level method that is not intended for general use.
1040 pub fn with_connection_builder(
1041 self,
1042 other: Builder<
1043 Host,
1044 impl HandleDispatchFrom<Host::Counterpart>,
1045 impl RunWithConnectionTo<Host::Counterpart>,
1046 impl HandleConnectionClose<Host::Counterpart>,
1047 >,
1048 ) -> Builder<
1049 Host,
1050 impl HandleDispatchFrom<Host::Counterpart>,
1051 impl RunWithConnectionTo<Host::Counterpart>,
1052 impl HandleConnectionClose<Host::Counterpart>,
1053 > {
1054 let Builder {
1055 name: other_name,
1056 handler: other_handler,
1057 runner: other_runner,
1058 protocol_mode: other_protocol_mode,
1059 on_close: other_on_close,
1060 host: _,
1061 } = other;
1062 Builder {
1063 host: self.host,
1064 name: self.name,
1065 handler: ChainedHandler::new(
1066 self.handler,
1067 NamedHandler::new(other_name, other_handler),
1068 ),
1069 runner: ChainRun::new(self.runner, other_runner),
1070 protocol_mode: self.protocol_mode.merge(other_protocol_mode),
1071 on_close: ChainedClose::new(self.on_close, other_on_close),
1072 }
1073 }
1074
1075 /// Add a new [`HandleDispatchFrom`] to the chain.
1076 ///
1077 /// Prefer [`Self::on_receive_request`] or [`Self::on_receive_notification`].
1078 /// This is a low-level method that is not intended for general use.
1079 pub fn with_handler(
1080 self,
1081 handler: impl HandleDispatchFrom<Host::Counterpart>,
1082 ) -> Builder<Host, impl HandleDispatchFrom<Host::Counterpart>, Runner, Close> {
1083 Builder {
1084 host: self.host,
1085 name: self.name,
1086 handler: ChainedHandler::new(self.handler, handler),
1087 runner: self.runner,
1088 protocol_mode: self.protocol_mode,
1089 on_close: self.on_close,
1090 }
1091 }
1092
1093 /// Add a new [`RunWithConnectionTo`] to the chain.
1094 pub fn with_runner<Run1>(
1095 self,
1096 runner: Run1,
1097 ) -> Builder<Host, Handler, impl RunWithConnectionTo<Host::Counterpart>, Close>
1098 where
1099 Run1: RunWithConnectionTo<Host::Counterpart>,
1100 {
1101 Builder {
1102 host: self.host,
1103 name: self.name,
1104 handler: self.handler,
1105 runner: ChainRun::new(self.runner, runner),
1106 protocol_mode: self.protocol_mode,
1107 on_close: self.on_close,
1108 }
1109 }
1110
1111 /// Enqueue a task to run once the connection is actively serving traffic.
1112 #[track_caller]
1113 pub fn with_spawned<F, Fut>(
1114 self,
1115 task: F,
1116 ) -> Builder<Host, Handler, impl RunWithConnectionTo<Host::Counterpart>, Close>
1117 where
1118 F: FnOnce(ConnectionTo<Host::Counterpart>) -> Fut + Send,
1119 Fut: Future<Output = Result<(), crate::Error>> + Send,
1120 {
1121 let location = Location::caller();
1122 self.with_runner(SpawnedRun::new(location, task))
1123 }
1124
1125 /// Run a callback when the incoming transport reaches clean EOF.
1126 ///
1127 /// Each callback runs at most once and receives the connection context. A
1128 /// successful callback observes the close without otherwise changing the
1129 /// lifetime of [`connect_with`](Self::connect_with). Returning an error
1130 /// shuts down the connection and cancels a still-running `connect_with`
1131 /// future.
1132 ///
1133 /// Multiple callbacks run sequentially in registration order. All of them
1134 /// run even if an earlier callback fails, after which the first error is
1135 /// returned. Pending requests are failed before callbacks begin, while
1136 /// [`ConnectionTo::incoming_closed`] completes only after they finish. A
1137 /// callback must therefore not await that close future itself.
1138 ///
1139 /// This separation lets applications choose their cancellation policy. A
1140 /// callback can notify application-owned tasks and return `Ok(())` for
1141 /// graceful cleanup, or return an error to stop them immediately.
1142 ///
1143 /// ```
1144 /// # use agent_client_protocol::{Client, ConnectTo, Error};
1145 /// # async fn example(transport: impl ConnectTo<Client>) -> Result<(), Error> {
1146 /// Client.builder()
1147 /// .on_close(async |_cx| {
1148 /// Err(Error::internal_error().data("agent transport closed"))
1149 /// })
1150 /// .connect_with(transport, async |_cx| {
1151 /// std::future::pending().await
1152 /// })
1153 /// .await?;
1154 /// # Ok(())
1155 /// # }
1156 /// ```
1157 pub fn on_close<F, Fut>(
1158 self,
1159 callback: F,
1160 ) -> Builder<Host, Handler, Runner, impl HandleConnectionClose<Host::Counterpart>>
1161 where
1162 F: FnOnce(ConnectionTo<Host::Counterpart>) -> Fut + Send,
1163 Fut: Future<Output = Result<(), crate::Error>> + Send,
1164 {
1165 Builder {
1166 host: self.host,
1167 name: self.name,
1168 handler: self.handler,
1169 runner: self.runner,
1170 protocol_mode: self.protocol_mode,
1171 on_close: ChainedClose::new(self.on_close, CloseCallback::new(callback)),
1172 }
1173 }
1174
1175 /// Register a handler for requests, notifications, and responses.
1176 ///
1177 /// Use this when you want to handle all JSON-RPC message kinds in one callback.
1178 /// Your handler receives a [`Dispatch<Req, Notif>`] with three variants:
1179 ///
1180 /// - `Dispatch::Request(request, responder)` - A request with its response context
1181 /// - `Dispatch::Notification(notification)` - A notification
1182 /// - `Dispatch::Response(result, router)` - A response to a request we sent
1183 ///
1184 /// # Example
1185 ///
1186 /// ```no_run
1187 /// # use agent_client_protocol_test::*;
1188 /// # use agent_client_protocol::Dispatch;
1189 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
1190 /// # let connection = mock_connection();
1191 /// connection.on_receive_dispatch(async |message: Dispatch<MyRequest, StatusUpdate>, _cx| {
1192 /// match message {
1193 /// Dispatch::Request(req, responder) => {
1194 /// // Handle request and send response
1195 /// responder.respond(MyResponse { status: "ok".into() })
1196 /// }
1197 /// Dispatch::Notification(notif) => {
1198 /// // Handle notification (no response needed)
1199 /// Ok(())
1200 /// }
1201 /// Dispatch::Response(result, router) => {
1202 /// // Forward response to its destination
1203 /// router.route_with_result(result)
1204 /// }
1205 /// }
1206 /// }, agent_client_protocol::on_receive_dispatch!())
1207 /// # .connect_to(agent_client_protocol_test::MockTransport).await?;
1208 /// # Ok(())
1209 /// # }
1210 /// ```
1211 ///
1212 /// For most use cases, prefer [`on_receive_request`](Self::on_receive_request) or
1213 /// [`on_receive_notification`](Self::on_receive_notification) which provide cleaner APIs
1214 /// for handling requests or notifications separately.
1215 ///
1216 /// # Ordering
1217 ///
1218 /// This callback runs inside the dispatch loop and blocks further message processing
1219 /// until it completes. See the [`ordering`](crate::concepts::ordering) module for details on
1220 /// ordering guarantees and how to avoid deadlocks.
1221 pub fn on_receive_dispatch<Req, Notif, F, T, ToFut>(
1222 self,
1223 op: F,
1224 to_future_hack: ToFut,
1225 ) -> Builder<Host, impl HandleDispatchFrom<Host::Counterpart>, Runner, Close>
1226 where
1227 Host::Counterpart: HasPeer<Host::Counterpart>,
1228 Req: JsonRpcRequest,
1229 Notif: JsonRpcNotification,
1230 F: AsyncFnMut(
1231 Dispatch<Req, Notif>,
1232 ConnectionTo<Host::Counterpart>,
1233 ) -> Result<T, crate::Error>
1234 + Send,
1235 T: IntoHandled<Dispatch<Req, Notif>>,
1236 ToFut: Fn(
1237 &mut F,
1238 Dispatch<Req, Notif>,
1239 ConnectionTo<Host::Counterpart>,
1240 ) -> crate::BoxFuture<'_, Result<T, crate::Error>>
1241 + Send
1242 + Sync,
1243 {
1244 let handler = MessageHandler::new(
1245 self.host.counterpart(),
1246 self.host.counterpart(),
1247 op,
1248 to_future_hack,
1249 );
1250 self.with_handler(handler)
1251 }
1252
1253 /// Register a handler for JSON-RPC requests of type `Req`.
1254 ///
1255 /// Your handler receives three arguments:
1256 /// 1. The request (type `Req`)
1257 /// 2. A [`Responder<Req::Response>`] for sending the response
1258 /// 3. A [`ConnectionTo`] for the peer that sent the request
1259 ///
1260 /// The request context allows you to:
1261 /// - Send the response with [`Responder::respond`]
1262 /// - Send notifications to the client with [`ConnectionTo::send_notification`]
1263 /// - Send requests to the client with [`ConnectionTo::send_request`]
1264 ///
1265 /// # Example
1266 ///
1267 /// ```no_run
1268 /// # use agent_client_protocol::{Agent, ConnectTo};
1269 /// # use agent_client_protocol::schema::v1::{PromptRequest, PromptResponse, SessionNotification};
1270 /// # async fn example(transport: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
1271 /// Agent.builder().on_receive_request(async |request: PromptRequest, responder, cx| {
1272 /// // Send a notification while processing
1273 /// let notif: SessionNotification = todo!();
1274 /// cx.send_notification(notif)?;
1275 ///
1276 /// // Send the response
1277 /// let response: PromptResponse = todo!();
1278 /// responder.respond(response)
1279 /// }, agent_client_protocol::on_receive_request!())
1280 /// .connect_to(transport)
1281 /// .await
1282 /// # }
1283 /// ```
1284 ///
1285 /// # Type Parameter
1286 ///
1287 /// `Req` can be either a single request type or an enum of multiple request types.
1288 /// See the [type-driven dispatch](Self#type-driven-message-dispatch) section for details.
1289 ///
1290 /// # Ordering
1291 ///
1292 /// This callback runs inside the dispatch loop and blocks further message processing
1293 /// until it completes. See the [`ordering`](crate::concepts::ordering) module for details on
1294 /// ordering guarantees and how to avoid deadlocks.
1295 pub fn on_receive_request<Req: JsonRpcRequest, F, T, ToFut>(
1296 self,
1297 op: F,
1298 to_future_hack: ToFut,
1299 ) -> Builder<Host, impl HandleDispatchFrom<Host::Counterpart>, Runner, Close>
1300 where
1301 Host::Counterpart: HasPeer<Host::Counterpart>,
1302 F: AsyncFnMut(
1303 Req,
1304 Responder<Req::Response>,
1305 ConnectionTo<Host::Counterpart>,
1306 ) -> Result<T, crate::Error>
1307 + Send,
1308 T: IntoHandled<(Req, Responder<Req::Response>)>,
1309 ToFut: Fn(
1310 &mut F,
1311 Req,
1312 Responder<Req::Response>,
1313 ConnectionTo<Host::Counterpart>,
1314 ) -> crate::BoxFuture<'_, Result<T, crate::Error>>
1315 + Send
1316 + Sync,
1317 {
1318 let handler = RequestHandler::new(
1319 self.host.counterpart(),
1320 self.host.counterpart(),
1321 op,
1322 to_future_hack,
1323 );
1324 self.with_handler(handler)
1325 }
1326
1327 /// Register a handler for JSON-RPC notifications of type `Notif`.
1328 ///
1329 /// Notifications are fire-and-forget messages that don't expect a response.
1330 /// Your handler receives:
1331 /// 1. The notification (type `Notif`)
1332 /// 2. A [`ConnectionTo<R>`] for sending messages to the other side
1333 ///
1334 /// Unlike request handlers, you cannot send a response (notifications don't have IDs),
1335 /// but you can still send your own requests and notifications using the context.
1336 ///
1337 /// # Example
1338 ///
1339 /// ```no_run
1340 /// # use agent_client_protocol_test::*;
1341 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
1342 /// # let connection = mock_connection();
1343 /// connection.on_receive_notification(async |notif: SessionUpdate, cx| {
1344 /// // Process the notification
1345 /// update_session_state(¬if)?;
1346 ///
1347 /// // Optionally send a notification back
1348 /// cx.send_notification(StatusUpdate {
1349 /// message: "Acknowledged".into(),
1350 /// })?;
1351 ///
1352 /// Ok(())
1353 /// }, agent_client_protocol::on_receive_notification!())
1354 /// # .connect_to(agent_client_protocol_test::MockTransport).await?;
1355 /// # Ok(())
1356 /// # }
1357 /// ```
1358 ///
1359 /// # Type Parameter
1360 ///
1361 /// `Notif` can be either a single notification type or an enum of multiple notification types.
1362 /// See the [type-driven dispatch](Self#type-driven-message-dispatch) section for details.
1363 ///
1364 /// # Ordering
1365 ///
1366 /// This callback runs inside the dispatch loop and blocks further message processing
1367 /// until it completes. See the [`ordering`](crate::concepts::ordering) module for details on
1368 /// ordering guarantees and how to avoid deadlocks.
1369 pub fn on_receive_notification<Notif, F, T, ToFut>(
1370 self,
1371 op: F,
1372 to_future_hack: ToFut,
1373 ) -> Builder<Host, impl HandleDispatchFrom<Host::Counterpart>, Runner, Close>
1374 where
1375 Host::Counterpart: HasPeer<Host::Counterpart>,
1376 Notif: JsonRpcNotification,
1377 F: AsyncFnMut(Notif, ConnectionTo<Host::Counterpart>) -> Result<T, crate::Error> + Send,
1378 T: IntoHandled<(Notif, ConnectionTo<Host::Counterpart>)>,
1379 ToFut: Fn(
1380 &mut F,
1381 Notif,
1382 ConnectionTo<Host::Counterpart>,
1383 ) -> crate::BoxFuture<'_, Result<T, crate::Error>>
1384 + Send
1385 + Sync,
1386 {
1387 let handler = NotificationHandler::new(
1388 self.host.counterpart(),
1389 self.host.counterpart(),
1390 op,
1391 to_future_hack,
1392 );
1393 self.with_handler(handler)
1394 }
1395
1396 /// Register a handler for messages from a specific peer.
1397 ///
1398 /// This is similar to [`on_receive_dispatch`](Self::on_receive_dispatch), but allows
1399 /// specifying the source peer explicitly. This is useful when receiving messages
1400 /// from a peer that requires message transformation (e.g., unwrapping `SuccessorMessage`
1401 /// envelopes when receiving from an agent via a proxy).
1402 ///
1403 /// For the common case of receiving from the default counterpart, use
1404 /// [`on_receive_dispatch`](Self::on_receive_dispatch) instead.
1405 ///
1406 /// # Ordering
1407 ///
1408 /// This callback runs inside the dispatch loop and blocks further message processing
1409 /// until it completes. See the [`ordering`](crate::concepts::ordering) module for details on
1410 /// ordering guarantees and how to avoid deadlocks.
1411 pub fn on_receive_dispatch_from<
1412 Req: JsonRpcRequest,
1413 Notif: JsonRpcNotification,
1414 Peer: Role,
1415 F,
1416 T,
1417 ToFut,
1418 >(
1419 self,
1420 peer: Peer,
1421 op: F,
1422 to_future_hack: ToFut,
1423 ) -> Builder<Host, impl HandleDispatchFrom<Host::Counterpart>, Runner, Close>
1424 where
1425 Host::Counterpart: HasPeer<Peer>,
1426 F: AsyncFnMut(
1427 Dispatch<Req, Notif>,
1428 ConnectionTo<Host::Counterpart>,
1429 ) -> Result<T, crate::Error>
1430 + Send,
1431 T: IntoHandled<Dispatch<Req, Notif>>,
1432 ToFut: Fn(
1433 &mut F,
1434 Dispatch<Req, Notif>,
1435 ConnectionTo<Host::Counterpart>,
1436 ) -> crate::BoxFuture<'_, Result<T, crate::Error>>
1437 + Send
1438 + Sync,
1439 {
1440 let handler = MessageHandler::new(self.host.counterpart(), peer, op, to_future_hack);
1441 self.with_handler(handler)
1442 }
1443
1444 /// Register a handler for JSON-RPC requests from a specific peer.
1445 ///
1446 /// This is similar to [`on_receive_request`](Self::on_receive_request), but allows
1447 /// specifying the source peer explicitly. This is useful when receiving messages
1448 /// from a peer that requires message transformation (e.g., unwrapping `SuccessorRequest`
1449 /// envelopes when receiving from an agent via a proxy).
1450 ///
1451 /// For the common case of receiving from the default counterpart, use
1452 /// [`on_receive_request`](Self::on_receive_request) instead.
1453 ///
1454 /// # Example
1455 ///
1456 /// ```ignore
1457 /// use agent_client_protocol::Agent;
1458 /// use agent_client_protocol::schema::v1::InitializeRequest;
1459 ///
1460 /// // Conductor receiving from agent direction - messages will be unwrapped from SuccessorMessage
1461 /// connection.on_receive_request_from(Agent, async |req: InitializeRequest, responder, cx| {
1462 /// // Handle the request
1463 /// responder.respond(InitializeResponse::make())
1464 /// })
1465 /// ```
1466 ///
1467 /// # Ordering
1468 ///
1469 /// This callback runs inside the dispatch loop and blocks further message processing
1470 /// until it completes. See the [`ordering`](crate::concepts::ordering) module for details on
1471 /// ordering guarantees and how to avoid deadlocks.
1472 pub fn on_receive_request_from<Req: JsonRpcRequest, Peer: Role, F, T, ToFut>(
1473 self,
1474 peer: Peer,
1475 op: F,
1476 to_future_hack: ToFut,
1477 ) -> Builder<Host, impl HandleDispatchFrom<Host::Counterpart>, Runner, Close>
1478 where
1479 Host::Counterpart: HasPeer<Peer>,
1480 F: AsyncFnMut(
1481 Req,
1482 Responder<Req::Response>,
1483 ConnectionTo<Host::Counterpart>,
1484 ) -> Result<T, crate::Error>
1485 + Send,
1486 T: IntoHandled<(Req, Responder<Req::Response>)>,
1487 ToFut: Fn(
1488 &mut F,
1489 Req,
1490 Responder<Req::Response>,
1491 ConnectionTo<Host::Counterpart>,
1492 ) -> crate::BoxFuture<'_, Result<T, crate::Error>>
1493 + Send
1494 + Sync,
1495 {
1496 let handler = RequestHandler::new(self.host.counterpart(), peer, op, to_future_hack);
1497 self.with_handler(handler)
1498 }
1499
1500 /// Register a handler for JSON-RPC notifications from a specific peer.
1501 ///
1502 /// This is similar to [`on_receive_notification`](Self::on_receive_notification), but allows
1503 /// specifying the source peer explicitly. This is useful when receiving messages
1504 /// from a peer that requires message transformation (e.g., unwrapping `SuccessorNotification`
1505 /// envelopes when receiving from an agent via a proxy).
1506 ///
1507 /// For the common case of receiving from the default counterpart, use
1508 /// [`on_receive_notification`](Self::on_receive_notification) instead.
1509 ///
1510 /// # Ordering
1511 ///
1512 /// This callback runs inside the dispatch loop and blocks further message processing
1513 /// until it completes. See the [`ordering`](crate::concepts::ordering) module for details on
1514 /// ordering guarantees and how to avoid deadlocks.
1515 pub fn on_receive_notification_from<Notif: JsonRpcNotification, Peer: Role, F, T, ToFut>(
1516 self,
1517 peer: Peer,
1518 op: F,
1519 to_future_hack: ToFut,
1520 ) -> Builder<Host, impl HandleDispatchFrom<Host::Counterpart>, Runner, Close>
1521 where
1522 Host::Counterpart: HasPeer<Peer>,
1523 F: AsyncFnMut(Notif, ConnectionTo<Host::Counterpart>) -> Result<T, crate::Error> + Send,
1524 T: IntoHandled<(Notif, ConnectionTo<Host::Counterpart>)>,
1525 ToFut: Fn(
1526 &mut F,
1527 Notif,
1528 ConnectionTo<Host::Counterpart>,
1529 ) -> crate::BoxFuture<'_, Result<T, crate::Error>>
1530 + Send
1531 + Sync,
1532 {
1533 let handler = NotificationHandler::new(self.host.counterpart(), peer, op, to_future_hack);
1534 self.with_handler(handler)
1535 }
1536
1537 /// Add an MCP server to session setup requests proxied through this connection.
1538 ///
1539 /// The same native MCP server declaration is added to new, load, and resume
1540 /// requests, plus fork requests when `unstable_session_fork` is enabled.
1541 ///
1542 /// Only applicable to proxies.
1543 #[cfg(feature = "unstable_mcp_over_acp")]
1544 #[cfg_attr(docsrs, doc(cfg(feature = "unstable_mcp_over_acp")))]
1545 pub fn with_mcp_server(
1546 self,
1547 mcp_server: McpServer<Host::Counterpart, impl RunWithConnectionTo<Host::Counterpart>>,
1548 ) -> Builder<
1549 Host,
1550 impl HandleDispatchFrom<Host::Counterpart>,
1551 impl RunWithConnectionTo<Host::Counterpart>,
1552 Close,
1553 >
1554 where
1555 Host::Counterpart: HasPeer<Agent> + HasPeer<Client>,
1556 {
1557 let (handler, runner) = mcp_server.into_handler_and_runner();
1558 self.with_handler(handler).with_runner(runner)
1559 }
1560
1561 /// Run in server mode with the provided transport.
1562 ///
1563 /// This drives the connection by continuously processing messages from the transport
1564 /// and dispatching them to your registered handlers. The connection will run until:
1565 /// - The transport closes (e.g., EOF on byte streams)
1566 /// - An error occurs
1567 ///
1568 /// Handler errors are normally contained: requests receive an Error Response,
1569 /// response-handler errors are routed to the pending local request, and
1570 /// notification errors are logged without a wire reply.
1571 ///
1572 /// On clean EOF, messages already accepted by the outgoing queue—including
1573 /// handler responses and close-callback notifications—are drained through
1574 /// the transport sink before this returns `Ok(())`.
1575 ///
1576 /// The transport boundary carries [`TransportFrame`] values. Physical stream adapters
1577 /// serialize and deserialize frames, while channel-based components relay them directly.
1578 ///
1579 /// Use this mode when you only need to respond to incoming messages and don't need
1580 /// to initiate your own requests. If you need to send requests to the other side,
1581 /// use [`connect_with`](Self::connect_with) instead.
1582 ///
1583 /// # Example: Byte Stream Transport
1584 ///
1585 /// ```no_run
1586 /// # use agent_client_protocol::UntypedRole;
1587 /// # use agent_client_protocol::{Builder};
1588 /// # use agent_client_protocol::Stdio;
1589 /// # use agent_client_protocol_test::*;
1590 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
1591 /// let transport = Stdio::new();
1592 ///
1593 /// UntypedRole.builder()
1594 /// .on_receive_request(async |req: MyRequest, responder, cx| {
1595 /// responder.respond(MyResponse { status: "ok".into() })
1596 /// }, agent_client_protocol::on_receive_request!())
1597 /// .connect_to(transport)
1598 /// .await?;
1599 /// # Ok(())
1600 /// # }
1601 /// ```
1602 pub async fn connect_to(
1603 self,
1604 transport: impl ConnectTo<Host> + 'static,
1605 ) -> Result<(), crate::Error> {
1606 self.connect_with(transport, async move |cx| {
1607 cx.incoming_closed().await;
1608 cx.drain_outgoing().await
1609 })
1610 .await
1611 }
1612
1613 /// Run the connection until the provided closure completes.
1614 ///
1615 /// This drives the connection by:
1616 /// 1. Running your registered handlers in the background to process incoming messages
1617 /// 2. Executing your `main_fn` closure with a [`ConnectionTo<R>`] for sending requests/notifications
1618 ///
1619 /// The connection stays active until your `main_fn` returns, then shuts down.
1620 /// Clean incoming EOF fails every pending request and makes future
1621 /// requests fail immediately. It does not cancel unrelated work in
1622 /// `main_fn`: that future may observe [`ConnectionTo::incoming_closed`], or
1623 /// the builder can use [`on_close`](Self::on_close) to notify it or return
1624 /// an error and stop it.
1625 ///
1626 /// Use this mode when you need to initiate communication (send requests/notifications)
1627 /// in addition to responding to incoming messages. For server-only mode where you just
1628 /// respond to messages, use [`connect_to`](Self::connect_to) instead.
1629 ///
1630 /// # Example
1631 ///
1632 /// ```no_run
1633 /// # use agent_client_protocol::UntypedRole;
1634 /// # use agent_client_protocol::{Builder};
1635 /// # use agent_client_protocol::ByteStreams;
1636 /// # use agent_client_protocol::schema::v1::InitializeRequest;
1637 /// # use agent_client_protocol::Stdio;
1638 /// # use agent_client_protocol_test::*;
1639 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
1640 /// let transport = Stdio::new();
1641 ///
1642 /// UntypedRole.builder()
1643 /// .on_receive_request(async |req: MyRequest, responder, cx| {
1644 /// // Handle incoming requests in the background
1645 /// responder.respond(MyResponse { status: "ok".into() })
1646 /// }, agent_client_protocol::on_receive_request!())
1647 /// .connect_with(transport, async |cx| {
1648 /// // Initialize the protocol
1649 /// let init_response = cx.send_request(InitializeRequest::make())
1650 /// .block_task()
1651 /// .await?;
1652 ///
1653 /// // Send more requests...
1654 /// let result = cx.send_request(MyRequest {})
1655 /// .block_task()
1656 /// .await?;
1657 ///
1658 /// // When this closure returns, the connection shuts down
1659 /// Ok(())
1660 /// })
1661 /// .await?;
1662 /// # Ok(())
1663 /// # }
1664 /// ```
1665 ///
1666 /// # Parameters
1667 ///
1668 /// - `main_fn`: Your client logic. Receives a [`ConnectionTo<R>`] for sending messages.
1669 ///
1670 /// # Errors
1671 ///
1672 /// Returns an error if a handler, background task, transport, or close
1673 /// callback fails, or if `main_fn` returns an error. Clean incoming EOF is
1674 /// observable through [`ConnectionTo::incoming_closed`] and is not itself
1675 /// an error in this mode.
1676 pub async fn connect_with<R>(
1677 self,
1678 transport: impl ConnectTo<Host> + 'static,
1679 main_fn: impl AsyncFnOnce(ConnectionTo<Host::Counterpart>) -> Result<R, crate::Error>,
1680 ) -> Result<R, crate::Error> {
1681 let (_, future) = self.into_connection_and_future(transport, main_fn);
1682 future.await
1683 }
1684
1685 /// Helper that returns a [`ConnectionTo<R>`] and a future that runs this connection until `main_fn` returns.
1686 fn into_connection_and_future<R>(
1687 self,
1688 transport: impl ConnectTo<Host> + 'static,
1689 main_fn: impl AsyncFnOnce(ConnectionTo<Host::Counterpart>) -> Result<R, crate::Error>,
1690 ) -> (
1691 ConnectionTo<Host::Counterpart>,
1692 impl Future<Output = Result<R, crate::Error>>,
1693 ) {
1694 let Self {
1695 name,
1696 handler,
1697 runner,
1698 host: me,
1699 protocol_mode,
1700 on_close,
1701 } = self;
1702
1703 let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
1704 let (new_task_tx, new_task_rx) = mpsc::unbounded();
1705 let (dynamic_handler_tx, dynamic_handler_rx) = mpsc::unbounded();
1706 let pending_replies = PendingReplies::default();
1707
1708 // Convert transport into server - this returns a channel for us to use
1709 // and a future that runs the transport.
1710 let transport_component = crate::DynConnectTo::new(transport);
1711 let (transport_channel, transport_future) = transport_component.into_channel_and_future();
1712 let (transport_completion_tx, transport_completion_rx) = oneshot::channel();
1713 let transport_completion = transport_completion_rx
1714 .map(|result| {
1715 result.unwrap_or_else(|error| {
1716 Err(crate::util::internal_error(format!(
1717 "transport task dropped before reporting completion: {error}"
1718 )))
1719 })
1720 })
1721 .boxed()
1722 .shared();
1723
1724 let connection = ConnectionTo::new(
1725 me.counterpart(),
1726 outgoing_tx,
1727 new_task_tx,
1728 dynamic_handler_tx,
1729 transport_completion,
1730 pending_replies.registrar(),
1731 );
1732 let spawn_result = connection.spawn(async move {
1733 let result = transport_future.await;
1734 drop(transport_completion_tx.send(result.clone()));
1735 result
1736 });
1737
1738 // Destructure the channel endpoints
1739 let Channel {
1740 rx: transport_incoming_rx,
1741 tx: transport_outgoing_tx,
1742 } = transport_channel;
1743
1744 let protocol_compat = ProtocolCompat::new(protocol_mode);
1745
1746 let future = crate::util::instrument_with_connection_name(name, {
1747 let connection = connection.clone();
1748 async move {
1749 let () = spawn_result?;
1750
1751 let background = async {
1752 let incoming = incoming_actor::incoming_protocol_actor(
1753 me.counterpart(),
1754 &connection,
1755 transport_incoming_rx,
1756 dynamic_handler_rx,
1757 pending_replies.clone(),
1758 incoming_actor::IncomingHandlers::new(handler, on_close),
1759 protocol_compat.clone(),
1760 );
1761 let other_actors = async {
1762 futures::try_join!(
1763 // Protocol layer: OutgoingMessage -> RawJsonRpcMessage
1764 outgoing_actor::outgoing_protocol_actor(
1765 outgoing_rx,
1766 pending_replies,
1767 transport_outgoing_tx,
1768 protocol_compat,
1769 ),
1770 task_actor::task_actor(new_task_rx, &connection),
1771 runner.run_with_connection_to(connection.clone()),
1772 )?;
1773 Ok(())
1774 };
1775
1776 // EOF can wake a pending request consumer, which may make
1777 // the task actor fail while close callbacks are running.
1778 // Keep the incoming actor alive until those callbacks have
1779 // all finished, just as we do when the foreground wakes.
1780 run_until_connection_close(
1781 incoming,
1782 other_actors,
1783 connection.incoming_closed.clone(),
1784 )
1785 .await
1786 };
1787
1788 run_until_connection_close(
1789 background,
1790 main_fn(connection.clone()),
1791 connection.incoming_closed.clone(),
1792 )
1793 .await
1794 }
1795 });
1796
1797 (connection, future)
1798 }
1799}
1800
1801impl<R, H, Run, Close> ConnectTo<R::Counterpart> for Builder<R, H, Run, Close>
1802where
1803 R: Role,
1804 H: HandleDispatchFrom<R::Counterpart> + 'static,
1805 Run: RunWithConnectionTo<R::Counterpart> + 'static,
1806 Close: HandleConnectionClose<R::Counterpart> + 'static,
1807{
1808 async fn connect_to(self, client: impl ConnectTo<R>) -> Result<(), crate::Error> {
1809 Builder::connect_to(self, client).await
1810 }
1811}
1812
1813/// The payload sent through the response oneshot channel.
1814///
1815/// Includes the response value and an optional ack channel for dispatch loop
1816/// synchronization.
1817pub(crate) struct ResponsePayload {
1818 /// The response result - either the JSON value or an error.
1819 pub(crate) result: Result<serde_json::Value, crate::Error>,
1820
1821 /// Optional acknowledgment channel for dispatch loop synchronization.
1822 ///
1823 /// When present, the receiver must send on this channel to signal that
1824 /// response processing is complete, allowing the dispatch loop to continue
1825 /// to the next message.
1826 ///
1827 /// This is present only when callback-style consumption was selected before
1828 /// the response was routed during its original dispatch. Local error paths,
1829 /// blocking consumers, and responses routed later do not hold the dispatch
1830 /// loop.
1831 pub(crate) ack_tx: Option<oneshot::Sender<()>>,
1832}
1833
1834impl std::fmt::Debug for ResponsePayload {
1835 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1836 f.debug_struct("ResponsePayload")
1837 .field("result", &self.result)
1838 .field("ack_tx", &self.ack_tx.as_ref().map(|_| "..."))
1839 .finish()
1840 }
1841}
1842
1843#[derive(Clone, Debug, Default)]
1844struct ResponseOrdering {
1845 ordered: Arc<AtomicBool>,
1846}
1847
1848impl ResponseOrdering {
1849 fn mark_ordered(&self) {
1850 self.ordered.store(true, Ordering::Release);
1851 }
1852
1853 fn is_ordered(&self) -> bool {
1854 self.ordered.load(Ordering::Acquire)
1855 }
1856}
1857
1858struct PendingReply {
1859 method: String,
1860 role_id: RoleId,
1861 sender: oneshot::Sender<ResponsePayload>,
1862 cancellation_disarm: SentRequestCancellationDisarm,
1863 ordering: ResponseOrdering,
1864}
1865
1866impl PendingReply {
1867 fn fail(self, error: crate::Error) {
1868 self.cancellation_disarm.disarm();
1869 if self
1870 .sender
1871 .send(ResponsePayload {
1872 result: Err(error),
1873 ack_tx: None,
1874 })
1875 .is_err()
1876 {
1877 tracing::trace!(method = %self.method, "Pending request was already dropped");
1878 }
1879 }
1880
1881 fn fail_incoming_closed(self) {
1882 let error = incoming_transport_closed_error(&self.method);
1883 self.fail(error);
1884 }
1885}
1886
1887#[derive(Default)]
1888struct PendingRepliesInner {
1889 incoming_closed: bool,
1890 replies: HashMap<RequestId, PendingReply>,
1891}
1892
1893#[derive(Clone, Default)]
1894struct PendingReplies {
1895 inner: Arc<Mutex<PendingRepliesInner>>,
1896}
1897
1898impl PendingReplies {
1899 fn registrar(&self) -> PendingRepliesRegistrar {
1900 PendingRepliesRegistrar {
1901 inner: Arc::downgrade(&self.inner),
1902 }
1903 }
1904
1905 fn contains(&self, id: &RequestId) -> bool {
1906 self.inner
1907 .lock()
1908 .expect("pending replies mutex poisoned")
1909 .replies
1910 .contains_key(id)
1911 }
1912
1913 fn remove(&self, id: &RequestId) -> Option<PendingReply> {
1914 self.inner
1915 .lock()
1916 .expect("pending replies mutex poisoned")
1917 .replies
1918 .remove(id)
1919 }
1920
1921 /// Atomically reject new subscriptions and fail every existing one.
1922 fn close_incoming(&self) -> usize {
1923 let replies = {
1924 let mut inner = self.inner.lock().expect("pending replies mutex poisoned");
1925 inner.incoming_closed = true;
1926 std::mem::take(&mut inner.replies)
1927 };
1928 let count = replies.len();
1929 for (_, reply) in replies {
1930 reply.fail_incoming_closed();
1931 }
1932 count
1933 }
1934}
1935
1936/// A non-owning handle used to register a request before it enters the
1937/// outgoing queue. Keeping this weak prevents escaped [`ConnectionTo`] clones
1938/// from extending the lifetime of response senders after the driver stops.
1939#[derive(Clone)]
1940struct PendingRepliesRegistrar {
1941 inner: Weak<Mutex<PendingRepliesInner>>,
1942}
1943
1944impl PendingRepliesRegistrar {
1945 /// Register a response destination before the request becomes observable.
1946 ///
1947 /// Returns `false` after failing `reply` when EOF has already made a
1948 /// response impossible or the connection driver is no longer running.
1949 fn subscribe(
1950 &self,
1951 id: RequestId,
1952 reply: PendingReply,
1953 incoming_closed: &IncomingClosed,
1954 ) -> bool {
1955 let Some(inner) = self.inner.upgrade() else {
1956 if incoming_closed.is_closing() {
1957 reply.fail_incoming_closed();
1958 } else {
1959 let method = reply.method.clone();
1960 reply.fail(crate::util::internal_error(format!(
1961 "failed to send outgoing request `{method}`: connection is no longer running"
1962 )));
1963 }
1964 return false;
1965 };
1966
1967 let result = {
1968 let mut inner = inner.lock().expect("pending replies mutex poisoned");
1969 if inner.incoming_closed {
1970 Err(reply)
1971 } else {
1972 Ok(inner.replies.insert(id, reply))
1973 }
1974 };
1975
1976 match result {
1977 Err(rejected) => {
1978 rejected.fail_incoming_closed();
1979 false
1980 }
1981 Ok(replaced) => {
1982 if let Some(replaced) = replaced {
1983 replaced.fail(
1984 crate::Error::internal_error()
1985 .data("outgoing request ID was reused before its response arrived"),
1986 );
1987 }
1988 true
1989 }
1990 }
1991 }
1992
1993 fn remove(&self, id: &RequestId) -> Option<PendingReply> {
1994 self.inner
1995 .upgrade()?
1996 .lock()
1997 .expect("pending replies mutex poisoned")
1998 .replies
1999 .remove(id)
2000 }
2001}
2002
2003impl Debug for PendingRepliesRegistrar {
2004 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2005 formatter
2006 .debug_struct("PendingRepliesRegistrar")
2007 .field("is_connected", &(self.inner.strong_count() > 0))
2008 .finish()
2009 }
2010}
2011
2012/// A request-local marker that is set when the peer asks to cancel the request.
2013///
2014/// Request handlers can get this handle from [`Responder::cancellation`] and
2015/// use it from spawned work to stop long-running request processing
2016/// cooperatively.
2017#[derive(Clone)]
2018pub struct RequestCancellation {
2019 state: Arc<RequestCancellationState>,
2020}
2021
2022struct RequestCancellationState {
2023 cancelled: AtomicBool,
2024 signal_tx: Mutex<Option<oneshot::Sender<()>>>,
2025 signal_rx: future::Shared<BoxFuture<'static, ()>>,
2026}
2027
2028impl RequestCancellation {
2029 fn new() -> Self {
2030 let (signal_tx, signal_rx) = oneshot::channel();
2031 let signal_rx = signal_rx.map(|_| ()).boxed().shared();
2032 Self {
2033 state: Arc::new(RequestCancellationState {
2034 cancelled: AtomicBool::new(false),
2035 signal_tx: Mutex::new(Some(signal_tx)),
2036 signal_rx,
2037 }),
2038 }
2039 }
2040
2041 /// Wait until the peer sends `$/cancel_request` for this request.
2042 ///
2043 /// If cancellation was already requested, this returns immediately.
2044 pub async fn cancelled(&self) {
2045 self.state.signal_rx.clone().await;
2046 }
2047
2048 /// Run request work until it completes or the peer asks to cancel it.
2049 ///
2050 /// If cancellation is requested first, this returns
2051 /// [`Error::request_cancelled`]. This is a convenience for request handlers
2052 /// that want to respond with the normal result or the standard
2053 /// cancellation error.
2054 ///
2055 /// When cancellation wins, `future` is dropped: work stops at its next
2056 /// await point, partial results are lost, and any cleanup must happen in
2057 /// `Drop` implementations. Handlers that need to flush partial results or
2058 /// run async cleanup should instead watch [`cancelled`](Self::cancelled)
2059 /// or poll [`is_cancelled`](Self::is_cancelled) from inside the work.
2060 ///
2061 /// [`Error::request_cancelled`]: crate::Error::request_cancelled
2062 pub async fn run_until_cancelled<T>(
2063 &self,
2064 future: impl std::future::Future<Output = Result<T, crate::Error>>,
2065 ) -> Result<T, crate::Error> {
2066 if self.is_cancelled() {
2067 return Err(crate::Error::request_cancelled());
2068 }
2069
2070 match future::select(pin!(future), pin!(self.cancelled())).await {
2071 Either::Left((result, _)) => result,
2072 Either::Right(((), _)) => Err(crate::Error::request_cancelled()),
2073 }
2074 }
2075
2076 /// Returns whether the peer has already requested cancellation.
2077 #[must_use]
2078 pub fn is_cancelled(&self) -> bool {
2079 self.state.cancelled.load(Ordering::Acquire)
2080 }
2081
2082 fn cancel(&self) {
2083 if self.state.cancelled.swap(true, Ordering::AcqRel) {
2084 return;
2085 }
2086
2087 let signal_tx = self
2088 .state
2089 .signal_tx
2090 .lock()
2091 .expect("request cancellation signal mutex poisoned")
2092 .take();
2093
2094 // Complete the oneshot outside the lock: it wakes waiters, and
2095 // arbitrary waker code must not observe the lock held.
2096 if let Some(signal_tx) = signal_tx {
2097 let _ = signal_tx.send(());
2098 }
2099 }
2100}
2101
2102impl Debug for RequestCancellation {
2103 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2104 formatter
2105 .debug_struct("RequestCancellation")
2106 .field("is_cancelled", &self.is_cancelled())
2107 .finish_non_exhaustive()
2108 }
2109}
2110
2111/// Per-request cancellation state tracked by [`RequestCancellationRegistry`].
2112///
2113/// The full [`RequestCancellation`] marker (with its wakeup machinery) is only
2114/// allocated once a handler asks for it via [`Responder::cancellation`]; until
2115/// then an incoming `$/cancel_request` just flips the entry to `Cancelled`.
2116/// This keeps the per-request cost of the registry to a single map entry.
2117#[derive(Debug)]
2118enum RequestCancellationEntry {
2119 /// The request is in flight; no marker handed out, no cancellation yet.
2120 Armed,
2121 /// `$/cancel_request` arrived before a marker was handed out.
2122 Cancelled,
2123 /// A marker was handed out via [`Responder::cancellation`].
2124 Marker(RequestCancellation),
2125}
2126
2127/// A registered request's cancellation state, tagged with the generation of
2128/// its registration.
2129///
2130/// The generation distinguishes a registration from earlier ones that used
2131/// the same request ID, so that when a (protocol-violating) peer reuses the
2132/// ID of a request that is still in flight, the stale request's responder can
2133/// neither remove nor observe the cancellation state of the newer request.
2134#[derive(Debug)]
2135struct RequestCancellationSlot {
2136 generation: u64,
2137 entry: RequestCancellationEntry,
2138}
2139
2140#[derive(Debug, Default)]
2141struct RequestCancellationRegistryInner {
2142 slots: HashMap<RequestId, RequestCancellationSlot>,
2143 next_generation: u64,
2144}
2145
2146#[derive(Clone, Debug, Default)]
2147struct RequestCancellationRegistry {
2148 inner: Arc<Mutex<RequestCancellationRegistryInner>>,
2149}
2150
2151#[derive(Debug)]
2152struct ResponderCancellation {
2153 id: RequestId,
2154 generation: u64,
2155 registry: RequestCancellationRegistry,
2156}
2157
2158impl RequestCancellationRegistry {
2159 fn new() -> Self {
2160 Self::default()
2161 }
2162
2163 fn register(&self, id: &RequestId) -> ResponderCancellation {
2164 let generation = {
2165 let mut inner = self
2166 .inner
2167 .lock()
2168 .expect("request cancellation registry mutex poisoned");
2169 let generation = inner.next_generation;
2170 inner.next_generation += 1;
2171 if inner
2172 .slots
2173 .insert(
2174 id.clone(),
2175 RequestCancellationSlot {
2176 generation,
2177 entry: RequestCancellationEntry::Armed,
2178 },
2179 )
2180 .is_some()
2181 {
2182 tracing::debug!(
2183 ?id,
2184 "peer reused the ID of a request that is still in flight"
2185 );
2186 }
2187 generation
2188 };
2189 ResponderCancellation {
2190 id: id.clone(),
2191 generation,
2192 registry: self.clone(),
2193 }
2194 }
2195
2196 /// Get the cancellation marker for a registered request, creating it on
2197 /// first use. Repeated calls return markers that share the same state.
2198 ///
2199 /// Exception: when the registration is stale (a protocol-violating peer
2200 /// reused this request ID and the slot now belongs to a newer request, or
2201 /// was already removed by it), every call returns a fresh *detached*
2202 /// marker. Detached markers can never fire, and detached markers from
2203 /// repeated calls do not share state with each other.
2204 fn marker(&self, id: &RequestId, generation: u64) -> RequestCancellation {
2205 let mut inner = self
2206 .inner
2207 .lock()
2208 .expect("request cancellation registry mutex poisoned");
2209 let Some(slot) = inner.slots.get_mut(id) else {
2210 // The slot lives as long as the responder that owns it, so this
2211 // is only reachable if the peer reused this request ID and the
2212 // newer request's responder already removed the replacement slot.
2213 // Hand out a detached marker rather than panicking.
2214 return RequestCancellation::new();
2215 };
2216 if slot.generation != generation {
2217 // The peer reused this request ID while the request was still in
2218 // flight, and the slot now belongs to the newer request. Hand the
2219 // stale responder a detached marker instead of cross-wiring the
2220 // two requests' cancellation states.
2221 return RequestCancellation::new();
2222 }
2223 let entry = &mut slot.entry;
2224 match entry {
2225 RequestCancellationEntry::Marker(marker) => marker.clone(),
2226 RequestCancellationEntry::Armed => {
2227 let marker = RequestCancellation::new();
2228 *entry = RequestCancellationEntry::Marker(marker.clone());
2229 marker
2230 }
2231 RequestCancellationEntry::Cancelled => {
2232 // No one can be waiting on a marker that did not exist yet,
2233 // so firing it while holding the registry lock is fine.
2234 let marker = RequestCancellation::new();
2235 marker.cancel();
2236 *entry = RequestCancellationEntry::Marker(marker.clone());
2237 marker
2238 }
2239 }
2240 }
2241
2242 fn cancel_if_requested(&self, dispatch: &Dispatch) -> Result<bool, crate::Error> {
2243 let Some(request_id) = cancellation_request_id(dispatch)? else {
2244 return Ok(false);
2245 };
2246 Ok(self.cancel(&request_id))
2247 }
2248
2249 /// Mark whichever request currently owns `request_id` as cancelled.
2250 fn cancel(&self, request_id: &RequestId) -> bool {
2251 let marker = {
2252 let mut inner = self
2253 .inner
2254 .lock()
2255 .expect("request cancellation registry mutex poisoned");
2256 let Some(slot) = inner.slots.get_mut(request_id) else {
2257 return false;
2258 };
2259 let entry = &mut slot.entry;
2260 match entry {
2261 RequestCancellationEntry::Marker(marker) => marker.clone(),
2262 RequestCancellationEntry::Cancelled => return true,
2263 RequestCancellationEntry::Armed => {
2264 *entry = RequestCancellationEntry::Cancelled;
2265 return true;
2266 }
2267 }
2268 };
2269
2270 // Fire the marker outside the registry lock: waking waiters runs
2271 // arbitrary waker code that must not observe the lock held.
2272 marker.cancel();
2273 true
2274 }
2275
2276 /// Remove the slot for `request_id`, but only if it still belongs to the
2277 /// registration identified by `generation`.
2278 fn remove(&self, request_id: &RequestId, generation: u64) {
2279 let mut inner = self
2280 .inner
2281 .lock()
2282 .expect("request cancellation registry mutex poisoned");
2283 if inner
2284 .slots
2285 .get(request_id)
2286 .is_some_and(|slot| slot.generation == generation)
2287 {
2288 inner.slots.remove(request_id);
2289 }
2290 }
2291}
2292
2293impl ResponderCancellation {
2294 fn cancellation(&self) -> RequestCancellation {
2295 self.registry.marker(&self.id, self.generation)
2296 }
2297}
2298
2299impl Drop for ResponderCancellation {
2300 fn drop(&mut self) {
2301 self.registry.remove(&self.id, self.generation);
2302 }
2303}
2304
2305fn cancellation_request_id(dispatch: &Dispatch) -> Result<Option<RequestId>, crate::Error> {
2306 let Dispatch::Notification(message) = dispatch else {
2307 return Ok(None);
2308 };
2309 cancellation_request_id_from_message(message)
2310}
2311
2312fn cancellation_request_id_from_message(
2313 message: &UntypedMessage,
2314) -> Result<Option<RequestId>, crate::Error> {
2315 let (method, params) = peel_successor_envelopes(&message.method, &message.params);
2316 if !crate::schema::v1::CancelRequestNotification::matches_method(method) {
2317 return Ok(None);
2318 }
2319
2320 let notification = crate::schema::v1::CancelRequestNotification::parse_message(method, params)?;
2321 Ok(Some(notification.request_id))
2322}
2323
2324/// Peel any [`SuccessorMessage`] envelopes off a notification by reference,
2325/// returning the innermost method and params.
2326///
2327/// This only peeks at the envelope's `method`/`params` fields instead of
2328/// deserializing the envelope, for two reasons:
2329///
2330/// - It avoids deep-cloning the params of every wrapped notification on the
2331/// hot dispatch path just to inspect the inner method name.
2332/// - It is deliberately lenient: a malformed envelope is left as-is here and
2333/// flows on to the handler chain, which is responsible for reporting it.
2334///
2335/// [`SuccessorMessage`]: crate::schema::SuccessorMessage
2336fn peel_successor_envelopes<'message>(
2337 mut method: &'message str,
2338 mut params: &'message serde_json::Value,
2339) -> (&'message str, &'message serde_json::Value) {
2340 while crate::schema::SuccessorMessage::<UntypedMessage>::matches_method(method) {
2341 let Some(inner_method) = params.get("method").and_then(serde_json::Value::as_str) else {
2342 break;
2343 };
2344 method = inner_method;
2345 params = params.get("params").unwrap_or(&serde_json::Value::Null);
2346 }
2347 (method, params)
2348}
2349
2350/// Whether a notification is a `$/cancel_request`, even when it is still
2351/// wrapped in `_proxy/successor` envelopes.
2352///
2353/// `$/cancel_request` is connection-scoped: its `requestId` was allocated on
2354/// the connection the notification arrived over and means nothing on any
2355/// other connection. Generic forwarding code (such as
2356/// [`ConnectionTo::send_proxied_message_to`]) uses this check to drop the raw
2357/// notification instead of tunneling it across a hop; the cancellation still
2358/// propagates because [`forward_response_to`](SentRequest::forward_response_to)
2359/// re-issues it with the forwarded request's own ID.
2360///
2361/// Checking a notification whose method is not the successor envelope is a
2362/// plain method-name comparison. Only successor-wrapped notifications pay for
2363/// a serialization to peel the envelope.
2364#[must_use]
2365pub fn is_cancel_request_notification<N: JsonRpcNotification>(notification: &N) -> bool {
2366 let method = notification.method();
2367 if crate::schema::v1::CancelRequestNotification::matches_method(method) {
2368 return true;
2369 }
2370 if !crate::schema::SuccessorMessage::<UntypedMessage>::matches_method(method) {
2371 return false;
2372 }
2373
2374 match notification.to_untyped_message() {
2375 Ok(untyped) => {
2376 let (method, _params) = peel_successor_envelopes(&untyped.method, &untyped.params);
2377 crate::schema::v1::CancelRequestNotification::matches_method(method)
2378 }
2379 Err(error) => {
2380 tracing::debug!(
2381 ?error,
2382 "failed to inspect successor-wrapped notification for cancellation"
2383 );
2384 false
2385 }
2386 }
2387}
2388
2389/// Messages send to be serialized over the transport.
2390#[derive(Clone)]
2391enum ResponseDestination {
2392 Individual(IndividualResponseSlot),
2393 Batch(BatchResponseSlot),
2394}
2395
2396impl std::fmt::Debug for ResponseDestination {
2397 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2398 match self {
2399 Self::Individual(slot) => formatter.debug_tuple("Individual").field(slot).finish(),
2400 Self::Batch(slot) => formatter.debug_tuple("Batch").field(slot).finish(),
2401 }
2402 }
2403}
2404
2405impl ResponseDestination {
2406 fn individual() -> Self {
2407 Self::Individual(IndividualResponseSlot::default())
2408 }
2409
2410 fn batch(slot_count: usize) -> (impl Iterator<Item = Self>, BatchDispatchCompletion) {
2411 let state = Arc::new(Mutex::new(BatchResponseState {
2412 remaining: slot_count,
2413 responses: (0..slot_count).map(|_| None).collect(),
2414 abandoned: (0..slot_count).map(|_| None).collect(),
2415 active_handler_attempts: (0..slot_count).map(|_| 0).collect(),
2416 dispatch_complete: false,
2417 emitted: false,
2418 }));
2419
2420 (
2421 (0..slot_count).map({
2422 let state = state.clone();
2423 move |index| {
2424 Self::Batch(BatchResponseSlot {
2425 state: state.clone(),
2426 index,
2427 })
2428 }
2429 }),
2430 BatchDispatchCompletion { state },
2431 )
2432 }
2433
2434 fn complete(self, response: RawJsonRpcMessage) -> Option<TransportFrame> {
2435 match self {
2436 Self::Individual(slot) => slot.complete(response),
2437 Self::Batch(slot) => slot.complete(response).map(batch_response_frame),
2438 }
2439 }
2440
2441 fn abandon(self, fallback: RawJsonRpcMessage) -> Option<TransportFrame> {
2442 match self {
2443 Self::Individual(_) => None,
2444 Self::Batch(slot) => slot.abandon(fallback).map(batch_response_frame),
2445 }
2446 }
2447
2448 fn is_batch(&self) -> bool {
2449 matches!(self, Self::Batch(_))
2450 }
2451
2452 fn begin_handler_attempt(
2453 &self,
2454 message_tx: OutgoingMessageTx,
2455 ) -> Option<ResponderHandlerAttempt> {
2456 let Self::Batch(slot) = self else {
2457 return None;
2458 };
2459 slot.begin_handler_attempt();
2460 Some(ResponderHandlerAttempt {
2461 message_tx,
2462 destination: self.clone(),
2463 })
2464 }
2465
2466 fn finish_handler_attempt(self) -> Option<TransportFrame> {
2467 match self {
2468 Self::Individual(_) => None,
2469 Self::Batch(slot) => slot.finish_handler_attempt().map(batch_response_frame),
2470 }
2471 }
2472}
2473
2474#[derive(Clone, Debug, Default)]
2475struct IndividualResponseSlot {
2476 completed: Arc<AtomicBool>,
2477}
2478
2479impl IndividualResponseSlot {
2480 fn complete(self, response: RawJsonRpcMessage) -> Option<TransportFrame> {
2481 if self.completed.swap(true, Ordering::AcqRel) {
2482 tracing::warn!("Ignoring duplicate completion of JSON-RPC request");
2483 return None;
2484 }
2485
2486 Some(TransportFrame::Single(response))
2487 }
2488}
2489
2490fn batch_response_frame(responses: Vec<RawJsonRpcMessage>) -> TransportFrame {
2491 TransportFrame::Batch(
2492 TransportBatch::from_messages(responses)
2493 .expect("a completed JSON-RPC response batch is non-empty"),
2494 )
2495}
2496
2497#[derive(Clone)]
2498struct BatchDispatchCompletion {
2499 state: Arc<Mutex<BatchResponseState>>,
2500}
2501
2502impl std::fmt::Debug for BatchDispatchCompletion {
2503 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2504 formatter
2505 .debug_struct("BatchDispatchCompletion")
2506 .finish_non_exhaustive()
2507 }
2508}
2509
2510impl BatchDispatchCompletion {
2511 fn complete(self) -> Option<TransportFrame> {
2512 let mut state = self
2513 .state
2514 .lock()
2515 .expect("batch response accumulator mutex poisoned");
2516 if state.dispatch_complete {
2517 tracing::warn!("Ignoring duplicate JSON-RPC batch dispatch completion");
2518 return None;
2519 }
2520 state.dispatch_complete = true;
2521 for index in 0..state.responses.len() {
2522 promote_abandoned_response(&mut state, index);
2523 }
2524 take_completed_batch(&mut state).map(batch_response_frame)
2525 }
2526}
2527
2528fn promote_abandoned_response(state: &mut BatchResponseState, index: usize) {
2529 if state.active_handler_attempts[index] == 0
2530 && state.responses[index].is_none()
2531 && let Some(fallback) = state.abandoned[index].take()
2532 {
2533 state.responses[index] = Some(fallback);
2534 state.remaining -= 1;
2535 }
2536}
2537
2538fn take_completed_batch(state: &mut BatchResponseState) -> Option<Vec<RawJsonRpcMessage>> {
2539 if !state.dispatch_complete || state.remaining != 0 || state.emitted {
2540 return None;
2541 }
2542
2543 state.emitted = true;
2544 Some(
2545 state
2546 .responses
2547 .iter_mut()
2548 .map(|response| {
2549 response
2550 .take()
2551 .expect("completed JSON-RPC batch has every response slot")
2552 })
2553 .collect(),
2554 )
2555}
2556
2557#[derive(Clone)]
2558struct BatchResponseSlot {
2559 state: Arc<Mutex<BatchResponseState>>,
2560 index: usize,
2561}
2562
2563impl std::fmt::Debug for BatchResponseSlot {
2564 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2565 formatter
2566 .debug_struct("BatchResponseSlot")
2567 .field("index", &self.index)
2568 .finish_non_exhaustive()
2569 }
2570}
2571
2572impl BatchResponseSlot {
2573 fn begin_handler_attempt(&self) {
2574 let mut state = self
2575 .state
2576 .lock()
2577 .expect("batch response accumulator mutex poisoned");
2578 state.active_handler_attempts[self.index] += 1;
2579 }
2580
2581 fn finish_handler_attempt(self) -> Option<Vec<RawJsonRpcMessage>> {
2582 let mut state = self
2583 .state
2584 .lock()
2585 .expect("batch response accumulator mutex poisoned");
2586 state.active_handler_attempts[self.index] = state.active_handler_attempts[self.index]
2587 .checked_sub(1)
2588 .expect("handler attempt completion without a matching start");
2589 if state.dispatch_complete {
2590 promote_abandoned_response(&mut state, self.index);
2591 }
2592 take_completed_batch(&mut state)
2593 }
2594
2595 fn complete(self, response: RawJsonRpcMessage) -> Option<Vec<RawJsonRpcMessage>> {
2596 let mut state = self
2597 .state
2598 .lock()
2599 .expect("batch response accumulator mutex poisoned");
2600 if state.emitted {
2601 tracing::warn!(
2602 index = self.index,
2603 "Ignoring response after JSON-RPC batch was already completed"
2604 );
2605 return None;
2606 }
2607 if self.index >= state.responses.len() {
2608 tracing::error!(index = self.index, "Invalid JSON-RPC batch response slot");
2609 return None;
2610 }
2611 if state.responses[self.index].is_some() {
2612 tracing::warn!(
2613 index = self.index,
2614 "Ignoring duplicate completion of JSON-RPC batch response slot"
2615 );
2616 return None;
2617 }
2618
2619 state.abandoned[self.index] = None;
2620 state.responses[self.index] = Some(response);
2621 state.remaining -= 1;
2622 take_completed_batch(&mut state)
2623 }
2624
2625 fn abandon(self, fallback: RawJsonRpcMessage) -> Option<Vec<RawJsonRpcMessage>> {
2626 let mut state = self
2627 .state
2628 .lock()
2629 .expect("batch response accumulator mutex poisoned");
2630 if state.emitted || state.responses[self.index].is_some() {
2631 return None;
2632 }
2633 if state.abandoned[self.index].is_some() {
2634 tracing::warn!(
2635 index = self.index,
2636 "Ignoring duplicate abandonment of JSON-RPC batch response slot"
2637 );
2638 return None;
2639 }
2640
2641 if state.dispatch_complete && state.active_handler_attempts[self.index] == 0 {
2642 state.responses[self.index] = Some(fallback);
2643 state.remaining -= 1;
2644 } else {
2645 state.abandoned[self.index] = Some(fallback);
2646 }
2647 take_completed_batch(&mut state)
2648 }
2649}
2650
2651struct BatchResponseState {
2652 remaining: usize,
2653 responses: Vec<Option<RawJsonRpcMessage>>,
2654 abandoned: Vec<Option<RawJsonRpcMessage>>,
2655 active_handler_attempts: Vec<usize>,
2656 dispatch_complete: bool,
2657 emitted: bool,
2658}
2659
2660#[derive(Clone, Debug)]
2661struct RequestReplyTarget {
2662 id: RequestId,
2663 method: String,
2664 destination: ResponseDestination,
2665}
2666
2667struct ResponderHandlerAttempt {
2668 message_tx: OutgoingMessageTx,
2669 destination: ResponseDestination,
2670}
2671
2672impl Drop for ResponderHandlerAttempt {
2673 fn drop(&mut self) {
2674 if let Err(error) = send_raw_message(
2675 &self.message_tx,
2676 OutgoingMessage::BatchHandlerAttemptComplete {
2677 destination: self.destination.clone(),
2678 },
2679 ) {
2680 tracing::debug!(?error, "could not complete JSON-RPC batch handler attempt");
2681 }
2682 }
2683}
2684
2685#[derive(Clone)]
2686struct ResponseReplyTarget {
2687 id: RequestId,
2688 method: String,
2689 sender: Arc<Mutex<Option<oneshot::Sender<ResponsePayload>>>>,
2690 ordering: ResponseOrdering,
2691 dispatch: ResponseDispatch,
2692}
2693
2694impl ResponseReplyTarget {
2695 fn route(self, result: Result<serde_json::Value, crate::Error>) {
2696 let sender = self
2697 .sender
2698 .lock()
2699 .expect("response reply mutex poisoned")
2700 .take();
2701 let Some(sender) = sender else {
2702 tracing::debug!(
2703 method = %self.method,
2704 id = ?self.id,
2705 "response was already routed to its local awaiter"
2706 );
2707 return;
2708 };
2709
2710 let ack_tx = self.dispatch.acknowledgment(&self.ordering);
2711 if sender.send(ResponsePayload { result, ack_tx }).is_err() {
2712 tracing::debug!(
2713 method = %self.method,
2714 id = ?self.id,
2715 "dropped response because local receiver was gone"
2716 );
2717 }
2718 }
2719}
2720
2721#[derive(Clone, Default)]
2722struct ResponseDispatch {
2723 state: Arc<Mutex<ResponseDispatchState>>,
2724}
2725
2726#[derive(Default)]
2727struct ResponseDispatchState {
2728 complete: bool,
2729 ack_rx: Option<oneshot::Receiver<()>>,
2730}
2731
2732impl ResponseDispatch {
2733 fn acknowledgment(&self, ordering: &ResponseOrdering) -> Option<oneshot::Sender<()>> {
2734 if !ordering.is_ordered() {
2735 return None;
2736 }
2737
2738 let mut state = self.state.lock().expect("response dispatch mutex poisoned");
2739 if state.complete {
2740 return None;
2741 }
2742
2743 let (ack_tx, ack_rx) = oneshot::channel();
2744 let previous_ack = state.ack_rx.replace(ack_rx);
2745 debug_assert!(
2746 previous_ack.is_none(),
2747 "a response dispatch can only be routed once"
2748 );
2749 Some(ack_tx)
2750 }
2751
2752 fn complete(&self) -> Option<oneshot::Receiver<()>> {
2753 let mut state = self.state.lock().expect("response dispatch mutex poisoned");
2754 state.complete = true;
2755 state.ack_rx.take()
2756 }
2757}
2758
2759enum HandlerErrorTarget {
2760 Request(RequestReplyTarget),
2761 Response(ResponseReplyTarget),
2762}
2763
2764impl HandlerErrorTarget {
2765 fn begin_handler_attempt(
2766 &self,
2767 message_tx: &OutgoingMessageTx,
2768 ) -> Option<ResponderHandlerAttempt> {
2769 match self {
2770 Self::Request(target) => target.destination.begin_handler_attempt(message_tx.clone()),
2771 Self::Response(_) => None,
2772 }
2773 }
2774}
2775
2776#[derive(Debug)]
2777enum OutgoingMessage {
2778 /// Close the outgoing application queue and acknowledge after every
2779 /// already-accepted message has entered the raw transport queue.
2780 CloseAfterDraining { done: oneshot::Sender<()> },
2781
2782 /// Mark every entry in an incoming batch as dispatched. A completed
2783 /// response array may only be emitted after this barrier.
2784 BatchDispatchComplete { completion: BatchDispatchCompletion },
2785
2786 /// Finish arbitration for a handler attempt that may have dropped a batch
2787 /// responder immediately before returning an error.
2788 BatchHandlerAttemptComplete { destination: ResponseDestination },
2789
2790 /// Record that a claimed batch request dropped its responder without
2791 /// replying. The fallback remains provisional while its handler attempt is
2792 /// active so a handler error can supply the authoritative response.
2793 AbandonedBatchResponse {
2794 id: RequestId,
2795 method: String,
2796 destination: ResponseDestination,
2797 },
2798
2799 /// Send a request to the server.
2800 Request {
2801 /// id assigned to this request (generated by sender)
2802 id: RequestId,
2803
2804 /// the original method
2805 method: String,
2806
2807 /// the message to send; this may have a distinct method
2808 /// depending on the peer
2809 untyped: UntypedMessage,
2810 },
2811
2812 /// Send a notification to the server.
2813 Notification {
2814 /// the message to send; this may have a distinct method
2815 /// depending on the peer
2816 untyped: UntypedMessage,
2817 },
2818
2819 /// Send a response to a message from the server
2820 Response {
2821 id: RequestId,
2822
2823 /// Method of the incoming request this response completes.
2824 method: String,
2825
2826 response: Result<serde_json::Value, crate::Error>,
2827
2828 destination: ResponseDestination,
2829 },
2830
2831 /// Send an Error Response that cannot be correlated to a request ID.
2832 UncorrelatedErrorResponse {
2833 error: crate::Error,
2834 destination: ResponseDestination,
2835 },
2836}
2837
2838/// Return type from JrHandler; indicates whether the request was handled or not.
2839#[must_use]
2840#[derive(Debug)]
2841pub enum Handled<T> {
2842 /// The message was handled
2843 Yes,
2844
2845 /// The message was not handled; returns the original value.
2846 ///
2847 /// If `retry` is true,
2848 No {
2849 /// The message to be passed to subsequent handlers
2850 /// (typically the original message, but it may have been
2851 /// mutated.)
2852 message: T,
2853
2854 /// If true, request the message to be queued and retried with
2855 /// dynamic handlers as they are added.
2856 ///
2857 /// This is used for managing session updates since the dynamic
2858 /// handler for a session cannot be added until the response to the
2859 /// new session request has been processed and there may be updates
2860 /// that get processed at the same time.
2861 retry: bool,
2862 },
2863}
2864
2865/// Trait for converting handler return values into [`Handled`].
2866///
2867/// This trait allows handlers to return either `()` (which becomes `Handled::Yes`)
2868/// or an explicit `Handled<T>` value for more control over handler propagation.
2869pub trait IntoHandled<T> {
2870 /// Convert this value into a `Handled<T>`.
2871 fn into_handled(self) -> Handled<T>;
2872}
2873
2874impl<T> IntoHandled<T> for () {
2875 fn into_handled(self) -> Handled<T> {
2876 Handled::Yes
2877 }
2878}
2879
2880impl<T> IntoHandled<T> for Handled<T> {
2881 fn into_handled(self) -> Handled<T> {
2882 self
2883 }
2884}
2885
2886/// Connection context for sending messages and spawning tasks.
2887///
2888/// This is the primary handle for interacting with the JSON-RPC connection from
2889/// within handler callbacks. You can use it to:
2890///
2891/// * Send requests and notifications to the other side
2892/// * Spawn concurrent tasks that run alongside the connection
2893/// * Respond to requests (via [`Responder`] which wraps this)
2894///
2895/// # Cloning
2896///
2897/// `ConnectionTo` is cheaply cloneable - all clones refer to the same underlying connection.
2898/// This makes it easy to share across async tasks.
2899///
2900/// # Event Loop and Concurrency
2901///
2902/// Handler callbacks run on the event loop, which means the connection cannot process new
2903/// messages while your handler is running. Use [`spawn`](Self::spawn) to offload any
2904/// expensive or blocking work to concurrent tasks.
2905///
2906/// See the [Event Loop and Concurrency](Builder#event-loop-and-concurrency) section
2907/// for more details.
2908#[derive(Clone, Debug)]
2909pub struct ConnectionTo<Counterpart: Role> {
2910 counterpart: Counterpart,
2911 message_tx: OutgoingMessageTx,
2912 task_tx: TaskTx,
2913 dynamic_handler_tx: mpsc::UnboundedSender<DynamicHandlerMessage<Counterpart>>,
2914 transport_completion: SharedTransportCompletion,
2915 pending_replies: PendingRepliesRegistrar,
2916 incoming_closed: IncomingClosed,
2917}
2918
2919type SharedTransportCompletion = future::Shared<BoxFuture<'static, Result<(), crate::Error>>>;
2920
2921#[derive(Clone)]
2922struct IncomingClosed {
2923 state: Arc<IncomingClosedState>,
2924}
2925
2926struct IncomingClosedState {
2927 closing: AtomicBool,
2928 closed: AtomicBool,
2929 signal_tx: Mutex<Option<oneshot::Sender<()>>>,
2930 signal_rx: future::Shared<BoxFuture<'static, ()>>,
2931}
2932
2933impl IncomingClosed {
2934 fn new() -> Self {
2935 let (signal_tx, signal_rx) = oneshot::channel();
2936 Self {
2937 state: Arc::new(IncomingClosedState {
2938 closing: AtomicBool::new(false),
2939 closed: AtomicBool::new(false),
2940 signal_tx: Mutex::new(Some(signal_tx)),
2941 signal_rx: signal_rx.map(|_| ()).boxed().shared(),
2942 }),
2943 }
2944 }
2945
2946 fn begin_close(&self) {
2947 self.state.closing.store(true, Ordering::Release);
2948 }
2949
2950 fn finish_close(&self) {
2951 self.state.closed.store(true, Ordering::Release);
2952 let signal_tx = self
2953 .state
2954 .signal_tx
2955 .lock()
2956 .expect("incoming-close signal mutex poisoned")
2957 .take();
2958
2959 if let Some(signal_tx) = signal_tx {
2960 let _ = signal_tx.send(());
2961 }
2962 }
2963
2964 async fn closed(&self) {
2965 self.state.signal_rx.clone().await;
2966 }
2967
2968 fn is_closed(&self) -> bool {
2969 self.state.closed.load(Ordering::Acquire)
2970 }
2971
2972 fn is_closing(&self) -> bool {
2973 self.state.closing.load(Ordering::Acquire)
2974 }
2975}
2976
2977impl Debug for IncomingClosed {
2978 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2979 formatter
2980 .debug_struct("IncomingClosed")
2981 .field("is_closing", &self.is_closing())
2982 .field("is_closed", &self.is_closed())
2983 .finish_non_exhaustive()
2984 }
2985}
2986
2987/// Stable discriminator stored in the `data.reason` field of errors produced
2988/// when the incoming transport reaches clean EOF before a request receives its
2989/// response.
2990pub const INCOMING_TRANSPORT_CLOSED_REASON: &str = "incoming_transport_closed";
2991
2992/// Return whether `error` reports that the incoming transport reached clean
2993/// EOF before a request received its response.
2994#[must_use]
2995pub fn is_incoming_transport_closed(error: &crate::Error) -> bool {
2996 error
2997 .data
2998 .as_ref()
2999 .and_then(|data| data.get("reason"))
3000 .and_then(serde_json::Value::as_str)
3001 == Some(INCOMING_TRANSPORT_CLOSED_REASON)
3002}
3003
3004fn incoming_transport_closed_error(method: &str) -> crate::Error {
3005 let mut error = crate::Error::internal_error();
3006 error.message = "Incoming transport closed".to_string();
3007 error.data(serde_json::json!({
3008 "reason": INCOMING_TRANSPORT_CLOSED_REASON,
3009 "method": method,
3010 }))
3011}
3012
3013/// Run the connection background alongside its foreground while ensuring that
3014/// a foreground woken by incoming EOF cannot cancel close callbacks midway.
3015fn run_until_connection_close<R>(
3016 background: impl Future<Output = Result<(), crate::Error>>,
3017 foreground: impl Future<Output = Result<R, crate::Error>>,
3018 incoming_closed: IncomingClosed,
3019) -> impl Future<Output = Result<R, crate::Error>> {
3020 // Box these before constructing the returned future. Keeping the generic
3021 // connection actors directly in this async state would substantially grow
3022 // every `connect_*` future.
3023 let background = Box::pin(background);
3024 let foreground = Box::pin(foreground);
3025
3026 async move {
3027 match future::select(background, foreground).await {
3028 Either::Left((background_result, foreground)) => {
3029 background_result?;
3030 foreground.await
3031 }
3032 Either::Right((foreground_result, background)) => {
3033 if !incoming_closed.is_closing() {
3034 return foreground_result;
3035 }
3036
3037 match future::select(background, Box::pin(incoming_closed.closed())).await {
3038 Either::Left((background_result, _)) => {
3039 background_result?;
3040 foreground_result
3041 }
3042 Either::Right(((), background)) => {
3043 // Poll the background first once more so an error returned
3044 // by the just-finished close callback wins over the ready
3045 // foreground result.
3046 crate::util::run_until(background, future::ready(foreground_result)).await
3047 }
3048 }
3049 }
3050 }
3051 }
3052}
3053
3054impl<Counterpart: Role> ConnectionTo<Counterpart> {
3055 fn new(
3056 counterpart: Counterpart,
3057 message_tx: mpsc::UnboundedSender<OutgoingMessage>,
3058 task_tx: mpsc::UnboundedSender<Task>,
3059 dynamic_handler_tx: mpsc::UnboundedSender<DynamicHandlerMessage<Counterpart>>,
3060 transport_completion: SharedTransportCompletion,
3061 pending_replies: PendingRepliesRegistrar,
3062 ) -> Self {
3063 Self {
3064 counterpart,
3065 message_tx,
3066 task_tx,
3067 dynamic_handler_tx,
3068 transport_completion,
3069 pending_replies,
3070 incoming_closed: IncomingClosed::new(),
3071 }
3072 }
3073
3074 /// Return the counterpart role this connection is talking to.
3075 pub fn counterpart(&self) -> Counterpart {
3076 self.counterpart.clone()
3077 }
3078
3079 /// Wait until the incoming transport reaches clean EOF.
3080 ///
3081 /// Transport closure means that no more messages or responses can arrive.
3082 /// Pending requests are failed first; this completes after registered
3083 /// [`Builder::on_close`] callbacks finish.
3084 /// It does not automatically cancel the future passed to
3085 /// [`Builder::connect_with`]; use [`Builder::on_close`] when the connection
3086 /// should run application-specific cleanup or terminate that future.
3087 pub async fn incoming_closed(&self) {
3088 self.incoming_closed.closed().await;
3089 }
3090
3091 /// Return whether clean incoming-EOF processing has completed.
3092 ///
3093 /// This remains `false` while [`Builder::on_close`] callbacks are running.
3094 #[must_use]
3095 pub fn is_incoming_closed(&self) -> bool {
3096 self.incoming_closed.is_closed()
3097 }
3098
3099 /// Stop accepting outgoing messages, drain those already accepted through
3100 /// the protocol actor, and wait for the transport sink to finish them.
3101 async fn drain_outgoing(&self) -> Result<(), crate::Error> {
3102 let (done_tx, done_rx) = oneshot::channel();
3103 let marker_result = send_raw_message(
3104 &self.message_tx,
3105 OutgoingMessage::CloseAfterDraining { done: done_tx },
3106 );
3107 let marker_result = match marker_result {
3108 Ok(()) => done_rx.await.map_err(|error| {
3109 crate::util::internal_error(format!(
3110 "outgoing drain marker was dropped before completion: {error}"
3111 ))
3112 }),
3113 Err(error) => Err(error),
3114 };
3115
3116 // The marker only proves that all accepted protocol messages entered
3117 // the raw transport queue. Transport completion is the sink-level
3118 // barrier that proves a backpressured writer finished them.
3119 self.transport_completion.clone().await?;
3120 marker_result
3121 }
3122
3123 fn is_incoming_closing(&self) -> bool {
3124 self.incoming_closed.is_closing()
3125 }
3126
3127 pub(super) fn begin_incoming_close(&self) {
3128 self.incoming_closed.begin_close();
3129 }
3130
3131 pub(super) fn finish_incoming_close(&self) {
3132 self.incoming_closed.finish_close();
3133 }
3134
3135 /// Spawns a task that will run so long as the JSON-RPC connection is being served.
3136 ///
3137 /// This is the primary mechanism for offloading expensive work from handler callbacks
3138 /// to avoid blocking the event loop. Spawned tasks run concurrently with the connection,
3139 /// allowing the server to continue processing messages.
3140 ///
3141 /// # Event Loop
3142 ///
3143 /// Handler callbacks run on the event loop, which cannot process new messages while
3144 /// your handler is running. Use `spawn` for any expensive operations:
3145 ///
3146 /// ```no_run
3147 /// # use agent_client_protocol_test::*;
3148 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
3149 /// # let connection = mock_connection();
3150 /// connection.on_receive_request(async |req: ProcessRequest, responder, cx| {
3151 /// // Clone cx for the spawned task
3152 /// cx.spawn({
3153 /// let connection = cx.clone();
3154 /// async move {
3155 /// let result = expensive_operation(&req.data).await?;
3156 /// connection.send_notification(ProcessComplete { result })?;
3157 /// Ok(())
3158 /// }
3159 /// })?;
3160 ///
3161 /// // Respond immediately
3162 /// responder.respond(ProcessResponse { result: "started".into() })
3163 /// }, agent_client_protocol::on_receive_request!())
3164 /// # .connect_to(agent_client_protocol_test::MockTransport).await?;
3165 /// # Ok(())
3166 /// # }
3167 /// ```
3168 ///
3169 /// # Errors
3170 ///
3171 /// If the spawned task returns an error, the entire server will shut down.
3172 #[track_caller]
3173 pub fn spawn(
3174 &self,
3175 task: impl IntoFuture<Output = Result<(), crate::Error>, IntoFuture: Send + 'static>,
3176 ) -> Result<(), crate::Error> {
3177 let location = std::panic::Location::caller();
3178 let task = task.into_future();
3179 Task::new(location, task).spawn(&self.task_tx)
3180 }
3181
3182 /// Spawn a JSON-RPC connection in the background and return a [`ConnectionTo`] for sending messages to it.
3183 ///
3184 /// This is useful for creating multiple connections that communicate with each other,
3185 /// such as implementing proxy patterns or connecting to multiple backend services.
3186 ///
3187 /// # Arguments
3188 ///
3189 /// - `builder`: The connection builder with handlers configured
3190 /// - `transport`: The transport component to connect to
3191 ///
3192 /// # Returns
3193 ///
3194 /// A `ConnectionTo` that you can use to send requests and notifications to the spawned connection.
3195 ///
3196 /// # Example: Proxying to a backend connection
3197 ///
3198 /// ```
3199 /// # use agent_client_protocol::UntypedRole;
3200 /// # use agent_client_protocol::{Builder, ConnectionTo};
3201 /// # use agent_client_protocol_test::*;
3202 /// # async fn example(cx: ConnectionTo<UntypedRole>) -> Result<(), agent_client_protocol::Error> {
3203 /// // Set up a backend connection builder
3204 /// let backend = UntypedRole.builder()
3205 /// .on_receive_request(async |req: MyRequest, responder, _cx| {
3206 /// responder.respond(MyResponse { status: "ok".into() })
3207 /// }, agent_client_protocol::on_receive_request!());
3208 ///
3209 /// // Spawn it and get a context to send requests to it
3210 /// let backend_connection = cx.spawn_connection(backend, MockTransport)?;
3211 ///
3212 /// // Now you can forward requests to the backend
3213 /// let response = backend_connection.send_request(MyRequest {}).block_task().await?;
3214 /// # Ok(())
3215 /// # }
3216 /// ```
3217 #[track_caller]
3218 pub fn spawn_connection<R: Role>(
3219 &self,
3220 builder: Builder<
3221 R,
3222 impl HandleDispatchFrom<R::Counterpart> + 'static,
3223 impl RunWithConnectionTo<R::Counterpart> + 'static,
3224 impl HandleConnectionClose<R::Counterpart> + 'static,
3225 >,
3226 transport: impl ConnectTo<R> + 'static,
3227 ) -> Result<ConnectionTo<R::Counterpart>, crate::Error> {
3228 let (connection, future) =
3229 builder.into_connection_and_future(transport, |_| std::future::pending());
3230 Task::new(std::panic::Location::caller(), future).spawn(&self.task_tx)?;
3231 Ok(connection)
3232 }
3233
3234 /// Send a request/notification and forward the response appropriately.
3235 ///
3236 /// The request context's response type matches the request's response type,
3237 /// enabling type-safe message forwarding.
3238 pub fn send_proxied_message<Req: JsonRpcRequest<Response: Send>, Notif: JsonRpcNotification>(
3239 &self,
3240 message: Dispatch<Req, Notif>,
3241 ) -> Result<(), crate::Error>
3242 where
3243 Counterpart: HasPeer<Counterpart>,
3244 {
3245 self.send_proxied_message_to(self.counterpart(), message)
3246 }
3247
3248 /// Send a request/notification and forward the response appropriately.
3249 ///
3250 /// The request context's response type matches the request's response type,
3251 /// enabling type-safe message forwarding.
3252 ///
3253 /// `$/cancel_request` notifications are *not* forwarded: their `requestId`
3254 /// refers to a request on the connection they arrived over and would be
3255 /// meaningless to `peer`. Cancellation instead propagates hop by hop,
3256 /// because the responders passed to
3257 /// [`forward_response_to`](SentRequest::forward_response_to) observe it
3258 /// and re-issue the cancellation with the forwarded request's own ID.
3259 pub fn send_proxied_message_to<
3260 Peer: Role,
3261 Req: JsonRpcRequest<Response: Send>,
3262 Notif: JsonRpcNotification,
3263 >(
3264 &self,
3265 peer: Peer,
3266 message: Dispatch<Req, Notif>,
3267 ) -> Result<(), crate::Error>
3268 where
3269 Counterpart: HasPeer<Peer>,
3270 {
3271 match message {
3272 Dispatch::Request(request, responder) => self
3273 .send_request_to(peer, request)
3274 .forward_response_to(responder),
3275 Dispatch::Notification(notification) => {
3276 // `$/cancel_request` is connection-scoped: its `requestId` was
3277 // allocated on the connection the notification arrived over
3278 // and means nothing to `peer`. The cancellation has already
3279 // been recorded on this connection's responder markers, and
3280 // `forward_response_to` re-issues it for the forwarded request
3281 // with the correct per-hop ID, so drop the raw notification
3282 // instead of tunneling a meaningless ID across the hop.
3283 if is_cancel_request_notification(¬ification) {
3284 tracing::debug!(
3285 "not forwarding hop-scoped `$/cancel_request` notification across proxy hop"
3286 );
3287 return Ok(());
3288 }
3289 self.send_notification_to(peer, notification)
3290 }
3291 Dispatch::Response(result, router) => {
3292 // Responses are forwarded directly to their destination
3293 router.route_with_result(result)
3294 }
3295 }
3296 }
3297
3298 /// Send an outgoing request and return a [`SentRequest`] for handling the reply.
3299 ///
3300 /// The returned [`SentRequest`] makes the response-consumption mode explicit:
3301 ///
3302 /// * [`on_receiving_result`](SentRequest::on_receiving_result) - Register a callback and
3303 /// return immediately. If registered before the response is routed during its original
3304 /// dispatch, the loop waits for the callback to complete.
3305 /// * [`block_task`](SentRequest::block_task) - Wait on the current task until the response
3306 /// arrives. This is only safe when that task already runs outside the dispatch loop.
3307 ///
3308 /// # Anti-Footgun Design
3309 ///
3310 /// The API intentionally makes it difficult to block on the result directly to prevent
3311 /// the common mistake of blocking the event loop while waiting for a response:
3312 ///
3313 /// ```compile_fail
3314 /// # use agent_client_protocol_test::*;
3315 /// # async fn example(cx: agent_client_protocol::ConnectionTo<agent_client_protocol::UntypedRole>) -> Result<(), agent_client_protocol::Error> {
3316 /// // ❌ This doesn't compile - prevents blocking the event loop
3317 /// let response = cx.send_request(MyRequest {}).await?;
3318 /// # Ok(())
3319 /// # }
3320 /// ```
3321 ///
3322 /// ```no_run
3323 /// # use agent_client_protocol_test::*;
3324 /// # async fn example(cx: agent_client_protocol::ConnectionTo<agent_client_protocol::UntypedRole>) -> Result<(), agent_client_protocol::Error> {
3325 /// // ✅ Option 1: Register an ordered callback (safe in handlers)
3326 /// cx.send_request(MyRequest {})
3327 /// .on_receiving_result(async |result| {
3328 /// // Handle the response
3329 /// Ok(())
3330 /// })?;
3331 ///
3332 /// // ✅ Option 2: Block in spawned task (safe because task is concurrent)
3333 /// cx.spawn({
3334 /// let cx = cx.clone();
3335 /// async move {
3336 /// let response = cx.send_request(MyRequest {})
3337 /// .block_task()
3338 /// .await?;
3339 /// // Process response...
3340 /// Ok(())
3341 /// }
3342 /// })?;
3343 /// # Ok(())
3344 /// # }
3345 /// ```
3346 /// Send an outgoing request to the default counterpart peer.
3347 ///
3348 /// This is a convenience method that sends to the counterpart role `R`.
3349 /// For explicit control over the target peer, use [`send_request_to`](Self::send_request_to).
3350 pub fn send_request<Req: JsonRpcRequest>(&self, request: Req) -> SentRequest<Req::Response>
3351 where
3352 Counterpart: HasPeer<Counterpart>,
3353 {
3354 self.send_request_to(self.counterpart.clone(), request)
3355 }
3356
3357 /// Send an outgoing request to a specific peer.
3358 ///
3359 /// The message will be transformed according to the [`HasPeer`](crate::role::HasPeer)
3360 /// implementation before being sent.
3361 pub fn send_request_to<Peer: Role, Req: JsonRpcRequest>(
3362 &self,
3363 peer: Peer,
3364 request: Req,
3365 ) -> SentRequest<Req::Response>
3366 where
3367 Counterpart: HasPeer<Peer>,
3368 {
3369 let method = request.method().to_string();
3370 let id = RequestId::Str(uuid::Uuid::new_v4().to_string());
3371 let (response_tx, response_rx) = oneshot::channel();
3372 let response_ordering = ResponseOrdering::default();
3373 let role_id = peer.role_id();
3374 let remote_style = self.counterpart.remote_style(peer);
3375 let cancellation =
3376 SentRequestCancellation::new(self.message_tx.clone(), remote_style, id.clone());
3377 if self.is_incoming_closing() {
3378 cancellation.disarm();
3379 drop(response_tx.send(ResponsePayload {
3380 result: Err(incoming_transport_closed_error(&method)),
3381 ack_tx: None,
3382 }));
3383 return SentRequest::new(
3384 id,
3385 method.clone(),
3386 self.task_tx.clone(),
3387 response_rx,
3388 cancellation,
3389 response_ordering,
3390 )
3391 .map(move |json| <Req::Response>::from_value(&method, json));
3392 }
3393
3394 match remote_style.transform_outgoing_message(request) {
3395 Ok(untyped) => {
3396 // Register before enqueueing so incoming EOF can fail every
3397 // observable request before close callbacks begin. The
3398 // outgoing actor checks that the registration still exists
3399 // before sending the request.
3400 let pending_reply = PendingReply {
3401 method: method.clone(),
3402 role_id,
3403 sender: response_tx,
3404 cancellation_disarm: cancellation.disarm_handle(),
3405 ordering: response_ordering.clone(),
3406 };
3407
3408 if self
3409 .pending_replies
3410 .subscribe(id.clone(), pending_reply, &self.incoming_closed)
3411 {
3412 let message = OutgoingMessage::Request {
3413 id: id.clone(),
3414 method: method.clone(),
3415 untyped,
3416 };
3417
3418 if let Err(error) = self.message_tx.unbounded_send(message) {
3419 cancellation.disarm();
3420
3421 let OutgoingMessage::Request { id, method, .. } = error.into_inner() else {
3422 unreachable!();
3423 };
3424
3425 if let Some(pending_reply) = self.pending_replies.remove(&id) {
3426 if self.is_incoming_closing() {
3427 pending_reply.fail_incoming_closed();
3428 } else {
3429 pending_reply.fail(crate::util::internal_error(format!(
3430 "failed to send outgoing request `{method}`"
3431 )));
3432 }
3433 }
3434 }
3435 }
3436 }
3437
3438 Err(err) => {
3439 cancellation.disarm();
3440
3441 response_tx
3442 .send(ResponsePayload {
3443 result: Err(crate::util::internal_error(format!(
3444 "failed to create untyped request for `{method}`: {err}"
3445 ))),
3446 ack_tx: None,
3447 })
3448 .unwrap();
3449 }
3450 }
3451
3452 SentRequest::new(
3453 id,
3454 method.clone(),
3455 self.task_tx.clone(),
3456 response_rx,
3457 cancellation,
3458 response_ordering,
3459 )
3460 .map(move |json| <Req::Response>::from_value(&method, json))
3461 }
3462
3463 /// Send an outgoing notification to the default counterpart peer (no reply expected).
3464 ///
3465 /// Notifications are fire-and-forget messages that don't have IDs and don't expect responses.
3466 /// This method sends the notification immediately and returns.
3467 ///
3468 /// This is a convenience method that sends to the counterpart role `R`.
3469 /// For explicit control over the target peer, use [`send_notification_to`](Self::send_notification_to).
3470 ///
3471 /// ```no_run
3472 /// # use agent_client_protocol_test::*;
3473 /// # async fn example(cx: agent_client_protocol::ConnectionTo<agent_client_protocol::Agent>) -> Result<(), agent_client_protocol::Error> {
3474 /// cx.send_notification(StatusUpdate {
3475 /// message: "Processing...".into(),
3476 /// })?;
3477 /// # Ok(())
3478 /// # }
3479 /// ```
3480 pub fn send_notification<N: JsonRpcNotification>(
3481 &self,
3482 notification: N,
3483 ) -> Result<(), crate::Error>
3484 where
3485 Counterpart: HasPeer<Counterpart>,
3486 {
3487 self.send_notification_to(self.counterpart.clone(), notification)
3488 }
3489
3490 /// Send an outgoing notification to a specific peer (no reply expected).
3491 ///
3492 /// The message will be transformed according to the [`HasPeer`](crate::role::HasPeer)
3493 /// implementation before being sent.
3494 pub fn send_notification_to<Peer: Role, N: JsonRpcNotification>(
3495 &self,
3496 peer: Peer,
3497 notification: N,
3498 ) -> Result<(), crate::Error>
3499 where
3500 Counterpart: HasPeer<Peer>,
3501 {
3502 let remote_style = self.counterpart.remote_style(peer);
3503 tracing::debug!(
3504 role = std::any::type_name::<Counterpart>(),
3505 peer = std::any::type_name::<Peer>(),
3506 notification_type = std::any::type_name::<N>(),
3507 ?remote_style,
3508 original_method = notification.method(),
3509 "send_notification_to"
3510 );
3511 let transformed = remote_style.transform_outgoing_message(notification)?;
3512 tracing::debug!(
3513 transformed_method = %transformed.method,
3514 "send_notification_to transformed"
3515 );
3516 send_raw_message(
3517 &self.message_tx,
3518 OutgoingMessage::Notification {
3519 untyped: transformed,
3520 },
3521 )
3522 }
3523
3524 /// Send a `$/cancel_request` notification for an arbitrary request ID to
3525 /// the default counterpart peer.
3526 ///
3527 /// Prefer [`SentRequest::cancel`] when you have the request handle: it
3528 /// already knows the correct peer, request ID, and proxy wrapping. Use this
3529 /// low-level method only when implementing custom routing with a request ID
3530 /// that is valid on this connection.
3531 pub fn send_cancel_request(
3532 &self,
3533 request_id: impl Into<crate::schema::v1::RequestId>,
3534 ) -> Result<(), crate::Error>
3535 where
3536 Counterpart: HasPeer<Counterpart>,
3537 {
3538 self.send_cancel_request_to(self.counterpart.clone(), request_id)
3539 }
3540
3541 /// Send a `$/cancel_request` notification for an arbitrary request ID to a
3542 /// specific peer.
3543 ///
3544 /// Prefer [`SentRequest::cancel`] when you have the request handle: it
3545 /// already knows the correct peer, request ID, and proxy wrapping. Use this
3546 /// low-level method only when implementing custom routing with a request ID
3547 /// that is valid on the target peer's connection.
3548 pub fn send_cancel_request_to<Peer: Role>(
3549 &self,
3550 peer: Peer,
3551 request_id: impl Into<crate::schema::v1::RequestId>,
3552 ) -> Result<(), crate::Error>
3553 where
3554 Counterpart: HasPeer<Peer>,
3555 {
3556 self.send_notification_to(
3557 peer,
3558 crate::schema::v1::CancelRequestNotification::new(request_id),
3559 )
3560 }
3561
3562 /// Register a dynamic message handler, used to intercept messages specific to a particular session
3563 /// or some similar modal thing.
3564 ///
3565 /// Dynamic message handlers run after the handlers registered on [`Builder`] and before the
3566 /// role's default handler. They receive messages that the builder handlers decline.
3567 ///
3568 /// The handler will stay registered until the returned registration guard is dropped.
3569 pub fn add_dynamic_handler(
3570 &self,
3571 handler: impl HandleDispatchFrom<Counterpart> + 'static,
3572 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error> {
3573 let uuid = Uuid::new_v4();
3574 self.dynamic_handler_tx
3575 .unbounded_send(DynamicHandlerMessage::AddDynamicHandler(
3576 uuid,
3577 Box::new(handler),
3578 ))
3579 .map_err(crate::util::internal_error)?;
3580
3581 Ok(DynamicHandlerGuard::new(uuid, self.clone()))
3582 }
3583
3584 fn remove_dynamic_handler(&self, uuid: Uuid) {
3585 // Ignore errors
3586 drop(
3587 self.dynamic_handler_tx
3588 .unbounded_send(DynamicHandlerMessage::RemoveDynamicHandler(uuid)),
3589 );
3590 }
3591}
3592
3593/// A guard that keeps a dynamic message handler registered.
3594///
3595/// Dropping the guard unregisters the handler. Use [`detach`](Self::detach) to
3596/// keep the handler registered for the remaining lifetime of the connection.
3597#[must_use = "dropping this guard unregisters the dynamic handler"]
3598#[derive(Debug)]
3599pub struct DynamicHandlerGuard<R: Role> {
3600 uuid: Option<Uuid>,
3601 cx: ConnectionTo<R>,
3602}
3603
3604impl<R: Role> DynamicHandlerGuard<R> {
3605 fn new(uuid: Uuid, cx: ConnectionTo<R>) -> Self {
3606 Self {
3607 uuid: Some(uuid),
3608 cx,
3609 }
3610 }
3611
3612 /// Keep the dynamic handler registered after this guard is dropped.
3613 ///
3614 /// The handler remains registered until the connection itself shuts down.
3615 /// Unlike leaking the guard, detaching does not retain an extra
3616 /// [`ConnectionTo`] handle.
3617 pub fn detach(mut self) {
3618 self.uuid = None;
3619 }
3620}
3621
3622impl<R: Role> Drop for DynamicHandlerGuard<R> {
3623 fn drop(&mut self) {
3624 if let Some(uuid) = self.uuid {
3625 self.cx.remove_dynamic_handler(uuid);
3626 }
3627 }
3628}
3629
3630/// The context to respond to an incoming request.
3631///
3632/// This context is provided to request handlers and serves a dual role:
3633///
3634/// 1. **Respond to the request** - Use [`respond`](Self::respond) or
3635/// [`respond_with_result`](Self::respond_with_result) to send the response
3636/// 2. **Send other messages** - Use the [`ConnectionTo`] parameter passed to your
3637/// handler, which provides [`send_request`](`ConnectionTo::send_request`),
3638/// [`send_notification`](`ConnectionTo::send_notification`), and
3639/// [`spawn`](`ConnectionTo::spawn`)
3640///
3641/// # Example
3642///
3643/// ```no_run
3644/// # use agent_client_protocol_test::*;
3645/// # async fn example() -> Result<(), agent_client_protocol::Error> {
3646/// # let connection = mock_connection();
3647/// connection.on_receive_request(async |req: ProcessRequest, responder, cx| {
3648/// // Send a notification while processing
3649/// cx.send_notification(StatusUpdate {
3650/// message: "processing".into(),
3651/// })?;
3652///
3653/// // Do some work...
3654/// let result = process(&req.data)?;
3655///
3656/// // Respond to the request
3657/// responder.respond(ProcessResponse { result })
3658/// }, agent_client_protocol::on_receive_request!())
3659/// # .connect_to(agent_client_protocol_test::MockTransport).await?;
3660/// # Ok(())
3661/// # }
3662/// ```
3663///
3664/// # Event Loop Considerations
3665///
3666/// Like all handlers, request handlers run on the event loop. Use
3667/// [`spawn`](ConnectionTo::spawn) for expensive operations to avoid blocking
3668/// the connection.
3669///
3670/// See the [Event Loop and Concurrency](Builder#event-loop-and-concurrency)
3671/// section for more details.
3672///
3673/// # Drop behavior
3674///
3675/// Dropping a responder for a request that arrived in a batch completes that
3676/// slot with an Internal Error, so one abandoned request cannot withhold valid
3677/// sibling responses forever. A responder for an individual request retains
3678/// the historical behavior: dropping it does not automatically send a reply.
3679#[must_use]
3680pub struct Responder<T: JsonRpcResponse = serde_json::Value> {
3681 /// The method of the request.
3682 method: String,
3683
3684 /// The `id` of the message we are replying to.
3685 id: RequestId,
3686
3687 /// Request-local cancellation state.
3688 cancellation: ResponderCancellation,
3689
3690 /// Whether this response is emitted on its own or collected into a batch.
3691 destination: ResponseDestination,
3692
3693 /// Function to send the response to its destination.
3694 ///
3695 /// For incoming requests: serializes to JSON and sends over the wire.
3696 /// For incoming responses: sends to the waiting oneshot channel.
3697 send_fn: Box<dyn FnOnce(Result<T, crate::Error>) -> Result<(), crate::Error> + Send>,
3698
3699 /// Completes an abandoned batch slot unless an explicit response disarms it.
3700 drop_guard: ResponderDropGuard,
3701}
3702
3703struct ResponderDropGuard {
3704 message_tx: OutgoingMessageTx,
3705 id: RequestId,
3706 method: String,
3707 destination: ResponseDestination,
3708 armed: bool,
3709}
3710
3711impl ResponderDropGuard {
3712 fn disarm(&mut self) {
3713 self.armed = false;
3714 }
3715}
3716
3717impl Drop for ResponderDropGuard {
3718 fn drop(&mut self) {
3719 if !self.armed || !self.destination.is_batch() {
3720 return;
3721 }
3722
3723 if let Err(error) = send_raw_message(
3724 &self.message_tx,
3725 OutgoingMessage::AbandonedBatchResponse {
3726 id: self.id.clone(),
3727 method: self.method.clone(),
3728 destination: self.destination.clone(),
3729 },
3730 ) {
3731 tracing::debug!(
3732 id = ?self.id,
3733 method = %self.method,
3734 ?error,
3735 "could not complete abandoned JSON-RPC batch response slot"
3736 );
3737 }
3738 }
3739}
3740
3741impl<T: JsonRpcResponse> std::fmt::Debug for Responder<T> {
3742 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3743 f.debug_struct("Responder")
3744 .field("method", &self.method)
3745 .field("id", &self.id)
3746 .field("response_type", &std::any::type_name::<T>())
3747 .finish_non_exhaustive()
3748 }
3749}
3750
3751impl Responder<serde_json::Value> {
3752 /// Create a new request context for an incoming request.
3753 ///
3754 /// The response will be serialized to JSON and sent over the wire.
3755 fn new(
3756 message_tx: OutgoingMessageTx,
3757 method: String,
3758 id: RequestId,
3759 cancellation_registry: &RequestCancellationRegistry,
3760 destination: ResponseDestination,
3761 ) -> Self {
3762 let id_clone = id.clone();
3763 let method_clone = method.clone();
3764 let cancellation = cancellation_registry.register(&id);
3765 let send_destination = destination.clone();
3766 let drop_guard = ResponderDropGuard {
3767 message_tx: message_tx.clone(),
3768 id: id.clone(),
3769 method: method.clone(),
3770 destination: destination.clone(),
3771 armed: true,
3772 };
3773 Self {
3774 method,
3775 id,
3776 cancellation,
3777 destination,
3778 send_fn: Box::new(move |response: Result<serde_json::Value, crate::Error>| {
3779 send_raw_message(
3780 &message_tx,
3781 OutgoingMessage::Response {
3782 id: id_clone,
3783 method: method_clone,
3784 response,
3785 destination: send_destination,
3786 },
3787 )
3788 }),
3789 drop_guard,
3790 }
3791 }
3792
3793 /// Cast this request context to a different response type.
3794 ///
3795 /// The provided type `T` will be serialized to JSON before sending.
3796 pub fn cast<T: JsonRpcResponse>(self) -> Responder<T> {
3797 self.wrap_params(move |method, value| match value {
3798 Ok(value) => T::into_json(value, method),
3799 Err(e) => Err(e),
3800 })
3801 }
3802}
3803
3804impl<T: JsonRpcResponse> Responder<T> {
3805 /// Method of the incoming request
3806 #[must_use]
3807 pub fn method(&self) -> &str {
3808 &self.method
3809 }
3810
3811 /// ID of the incoming request.
3812 #[must_use]
3813 pub fn id(&self) -> &RequestId {
3814 &self.id
3815 }
3816
3817 /// Returns the cancellation marker for this request.
3818 ///
3819 /// The marker is set when the peer sends `$/cancel_request` for this
3820 /// request's JSON-RPC ID. Cancellation is cooperative: handlers should use
3821 /// the marker to stop long-running work and then decide whether to respond
3822 /// with [`Error::request_cancelled`] or partial data.
3823 ///
3824 /// [`Error::request_cancelled`]: crate::Error::request_cancelled
3825 #[must_use]
3826 pub fn cancellation(&self) -> RequestCancellation {
3827 self.cancellation.cancellation()
3828 }
3829
3830 /// Convert to a `Responder` that expects a JSON value
3831 /// and which checks (dynamically) that the JSON value it receives
3832 /// can be converted to `T`.
3833 pub fn erase_to_json(self) -> Responder<serde_json::Value> {
3834 self.wrap_params(|method, value| T::from_value(method, value?))
3835 }
3836
3837 /// Return a new Responder with a different method name.
3838 pub fn wrap_method(mut self, method: String) -> Responder<T> {
3839 self.drop_guard.method.clone_from(&method);
3840 Responder {
3841 method,
3842 id: self.id,
3843 cancellation: self.cancellation,
3844 destination: self.destination,
3845 send_fn: self.send_fn,
3846 drop_guard: self.drop_guard,
3847 }
3848 }
3849
3850 /// Return a new Responder that expects a response of type U.
3851 ///
3852 /// `wrap_fn` will be invoked with the method name and the result to transform
3853 /// type `U` into type `T` before sending.
3854 pub fn wrap_params<U: JsonRpcResponse>(
3855 self,
3856 wrap_fn: impl FnOnce(&str, Result<U, crate::Error>) -> Result<T, crate::Error> + Send + 'static,
3857 ) -> Responder<U> {
3858 let method = self.method.clone();
3859 Responder {
3860 method: self.method,
3861 id: self.id,
3862 cancellation: self.cancellation,
3863 destination: self.destination,
3864 send_fn: Box::new(move |input: Result<U, crate::Error>| {
3865 let t_value = wrap_fn(&method, input);
3866 (self.send_fn)(t_value)
3867 }),
3868 drop_guard: self.drop_guard,
3869 }
3870 }
3871
3872 /// Respond to the JSON-RPC request with either a value (`Ok`) or an error (`Err`).
3873 pub fn respond_with_result(
3874 mut self,
3875 response: Result<T, crate::Error>,
3876 ) -> Result<(), crate::Error> {
3877 tracing::debug!(id = ?self.id, "respond called");
3878 self.drop_guard.disarm();
3879 (self.send_fn)(response)
3880 }
3881
3882 /// Respond to the JSON-RPC request with a value.
3883 pub fn respond(self, response: T) -> Result<(), crate::Error> {
3884 self.respond_with_result(Ok(response))
3885 }
3886
3887 /// Respond to the JSON-RPC request with an internal error containing a message.
3888 pub fn respond_with_internal_error(self, message: impl ToString) -> Result<(), crate::Error> {
3889 self.respond_with_error(crate::util::internal_error(message))
3890 }
3891
3892 /// Respond to the JSON-RPC request with an error.
3893 pub fn respond_with_error(self, error: crate::Error) -> Result<(), crate::Error> {
3894 tracing::debug!(id = ?self.id, ?error, "respond_with_error called");
3895 self.respond_with_result(Err(error))
3896 }
3897
3898 fn reply_target(&self) -> RequestReplyTarget {
3899 RequestReplyTarget {
3900 id: self.id.clone(),
3901 method: self.method.clone(),
3902 destination: self.destination.clone(),
3903 }
3904 }
3905}
3906
3907/// Context for handling an incoming JSON-RPC response.
3908///
3909/// This is the response-side counterpart to [`Responder`]. While `Responder` handles
3910/// incoming requests (where you send a response over the wire), `ResponseRouter` handles
3911/// incoming responses (where you route the response to a local task waiting for it).
3912///
3913/// Both are fundamentally "sinks" that push the message through a `send_fn`, but they
3914/// represent different points in the message lifecycle and carry different metadata.
3915///
3916/// # Drop Behavior
3917///
3918/// Dropping a `ResponseRouter` without routing the response (for example, from a
3919/// dispatch handler that claims a [`Dispatch::Response`]) discards the
3920/// response: the local awaiter observes the response as never received. The
3921/// request still counts as settled: routing a response this far disarms the
3922/// originating [`SentRequest`]'s drop-time auto-cancellation even if the router
3923/// is never invoked, since the peer has already answered.
3924#[must_use]
3925pub struct ResponseRouter<T: JsonRpcResponse = serde_json::Value> {
3926 /// The method of the original request.
3927 method: String,
3928
3929 /// The `id` of the original request.
3930 id: RequestId,
3931
3932 /// The RoleId to which the original request was sent
3933 /// (and hence from which the reply is expected).
3934 role_id: RoleId,
3935
3936 /// Function to send the response to the waiting task.
3937 send_fn: Box<dyn FnOnce(Result<T, crate::Error>) -> Result<(), crate::Error> + Send>,
3938
3939 /// Shared route used to deliver a dispatch-handler error to the same waiter.
3940 reply_target: ResponseReplyTarget,
3941}
3942
3943impl<T: JsonRpcResponse> std::fmt::Debug for ResponseRouter<T> {
3944 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3945 f.debug_struct("ResponseRouter")
3946 .field("method", &self.method)
3947 .field("id", &self.id)
3948 .field("response_type", &std::any::type_name::<T>())
3949 .finish_non_exhaustive()
3950 }
3951}
3952
3953impl ResponseRouter<serde_json::Value> {
3954 /// Create a new response context for routing a response to a local awaiter.
3955 ///
3956 /// When [`route_with_result`](Self::route_with_result) is called, the response is sent through the oneshot
3957 /// channel to the code that originally sent the request. If that receiver was
3958 /// dropped, the response is discarded because there is no local awaiter left.
3959 fn new(
3960 method: String,
3961 id: RequestId,
3962 role_id: RoleId,
3963 sender: oneshot::Sender<ResponsePayload>,
3964 cancellation_disarm: SentRequestCancellationDisarm,
3965 ordering: ResponseOrdering,
3966 dispatch: ResponseDispatch,
3967 ) -> Self {
3968 let reply_target = ResponseReplyTarget {
3969 id: id.clone(),
3970 method: method.clone(),
3971 sender: Arc::new(Mutex::new(Some(sender))),
3972 ordering,
3973 dispatch,
3974 };
3975 let send_target = reply_target.clone();
3976 // A response for the request reached this router, so the request is
3977 // settled from the peer's perspective and a `$/cancel_request` could
3978 // only ever be redundant. Disarm immediately so handlers may retain
3979 // the router without leaving auto-cancellation armed.
3980 cancellation_disarm.disarm();
3981 Self {
3982 method,
3983 id,
3984 role_id,
3985 send_fn: Box::new(move |response: Result<serde_json::Value, crate::Error>| {
3986 send_target.route(response);
3987 Ok(())
3988 }),
3989 reply_target,
3990 }
3991 }
3992
3993 /// Cast this response context to a different response type.
3994 ///
3995 /// The provided type `T` will be serialized to JSON before sending.
3996 pub fn cast<T: JsonRpcResponse>(self) -> ResponseRouter<T> {
3997 self.wrap_params(move |method, value| match value {
3998 Ok(value) => T::into_json(value, method),
3999 Err(e) => Err(e),
4000 })
4001 }
4002}
4003
4004impl<T: JsonRpcResponse> ResponseRouter<T> {
4005 /// Method of the original request
4006 #[must_use]
4007 pub fn method(&self) -> &str {
4008 &self.method
4009 }
4010
4011 /// ID of the original request.
4012 #[must_use]
4013 pub fn id(&self) -> &RequestId {
4014 &self.id
4015 }
4016
4017 /// The peer to which the original request was sent.
4018 ///
4019 /// This is the peer from which we expect to receive the response.
4020 #[must_use]
4021 pub fn role_id(&self) -> RoleId {
4022 self.role_id.clone()
4023 }
4024
4025 /// Convert to a `ResponseRouter` that expects a JSON value
4026 /// and which checks (dynamically) that the JSON value it receives
4027 /// can be converted to `T`.
4028 pub fn erase_to_json(self) -> ResponseRouter<serde_json::Value> {
4029 self.wrap_params(|method, value| T::from_value(method, value?))
4030 }
4031
4032 /// Return a new ResponseRouter that expects a response of type U.
4033 ///
4034 /// `wrap_fn` will be invoked with the method name and the result to transform
4035 /// type `U` into type `T` before sending.
4036 fn wrap_params<U: JsonRpcResponse>(
4037 self,
4038 wrap_fn: impl FnOnce(&str, Result<U, crate::Error>) -> Result<T, crate::Error> + Send + 'static,
4039 ) -> ResponseRouter<U> {
4040 let method = self.method.clone();
4041 ResponseRouter {
4042 method: self.method,
4043 id: self.id,
4044 role_id: self.role_id,
4045 send_fn: Box::new(move |input: Result<U, crate::Error>| {
4046 let t_value = wrap_fn(&method, input);
4047 (self.send_fn)(t_value)
4048 }),
4049 reply_target: self.reply_target,
4050 }
4051 }
4052
4053 /// Route the response result to the waiting task.
4054 pub fn route_with_result(self, response: Result<T, crate::Error>) -> Result<(), crate::Error> {
4055 tracing::debug!(id = ?self.id, "response routed to awaiter");
4056 (self.send_fn)(response)
4057 }
4058
4059 /// Route a successful response value to the waiting task.
4060 pub fn route(self, response: T) -> Result<(), crate::Error> {
4061 self.route_with_result(Ok(response))
4062 }
4063
4064 /// Route an internal error to the waiting task.
4065 pub fn route_with_internal_error(self, message: impl ToString) -> Result<(), crate::Error> {
4066 self.route_with_error(crate::util::internal_error(message))
4067 }
4068
4069 /// Route an error response to the waiting task.
4070 pub fn route_with_error(self, error: crate::Error) -> Result<(), crate::Error> {
4071 tracing::debug!(id = ?self.id, ?error, "error routed to awaiter");
4072 self.route_with_result(Err(error))
4073 }
4074}
4075
4076/// Common bounds for any JSON-RPC message.
4077///
4078/// # Derive Macro
4079///
4080/// For simple message types, you can use the `JsonRpcRequest` or `JsonRpcNotification` derive macros
4081/// which will implement both `JsonRpcMessage` and the respective trait. See [`JsonRpcRequest`] and
4082/// [`JsonRpcNotification`] for examples.
4083pub trait JsonRpcMessage: 'static + Debug + Sized + Send + Clone {
4084 /// Check if this message type matches the given method name.
4085 fn matches_method(method: &str) -> bool;
4086
4087 /// The method name for the message.
4088 fn method(&self) -> &str;
4089
4090 /// Convert this message into an untyped message.
4091 fn to_untyped_message(&self) -> Result<UntypedMessage, crate::Error>;
4092
4093 /// Parse this type from a method name and parameters.
4094 ///
4095 /// Returns an error if the method doesn't match or deserialization fails.
4096 /// Callers should use `matches_method` first to check if this type handles the method.
4097 fn parse_message(method: &str, params: &impl Serialize) -> Result<Self, crate::Error>;
4098}
4099
4100/// Defines the "payload" of a successful response to a JSON-RPC request.
4101///
4102/// # Derive Macro
4103///
4104/// Use `#[derive(JsonRpcResponse)]` to automatically implement this trait:
4105///
4106/// ```ignore
4107/// use agent_client_protocol::JsonRpcResponse;
4108/// use serde::{Serialize, Deserialize};
4109///
4110/// #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)]
4111/// struct HelloResponse {
4112/// greeting: String,
4113/// }
4114/// ```
4115pub trait JsonRpcResponse: 'static + Debug + Sized + Send + Clone {
4116 /// Convert this message into a JSON value.
4117 fn into_json(self, method: &str) -> Result<serde_json::Value, crate::Error>;
4118
4119 /// Parse a JSON value into the response type.
4120 fn from_value(method: &str, value: serde_json::Value) -> Result<Self, crate::Error>;
4121}
4122
4123impl JsonRpcResponse for serde_json::Value {
4124 fn from_value(_method: &str, value: serde_json::Value) -> Result<Self, crate::Error> {
4125 Ok(value)
4126 }
4127
4128 fn into_json(self, _method: &str) -> Result<serde_json::Value, crate::Error> {
4129 Ok(self)
4130 }
4131}
4132
4133/// A struct that represents a notification (JSON-RPC message that does not expect a response).
4134///
4135/// # Derive Macro
4136///
4137/// Use `#[derive(JsonRpcNotification)]` to automatically implement both `JsonRpcMessage` and `JsonRpcNotification`:
4138///
4139/// ```ignore
4140/// use agent_client_protocol::JsonRpcNotification;
4141/// use serde::{Serialize, Deserialize};
4142///
4143/// #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcNotification)]
4144/// #[notification(method = "_ping")]
4145/// struct PingNotification {
4146/// timestamp: u64,
4147/// }
4148/// ```
4149pub trait JsonRpcNotification: JsonRpcMessage {}
4150
4151/// A struct that represents a request (JSON-RPC message expecting a response).
4152///
4153/// # Derive Macro
4154///
4155/// Use `#[derive(JsonRpcRequest)]` to automatically implement both `JsonRpcMessage` and `JsonRpcRequest`:
4156///
4157/// ```ignore
4158/// use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse};
4159/// use serde::{Serialize, Deserialize};
4160///
4161/// #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)]
4162/// #[request(method = "_hello", response = HelloResponse)]
4163/// struct HelloRequest {
4164/// name: String,
4165/// }
4166///
4167/// #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)]
4168/// struct HelloResponse {
4169/// greeting: String,
4170/// }
4171/// ```
4172pub trait JsonRpcRequest: JsonRpcMessage {
4173 /// The type of data expected in response.
4174 type Response: JsonRpcResponse;
4175}
4176
4177/// An incoming request, notification, or response being dispatched through handlers.
4178/// Requests include the context used to answer them; responses include the context
4179/// used to route them to the local requester.
4180///
4181/// Type parameters allow specifying the concrete request and notification types.
4182/// By default, both are `UntypedMessage` for dynamic dispatch.
4183/// The request context's response type matches the request's response type.
4184#[derive(Debug)]
4185pub enum Dispatch<Req: JsonRpcRequest = UntypedMessage, Notif: JsonRpcNotification = UntypedMessage>
4186{
4187 /// Incoming request and the context where the response should be sent.
4188 Request(Req, Responder<Req::Response>),
4189
4190 /// Incoming notification.
4191 Notification(Notif),
4192
4193 /// Incoming response to a request we sent.
4194 ///
4195 /// The first field is the response result (success or error from the remote).
4196 /// The second field is the context for forwarding the response to its destination
4197 /// (typically a waiting oneshot channel).
4198 Response(
4199 Result<Req::Response, crate::Error>,
4200 ResponseRouter<Req::Response>,
4201 ),
4202}
4203
4204impl<Req: JsonRpcRequest, Notif: JsonRpcNotification> Dispatch<Req, Notif> {
4205 /// Map the request and notification types to new types.
4206 ///
4207 /// Note: Response variants are passed through unchanged since they don't
4208 /// contain a parseable message payload.
4209 pub fn map<Req1, Notif1>(
4210 self,
4211 map_request: impl FnOnce(Req, Responder<Req::Response>) -> (Req1, Responder<Req1::Response>),
4212 map_notification: impl FnOnce(Notif) -> Notif1,
4213 ) -> Dispatch<Req1, Notif1>
4214 where
4215 Req1: JsonRpcRequest<Response = Req::Response>,
4216 Notif1: JsonRpcNotification,
4217 {
4218 match self {
4219 Dispatch::Request(request, responder) => {
4220 let (new_request, new_responder) = map_request(request, responder);
4221 Dispatch::Request(new_request, new_responder)
4222 }
4223 Dispatch::Notification(notification) => {
4224 let new_notification = map_notification(notification);
4225 Dispatch::Notification(new_notification)
4226 }
4227 Dispatch::Response(result, router) => Dispatch::Response(result, router),
4228 }
4229 }
4230
4231 /// Convert the message in self to an untyped message.
4232 ///
4233 /// Note: Response variants don't have an untyped message representation.
4234 /// This returns an error for Response variants.
4235 pub fn to_untyped_message(&self) -> Result<UntypedMessage, crate::Error> {
4236 match self {
4237 Dispatch::Request(request, _) => request.to_untyped_message(),
4238 Dispatch::Notification(notification) => notification.to_untyped_message(),
4239 Dispatch::Response(_, _) => Err(crate::util::internal_error(
4240 "Response variant has no untyped message representation",
4241 )),
4242 }
4243 }
4244
4245 /// Convert self to an untyped message context.
4246 ///
4247 /// Note: Response variants cannot be converted. This returns an error for Response variants.
4248 pub fn into_untyped_dispatch(self) -> Result<Dispatch, crate::Error> {
4249 match self {
4250 Dispatch::Request(request, responder) => Ok(Dispatch::Request(
4251 request.to_untyped_message()?,
4252 responder.erase_to_json(),
4253 )),
4254 Dispatch::Notification(notification) => {
4255 Ok(Dispatch::Notification(notification.to_untyped_message()?))
4256 }
4257 Dispatch::Response(_, _) => Err(crate::util::internal_error(
4258 "cannot convert Response variant to untyped message context",
4259 )),
4260 }
4261 }
4262
4263 /// Returns the request ID if this is a request or response, None if notification.
4264 pub fn id(&self) -> Option<&RequestId> {
4265 match self {
4266 Dispatch::Request(_, cx) => Some(cx.id()),
4267 Dispatch::Notification(_) => None,
4268 Dispatch::Response(_, cx) => Some(cx.id()),
4269 }
4270 }
4271
4272 fn handler_error_target(&self) -> Option<HandlerErrorTarget> {
4273 match self {
4274 Dispatch::Request(_, responder) => {
4275 Some(HandlerErrorTarget::Request(responder.reply_target()))
4276 }
4277 Dispatch::Notification(_) => None,
4278 Dispatch::Response(_, router) => {
4279 Some(HandlerErrorTarget::Response(router.reply_target.clone()))
4280 }
4281 }
4282 }
4283
4284 /// Returns the method of the message.
4285 ///
4286 /// For requests and notifications, this is the method from the message payload.
4287 /// For responses, this is the method of the original request.
4288 pub fn method(&self) -> &str {
4289 match self {
4290 Dispatch::Request(msg, _) => msg.method(),
4291 Dispatch::Notification(msg) => msg.method(),
4292 Dispatch::Response(_, cx) => cx.method(),
4293 }
4294 }
4295}
4296
4297impl Dispatch {
4298 /// Attempts to parse `self` into a typed message context.
4299 ///
4300 /// # Returns
4301 ///
4302 /// * `Ok(Ok(typed))` if this dispatch matches the requested type for its variant
4303 /// * `Ok(Err(self))` if it does not match the requested type for its variant
4304 /// * `Err` if its method matches the requested type but parsing fails
4305 #[tracing::instrument(skip(self), fields(Request = ?std::any::type_name::<Req>(), Notif = ?std::any::type_name::<Notif>()), level = "trace", ret)]
4306 pub(crate) fn into_typed_dispatch<Req: JsonRpcRequest, Notif: JsonRpcNotification>(
4307 self,
4308 ) -> Result<Result<Dispatch<Req, Notif>, Dispatch>, crate::Error> {
4309 tracing::debug!(
4310 message = ?self,
4311 "into_typed_dispatch"
4312 );
4313 match self {
4314 Dispatch::Request(message, responder) => {
4315 if Req::matches_method(&message.method) {
4316 match Req::parse_message(&message.method, &message.params) {
4317 Ok(req) => {
4318 tracing::trace!(?req, "parsed ok");
4319 Ok(Ok(Dispatch::Request(req, responder.cast())))
4320 }
4321 Err(err) => {
4322 tracing::trace!(?err, "parse error");
4323 Err(err)
4324 }
4325 }
4326 } else {
4327 tracing::trace!("method doesn't match");
4328 Ok(Err(Dispatch::Request(message, responder)))
4329 }
4330 }
4331
4332 Dispatch::Notification(message) => {
4333 if Notif::matches_method(&message.method) {
4334 match Notif::parse_message(&message.method, &message.params) {
4335 Ok(notif) => {
4336 tracing::trace!(?notif, "parse ok");
4337 Ok(Ok(Dispatch::Notification(notif)))
4338 }
4339 Err(err) => {
4340 tracing::trace!(?err, "parse error");
4341 Err(err)
4342 }
4343 }
4344 } else {
4345 tracing::trace!("method doesn't match");
4346 Ok(Err(Dispatch::Notification(message)))
4347 }
4348 }
4349
4350 Dispatch::Response(result, cx) => {
4351 let method = cx.method();
4352 if Req::matches_method(method) {
4353 // Parse the response result
4354 let typed_result = match result {
4355 Ok(value) => {
4356 match <Req::Response as JsonRpcResponse>::from_value(method, value) {
4357 Ok(parsed) => {
4358 tracing::trace!(?parsed, "parse ok");
4359 Ok(parsed)
4360 }
4361 Err(err) => {
4362 tracing::trace!(?err, "parse error");
4363 return Err(err);
4364 }
4365 }
4366 }
4367 Err(err) => {
4368 tracing::trace!("error, passthrough");
4369 Err(err)
4370 }
4371 };
4372 Ok(Ok(Dispatch::Response(typed_result, cx.cast())))
4373 } else {
4374 tracing::trace!("method doesn't match");
4375 Ok(Err(Dispatch::Response(result, cx)))
4376 }
4377 }
4378 }
4379 }
4380
4381 /// True if this message has a field with the given name.
4382 ///
4383 /// Returns `false` for Response variants.
4384 #[must_use]
4385 pub fn has_field(&self, field_name: &str) -> bool {
4386 self.message()
4387 .and_then(|m| m.params().get(field_name))
4388 .is_some()
4389 }
4390
4391 /// Returns true if this message has a session-id field.
4392 ///
4393 /// Returns `false` for Response variants.
4394 pub(crate) fn has_session_id(&self) -> bool {
4395 self.has_field("sessionId")
4396 }
4397
4398 /// Extract the ACP session-id from this message (if any).
4399 ///
4400 /// Returns `Ok(None)` for Response variants.
4401 pub(crate) fn get_session_id(&self) -> Result<Option<SessionId>, crate::Error> {
4402 let Some(message) = self.message() else {
4403 return Ok(None);
4404 };
4405 let Some(value) = message.params().get("sessionId") else {
4406 return Ok(None);
4407 };
4408 let session_id = serde_json::from_value(value.clone())?;
4409 Ok(Some(session_id))
4410 }
4411
4412 /// Try to parse this as a notification of the given type.
4413 ///
4414 /// # Returns
4415 ///
4416 /// * `Ok(Ok(typed))` if this is a notification of the requested type
4417 /// * `Ok(Err(self))` if this is not a matching notification
4418 /// * `Err` if its method matches the requested type but parsing fails
4419 pub fn into_notification<N: JsonRpcNotification>(
4420 self,
4421 ) -> Result<Result<N, Dispatch>, crate::Error> {
4422 match self {
4423 Dispatch::Notification(msg) => {
4424 if !N::matches_method(&msg.method) {
4425 return Ok(Err(Dispatch::Notification(msg)));
4426 }
4427 match N::parse_message(&msg.method, &msg.params) {
4428 Ok(n) => Ok(Ok(n)),
4429 Err(err) => Err(err),
4430 }
4431 }
4432 Dispatch::Request(..) | Dispatch::Response(..) => Ok(Err(self)),
4433 }
4434 }
4435
4436 /// Try to parse this as a request of the given type.
4437 ///
4438 /// # Returns
4439 ///
4440 /// * `Ok(Ok(typed))` if this is a request of the requested type
4441 /// * `Ok(Err(self))` if this is not a matching request
4442 /// * `Err` if its method matches the requested type but parsing fails
4443 pub fn into_request<Req: JsonRpcRequest>(
4444 self,
4445 ) -> Result<Result<(Req, Responder<Req::Response>), Dispatch>, crate::Error> {
4446 match self {
4447 Dispatch::Request(msg, responder) => {
4448 if !Req::matches_method(&msg.method) {
4449 return Ok(Err(Dispatch::Request(msg, responder)));
4450 }
4451 match Req::parse_message(&msg.method, &msg.params) {
4452 Ok(req) => Ok(Ok((req, responder.cast()))),
4453 Err(err) => Err(err),
4454 }
4455 }
4456 Dispatch::Notification(..) | Dispatch::Response(..) => Ok(Err(self)),
4457 }
4458 }
4459}
4460
4461impl<M: JsonRpcRequest + JsonRpcNotification> Dispatch<M, M> {
4462 /// Returns the message payload for requests and notifications.
4463 ///
4464 /// Returns `None` for Response variants since they don't contain a message payload.
4465 pub fn message(&self) -> Option<&M> {
4466 match self {
4467 Dispatch::Request(msg, _) | Dispatch::Notification(msg) => Some(msg),
4468 Dispatch::Response(_, _) => None,
4469 }
4470 }
4471
4472 /// Map the request/notification message.
4473 ///
4474 /// Response variants pass through unchanged.
4475 pub(crate) fn try_map_message(
4476 self,
4477 map_message: impl FnOnce(M) -> Result<M, crate::Error>,
4478 ) -> Result<Dispatch<M, M>, crate::Error> {
4479 match self {
4480 Dispatch::Request(request, cx) => Ok(Dispatch::Request(map_message(request)?, cx)),
4481 Dispatch::Notification(notification) => {
4482 Ok(Dispatch::<M, M>::Notification(map_message(notification)?))
4483 }
4484 Dispatch::Response(result, cx) => Ok(Dispatch::Response(result, cx)),
4485 }
4486 }
4487}
4488
4489/// An incoming JSON message without any typing. Can be a request or a notification.
4490#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4491pub struct UntypedMessage {
4492 /// The JSON-RPC method name
4493 pub method: String,
4494 /// The JSON-RPC parameters as a raw JSON value
4495 pub params: serde_json::Value,
4496}
4497
4498impl UntypedMessage {
4499 /// Returns an untyped message with the given method and parameters.
4500 pub fn new(method: &str, params: impl Serialize) -> Result<Self, crate::Error> {
4501 let params = serde_json::to_value(params)?;
4502 Ok(Self {
4503 method: method.to_string(),
4504 params,
4505 })
4506 }
4507
4508 /// Returns the method name
4509 #[must_use]
4510 pub fn method(&self) -> &str {
4511 &self.method
4512 }
4513
4514 /// Returns the parameters as a JSON value
4515 #[must_use]
4516 pub fn params(&self) -> &serde_json::Value {
4517 &self.params
4518 }
4519
4520 /// Consumes this message and returns the method and params
4521 #[must_use]
4522 pub fn into_parts(self) -> (String, serde_json::Value) {
4523 (self.method, self.params)
4524 }
4525
4526 /// Convert `self` to a raw JSON-RPC message.
4527 pub(crate) fn into_raw_jsonrpc_message(
4528 self,
4529 id: Option<RequestId>,
4530 ) -> Result<RawJsonRpcMessage, crate::Error> {
4531 let Self { method, params } = self;
4532 match id {
4533 Some(id) => RawJsonRpcMessage::request(method, params, id),
4534 None => RawJsonRpcMessage::notification(method, params),
4535 }
4536 }
4537}
4538
4539impl JsonRpcMessage for UntypedMessage {
4540 fn matches_method(_method: &str) -> bool {
4541 // UntypedMessage matches any method - it's the untyped fallback
4542 true
4543 }
4544
4545 fn method(&self) -> &str {
4546 &self.method
4547 }
4548
4549 fn to_untyped_message(&self) -> Result<UntypedMessage, crate::Error> {
4550 Ok(self.clone())
4551 }
4552
4553 fn parse_message(method: &str, params: &impl Serialize) -> Result<Self, crate::Error> {
4554 UntypedMessage::new(method, params)
4555 }
4556}
4557
4558impl JsonRpcRequest for UntypedMessage {
4559 type Response = serde_json::Value;
4560}
4561
4562impl JsonRpcNotification for UntypedMessage {}
4563
4564/// Represents a pending response of type `R` from an outgoing request.
4565///
4566/// Returned by [`ConnectionTo::send_request`], this type provides explicit response-consumption
4567/// modes. The API is intentionally designed to make it difficult to accidentally wait for a
4568/// response inside the dispatch loop.
4569///
4570/// # Anti-Footgun Design
4571///
4572/// You cannot directly `.await` a `SentRequest`. Instead, you must choose how to handle
4573/// the response:
4574///
4575/// ## Option 1: Register an Ordered Callback (Safe in Handlers)
4576///
4577/// Calling [`on_receiving_result`](Self::on_receiving_result) registers the callback and returns
4578/// immediately. When ordered consumption is selected before the response is routed during its
4579/// original dispatch, the loop waits for the callback to complete before processing the next
4580/// message:
4581///
4582/// ```no_run
4583/// # use agent_client_protocol_test::*;
4584/// # async fn example(cx: agent_client_protocol::ConnectionTo<agent_client_protocol::UntypedRole>) -> Result<(), agent_client_protocol::Error> {
4585/// cx.send_request(MyRequest {})
4586/// .on_receiving_result(async |result| {
4587/// match result {
4588/// Ok(response) => {
4589/// // Handle successful response
4590/// Ok(())
4591/// }
4592/// Err(error) => {
4593/// // Handle error
4594/// Err(error)
4595/// }
4596/// }
4597/// })?;
4598/// # Ok(())
4599/// # }
4600/// ```
4601///
4602/// ## Option 2: Wait Outside the Dispatch Loop
4603///
4604/// Use [`block_task`](Self::block_task) only when the current task already runs outside the
4605/// dispatch loop—for example, in the foreground future passed to `connect_with` or in a task
4606/// created with [`ConnectionTo::spawn`]. Never await it in a handler:
4607///
4608/// ```no_run
4609/// # use agent_client_protocol_test::*;
4610/// # async fn example(cx: agent_client_protocol::ConnectionTo<agent_client_protocol::UntypedRole>) -> Result<(), agent_client_protocol::Error> {
4611/// // ✅ Safe: Spawned task runs concurrently
4612/// cx.spawn({
4613/// let cx = cx.clone();
4614/// async move {
4615/// let response = cx.send_request(MyRequest {})
4616/// .block_task()
4617/// .await?;
4618/// // Process response...
4619/// Ok(())
4620/// }
4621/// })?;
4622/// # Ok(())
4623/// # }
4624/// ```
4625///
4626/// ```no_run
4627/// # use agent_client_protocol_test::*;
4628/// # async fn example() -> Result<(), agent_client_protocol::Error> {
4629/// # let connection = mock_connection();
4630/// // ❌ NEVER do this in a handler - blocks the event loop!
4631/// connection.on_receive_request(async |req: MyRequest, responder, cx| {
4632/// let response = cx.send_request(MyRequest {})
4633/// .block_task() // This will deadlock!
4634/// .await?;
4635/// responder.respond(response)
4636/// }, agent_client_protocol::on_receive_request!())
4637/// # .connect_to(agent_client_protocol_test::MockTransport).await?;
4638/// # Ok(())
4639/// # }
4640/// ```
4641///
4642/// # Why This Design?
4643///
4644/// If you block the event loop while waiting for a response, the connection cannot process
4645/// the incoming response message, creating a deadlock. This API design prevents that footgun
4646/// by making blocking explicit and encouraging non-blocking patterns.
4647///
4648/// # Drop Behavior
4649///
4650/// By default, dropping a `SentRequest` before the SDK has received the
4651/// response sends a `$/cancel_request` notification asking the peer to cancel
4652/// the request, then discards the response when it arrives. Requests whose
4653/// eventual response should be ignored, but which should keep running on the
4654/// peer, should use [`detach`](Self::detach) instead.
4655///
4656/// # Incoming Transport EOF
4657///
4658/// If the incoming transport reaches clean EOF before the response arrives, every
4659/// consumption mode receives an error with the message `Incoming transport
4660/// closed` and data containing
4661/// `{"reason":"incoming_transport_closed","method":"..."}`. Requests made
4662/// after incoming EOF fail immediately with the same error. Use
4663/// [`is_incoming_transport_closed`] to identify it.
4664#[must_use = "dropping a SentRequest asks the peer to cancel the request and \
4665 discards the response; consume it with `block_task`, \
4666 `on_receiving_result`, `forward_response_to`, or `detach`"]
4667pub struct SentRequest<T> {
4668 id: RequestId,
4669 method: String,
4670 task_tx: TaskTx,
4671 response_rx: oneshot::Receiver<ResponsePayload>,
4672 to_result: Box<dyn FnOnce(serde_json::Value) -> Result<T, crate::Error> + Send>,
4673 cancellation: SentRequestCancellation,
4674 response_ordering: ResponseOrdering,
4675 /// Cancellation markers of other (incoming) requests whose cancellation
4676 /// should be forwarded to this request. See
4677 /// [`forward_cancellation_from`](Self::forward_cancellation_from).
4678 cancellation_sources: Vec<RequestCancellation>,
4679}
4680
4681#[derive(Clone, Debug)]
4682pub(crate) struct SentRequestCancellationDisarm {
4683 armed: Arc<AtomicBool>,
4684}
4685
4686impl SentRequestCancellationDisarm {
4687 fn new() -> Self {
4688 Self {
4689 armed: Arc::new(AtomicBool::new(true)),
4690 }
4691 }
4692
4693 fn disarm(&self) {
4694 self.armed.store(false, Ordering::Release);
4695 }
4696}
4697
4698struct SentRequestCancellation {
4699 message_tx: OutgoingMessageTx,
4700 remote_style: crate::role::RemoteStyle,
4701 request_id: RequestId,
4702 disarm: SentRequestCancellationDisarm,
4703}
4704
4705impl SentRequestCancellation {
4706 fn new(
4707 message_tx: OutgoingMessageTx,
4708 remote_style: crate::role::RemoteStyle,
4709 request_id: RequestId,
4710 ) -> Self {
4711 Self {
4712 message_tx,
4713 remote_style,
4714 request_id,
4715 disarm: SentRequestCancellationDisarm::new(),
4716 }
4717 }
4718
4719 fn disarm(&self) {
4720 self.disarm.disarm();
4721 }
4722
4723 fn disarm_handle(&self) -> SentRequestCancellationDisarm {
4724 self.disarm.clone()
4725 }
4726
4727 fn send(&self) -> Result<(), crate::Error> {
4728 if !self.disarm.armed.swap(false, Ordering::AcqRel) {
4729 return Ok(());
4730 }
4731
4732 // Build the notification lazily: most requests are never cancelled,
4733 // so this avoids serializing a notification per outgoing request.
4734 let untyped = self.remote_style.transform_outgoing_message(
4735 crate::schema::v1::CancelRequestNotification::new(self.request_id.clone()),
4736 )?;
4737
4738 send_raw_message(&self.message_tx, OutgoingMessage::Notification { untyped })
4739 }
4740}
4741
4742impl Drop for SentRequestCancellation {
4743 fn drop(&mut self) {
4744 if let Err(error) = self.send() {
4745 tracing::debug!(?error, "failed to auto-cancel dropped request");
4746 }
4747 }
4748}
4749
4750impl Debug for SentRequestCancellation {
4751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4752 f.debug_struct("SentRequestCancellation")
4753 .field("request_id", &self.request_id)
4754 .field("remote_style", &self.remote_style)
4755 .field("armed", &self.disarm.armed.load(Ordering::Acquire))
4756 .finish_non_exhaustive()
4757 }
4758}
4759
4760/// Await the response payload for an outgoing request, watching `sources` for
4761/// cancellation of the upstream requests it was registered with.
4762///
4763/// When any source reports cancellation, a `$/cancel_request` is forwarded to
4764/// the outgoing request (at most once, shared with [`SentRequest::cancel`] and
4765/// drop-time auto-cancellation), and the response is *still* awaited: the peer
4766/// always answers, with normal data or a cancellation error.
4767///
4768/// Watching is deliberately bounded by response arrival so that completed
4769/// requests do not leak waiters on markers that will never fire.
4770async fn await_response_forwarding_cancellation(
4771 response_rx: oneshot::Receiver<ResponsePayload>,
4772 cancellation: &SentRequestCancellation,
4773 sources: &[RequestCancellation],
4774) -> Result<ResponsePayload, oneshot::Canceled> {
4775 // Failing to forward the cancellation must not abort the wait: the
4776 // response (normal data or a cancellation error) may still arrive and
4777 // must still be processed.
4778 let forward_cancellation = || {
4779 if let Err(error) = cancellation.send() {
4780 tracing::debug!(
4781 ?error,
4782 "failed to forward cancellation to downstream request"
4783 );
4784 }
4785 };
4786
4787 let response = if sources.is_empty() {
4788 response_rx.await
4789 } else if sources.iter().any(RequestCancellation::is_cancelled) {
4790 forward_cancellation();
4791 response_rx.await
4792 } else {
4793 let cancelled = sources.iter().map(|source| source.state.signal_rx.clone());
4794 match future::select(future::select_all(cancelled), response_rx).await {
4795 Either::Left((_, response_rx)) => {
4796 forward_cancellation();
4797 response_rx.await
4798 }
4799 Either::Right((response, _)) => response,
4800 }
4801 };
4802
4803 cancellation.disarm();
4804 response
4805}
4806
4807impl<T: Debug> Debug for SentRequest<T> {
4808 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4809 let mut debug = f.debug_struct("SentRequest");
4810 debug
4811 .field("id", &self.id)
4812 .field("method", &self.method)
4813 .field("task_tx", &self.task_tx)
4814 .field("response_rx", &self.response_rx);
4815 debug
4816 .field("cancellation", &self.cancellation)
4817 .field("cancellation_sources", &self.cancellation_sources);
4818 debug.finish_non_exhaustive()
4819 }
4820}
4821
4822impl SentRequest<serde_json::Value> {
4823 fn new(
4824 id: RequestId,
4825 method: String,
4826 task_tx: mpsc::UnboundedSender<Task>,
4827 response_rx: oneshot::Receiver<ResponsePayload>,
4828 cancellation: SentRequestCancellation,
4829 response_ordering: ResponseOrdering,
4830 ) -> Self {
4831 Self {
4832 id,
4833 method,
4834 response_rx,
4835 task_tx,
4836 to_result: Box::new(Ok),
4837 cancellation,
4838 response_ordering,
4839 cancellation_sources: Vec::new(),
4840 }
4841 }
4842}
4843
4844impl<T> SentRequest<T> {
4845 /// Detach this request handle without waiting for its response.
4846 ///
4847 /// The response will be discarded when it arrives. This also disarms the
4848 /// drop-time automatic cancellation described in
4849 /// [Drop Behavior](Self#drop-behavior), so use it for requests whose
4850 /// eventual response should be ignored, but which should keep running on
4851 /// the peer. The peer is still expected to answer the JSON-RPC request
4852 /// eventually; use a notification instead when no response is expected at
4853 /// all.
4854 ///
4855 /// To ask the peer to stop the request, call `cancel` instead, or drop the
4856 /// handle while automatic cancellation is armed.
4857 pub fn detach(self) {
4858 self.cancellation.disarm();
4859 }
4860
4861 /// Send a `$/cancel_request` notification for this outgoing request.
4862 ///
4863 /// This uses the same peer and message wrapping that were used to send the
4864 /// original request, so it is the preferred way to cancel a [`SentRequest`]
4865 /// when the request handle is still available.
4866 ///
4867 /// At most one `$/cancel_request` is ever sent per request: the first
4868 /// `cancel` call sends it (and also prevents the drop-time automatic
4869 /// cancellation described in [Drop Behavior](Self#drop-behavior)), while
4870 /// later calls return `Ok(())` without sending anything. Likewise, once
4871 /// the SDK has routed the response to this handle, `cancel` becomes a
4872 /// no-op: there is nothing left to cancel.
4873 ///
4874 /// Errors are only reported by the call that attempts to send the
4875 /// notification.
4876 pub fn cancel(&self) -> Result<(), crate::Error> {
4877 self.cancellation.send()
4878 }
4879
4880 /// Forward cancellation of another request to this one.
4881 ///
4882 /// When the request that `source` belongs to is cancelled by its peer,
4883 /// a `$/cancel_request` for *this* request is sent to its peer, using the
4884 /// same wrapping as the original request. The response is still awaited
4885 /// and delivered as usual (normal data or a cancellation error), so this
4886 /// composes with [`block_task`](Self::block_task) and
4887 /// [`on_receiving_result`](Self::on_receiving_result).
4888 ///
4889 /// This is the building block for proxies that forward a request with
4890 /// custom logic instead of [`forward_response_to`](Self::forward_response_to)
4891 /// (which wires this up automatically from its responder). Without it,
4892 /// custom forwarding *absorbs* cancellation: the upstream marker is still
4893 /// set, but nothing is sent downstream.
4894 ///
4895 /// ```
4896 /// # use agent_client_protocol::{ConnectionTo, Error, Responder, UntypedRole};
4897 /// # use agent_client_protocol_test::{MyRequest, MyResponse};
4898 /// # async fn example(request: MyRequest, responder: Responder<MyResponse>, backend: ConnectionTo<UntypedRole>) -> Result<(), Error> {
4899 /// backend
4900 /// .send_request(request)
4901 /// .forward_cancellation_from(responder.cancellation())
4902 /// .on_receiving_result(async move |result| {
4903 /// // Custom result handling, e.g. bookkeeping or rewriting.
4904 /// responder.respond_with_result(result)
4905 /// })?;
4906 /// # Ok(())
4907 /// # }
4908 /// ```
4909 ///
4910 /// May be called multiple times; cancellation of any registered source
4911 /// triggers the forwarding (at most one `$/cancel_request` is ever sent
4912 /// per request). Sources are observed while the response is being
4913 /// awaited — that is, once the handle is consumed with
4914 /// [`block_task`](Self::block_task),
4915 /// [`on_receiving_result`](Self::on_receiving_result), or
4916 /// [`forward_response_to`](Self::forward_response_to); a source that was
4917 /// already cancelled by then is honored immediately.
4918 pub fn forward_cancellation_from(mut self, source: RequestCancellation) -> Self {
4919 self.cancellation_sources.push(source);
4920 self
4921 }
4922}
4923
4924impl<T> SentRequest<T> {
4925 /// The id of the outgoing request.
4926 #[must_use]
4927 pub fn id(&self) -> &RequestId {
4928 &self.id
4929 }
4930
4931 /// The method of the request this is in response to.
4932 #[must_use]
4933 pub fn method(&self) -> &str {
4934 &self.method
4935 }
4936
4937 /// Map a successful JSON-RPC response into an application type.
4938 ///
4939 /// The mapped type does not need to implement [`JsonRpcResponse`]. The
4940 /// mapper runs at most once and may consume captured state. JSON-RPC error
4941 /// responses bypass the mapper. The mapped type may carry a non-`'static`
4942 /// lifetime when it is consumed with [`block_task`](Self::block_task);
4943 /// callback-style consumption still requires a `'static` mapped type
4944 /// because its work is spawned onto the connection.
4945 pub fn map<U>(
4946 self,
4947 map_fn: impl FnOnce(T) -> Result<U, crate::Error> + 'static + Send,
4948 ) -> SentRequest<U>
4949 where
4950 T: 'static,
4951 {
4952 SentRequest {
4953 id: self.id,
4954 method: self.method,
4955 response_rx: self.response_rx,
4956 task_tx: self.task_tx,
4957 to_result: Box::new(move |value| map_fn((self.to_result)(value)?)),
4958 cancellation: self.cancellation,
4959 response_ordering: self.response_ordering,
4960 cancellation_sources: self.cancellation_sources,
4961 }
4962 }
4963
4964 /// Forward the response (success or error) to a request context when it arrives.
4965 ///
4966 /// This is a convenience method for proxying messages between connections. When the
4967 /// response arrives, it will be automatically sent to the provided request context,
4968 /// whether it's a successful response or an error.
4969 ///
4970 /// # Example: Proxying requests
4971 ///
4972 /// ```
4973 /// # use agent_client_protocol::UntypedRole;
4974 /// # use agent_client_protocol::{Builder, ConnectionTo};
4975 /// # use agent_client_protocol_test::*;
4976 /// # async fn example(cx: ConnectionTo<UntypedRole>) -> Result<(), agent_client_protocol::Error> {
4977 /// // Set up backend connection builder
4978 /// let backend = UntypedRole.builder()
4979 /// .on_receive_request(async |req: MyRequest, responder, cx| {
4980 /// responder.respond(MyResponse { status: "ok".into() })
4981 /// }, agent_client_protocol::on_receive_request!());
4982 ///
4983 /// // Spawn backend and get a context to send to it
4984 /// let backend_connection = cx.spawn_connection(backend, MockTransport)?;
4985 ///
4986 /// // Set up proxy that forwards requests to backend
4987 /// UntypedRole.builder()
4988 /// .on_receive_request({
4989 /// let backend_connection = backend_connection.clone();
4990 /// async move |req: MyRequest, responder, cx| {
4991 /// // Forward the request to backend and proxy the response back
4992 /// backend_connection.send_request(req)
4993 /// .forward_response_to(responder)?;
4994 /// Ok(())
4995 /// }
4996 /// }, agent_client_protocol::on_receive_request!());
4997 /// # Ok(())
4998 /// # }
4999 /// ```
5000 ///
5001 /// # Type Safety
5002 ///
5003 /// The request context's response type must match the request's response type,
5004 /// ensuring type-safe message forwarding.
5005 ///
5006 /// # When to Use
5007 ///
5008 /// Use this when:
5009 /// - You're implementing a proxy or gateway pattern
5010 /// - You want to forward responses without processing them
5011 /// - The response types match between the outgoing request and incoming request
5012 ///
5013 /// This is equivalent to calling `on_receiving_result` and manually forwarding
5014 /// the result, with two proxy-specific additions:
5015 ///
5016 /// - If the pending response cannot be delivered, the incoming request is
5017 /// answered with an internal error instead of being left unanswered.
5018 /// Known clean incoming EOF is delivered like any other response
5019 /// error; an unexpected response-channel loss is forwarded as an outer
5020 /// consumption error.
5021 /// - When the peer cancels the incoming request, the cancellation is
5022 /// forwarded to the outgoing request, and the downstream response
5023 /// (normal data or a cancellation error) is still forwarded back. This is
5024 /// equivalent to registering the responder's marker with
5025 /// `forward_cancellation_from`.
5026 #[track_caller]
5027 pub fn forward_response_to(self, responder: Responder<T>) -> Result<(), crate::Error>
5028 where
5029 T: JsonRpcResponse,
5030 {
5031 let this = self.forward_cancellation_from(responder.cancellation());
5032
5033 this.consume_with(async move |response| {
5034 // An unexpected response-channel loss (outer `Err`) is forwarded
5035 // as an error: the incoming request must not be left unanswered.
5036 responder.respond_with_result(response.unwrap_or_else(Err))
5037 })
5038 }
5039
5040 /// Spawn the response-consumption task shared by
5041 /// [`on_receiving_result`](Self::on_receiving_result) and
5042 /// [`forward_response_to`](Self::forward_response_to).
5043 ///
5044 /// The task awaits the response (forwarding cancellation from registered
5045 /// sources while waiting, converts the payload, and invokes `handle` with
5046 /// the typed result (`Ok(Result<T, _>)`). The dispatch loop's ack, if any,
5047 /// is sent after `handle` completes.
5048 ///
5049 /// Clean incoming EOF is delivered as `Ok(Err(error))`, just like
5050 /// a peer response error, so callback-style consumers still run. If the
5051 /// response channel disappears for another reason, `handle` receives an
5052 /// outer `Err` describing that unexpected loss; there is no ack then.
5053 #[track_caller]
5054 fn consume_with<F>(
5055 self,
5056 handle: impl FnOnce(Result<Result<T, crate::Error>, crate::Error>) -> F + 'static + Send,
5057 ) -> Result<(), crate::Error>
5058 where
5059 T: 'static,
5060 F: Future<Output = Result<(), crate::Error>> + 'static + Send,
5061 {
5062 self.response_ordering.mark_ordered();
5063 let task_tx = self.task_tx.clone();
5064 let method = self.method;
5065 let response_rx = self.response_rx;
5066 let to_result = self.to_result;
5067 let cancellation = self.cancellation;
5068 let cancellation_sources = self.cancellation_sources;
5069 let location = Location::caller();
5070
5071 Task::new(location, async move {
5072 let response = await_response_forwarding_cancellation(
5073 response_rx,
5074 &cancellation,
5075 &cancellation_sources,
5076 )
5077 .await;
5078
5079 match response {
5080 Ok(ResponsePayload { result, ack_tx }) => {
5081 // Convert the result using to_result for Ok values
5082 let typed_result = match result {
5083 Ok(json_value) => to_result(json_value),
5084 Err(err) => Err(err),
5085 };
5086
5087 let outcome = handle(Ok(typed_result)).await;
5088
5089 // Ack AFTER the handler completes - this is the key
5090 // difference from block_task. The dispatch loop waits for
5091 // this ack.
5092 if let Some(tx) = ack_tx {
5093 let _ = tx.send(());
5094 }
5095
5096 outcome
5097 }
5098 Err(err) => {
5099 handle(Err(crate::util::internal_error(format!(
5100 "response to `{method}` never received: {err}"
5101 ))))
5102 .await
5103 }
5104 }
5105 })
5106 .spawn(&task_tx)
5107 }
5108
5109 /// Block the current task until the response is received.
5110 ///
5111 /// **Warning:** This method blocks the current async task. It is safe only when that task
5112 /// already runs outside the dispatch loop, such as the foreground future passed to
5113 /// `connect_with` or a task created with [`ConnectionTo::spawn`]. Using it directly in a
5114 /// handler callback will deadlock the connection.
5115 ///
5116 /// # Safe Usage (outside the dispatch loop)
5117 ///
5118 /// ```no_run
5119 /// # use agent_client_protocol_test::*;
5120 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
5121 /// # let connection = mock_connection();
5122 /// connection.on_receive_request(async |req: MyRequest, responder, cx| {
5123 /// // Spawn a task to handle the request
5124 /// cx.spawn({
5125 /// let connection = cx.clone();
5126 /// async move {
5127 /// // Safe: We're in a spawned task, not blocking the event loop
5128 /// let response = connection.send_request(OtherRequest {})
5129 /// .block_task()
5130 /// .await?;
5131 ///
5132 /// // Process the response...
5133 /// Ok(())
5134 /// }
5135 /// })?;
5136 ///
5137 /// // Respond immediately
5138 /// responder.respond(MyResponse { status: "ok".into() })
5139 /// }, agent_client_protocol::on_receive_request!())
5140 /// # .connect_to(agent_client_protocol_test::MockTransport).await?;
5141 /// # Ok(())
5142 /// # }
5143 /// ```
5144 ///
5145 /// # Unsafe Usage (in handlers - will deadlock!)
5146 ///
5147 /// ```no_run
5148 /// # use agent_client_protocol_test::*;
5149 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
5150 /// # let connection = mock_connection();
5151 /// connection.on_receive_request(async |req: MyRequest, responder, cx| {
5152 /// // ❌ DEADLOCK: Handler blocks event loop, which can't process the response
5153 /// let response = cx.send_request(OtherRequest {})
5154 /// .block_task()
5155 /// .await?;
5156 ///
5157 /// responder.respond(MyResponse { status: response.value })
5158 /// }, agent_client_protocol::on_receive_request!())
5159 /// # .connect_to(agent_client_protocol_test::MockTransport).await?;
5160 /// # Ok(())
5161 /// # }
5162 /// ```
5163 ///
5164 /// # When to Use
5165 ///
5166 /// Use this method when:
5167 /// - Your current task already runs outside the dispatch loop
5168 /// - You need the response value to proceed with your logic
5169 /// - Linear control flow is more natural than callbacks
5170 ///
5171 /// For handler callbacks, use [`on_receiving_result`](Self::on_receiving_result) instead.
5172 pub async fn block_task(self) -> Result<T, crate::Error> {
5173 let response = await_response_forwarding_cancellation(
5174 self.response_rx,
5175 &self.cancellation,
5176 &self.cancellation_sources,
5177 )
5178 .await;
5179
5180 match response {
5181 Ok(ResponsePayload {
5182 result: Ok(json_value),
5183 ack_tx,
5184 }) => {
5185 // Blocking consumers ack before converting or returning the
5186 // value, so dispatch can continue while the caller processes it.
5187 if let Some(tx) = ack_tx {
5188 let _ = tx.send(());
5189 }
5190 match (self.to_result)(json_value) {
5191 Ok(value) => Ok(value),
5192 Err(err) => Err(err),
5193 }
5194 }
5195 Ok(ResponsePayload {
5196 result: Err(err),
5197 ack_tx,
5198 }) => {
5199 if let Some(tx) = ack_tx {
5200 let _ = tx.send(());
5201 }
5202 Err(err)
5203 }
5204 Err(err) => Err(crate::util::internal_error(format!(
5205 "response to `{}` never received: {}",
5206 self.method, err
5207 ))),
5208 }
5209 }
5210
5211 /// Schedule an async task to run when a successful response is received.
5212 ///
5213 /// This is a convenience wrapper around [`on_receiving_result`](Self::on_receiving_result)
5214 /// for the common pattern of forwarding errors to a request context while only processing
5215 /// successful responses.
5216 ///
5217 /// # Behavior
5218 ///
5219 /// - If the response is `Ok(value)`, your task receives the value and the request context
5220 /// - If the response is `Err(error)`, the error is automatically sent to `responder`
5221 /// and your task is not called
5222 ///
5223 /// # Example: Chaining requests
5224 ///
5225 /// ```no_run
5226 /// # use agent_client_protocol_test::*;
5227 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
5228 /// # let connection = mock_connection();
5229 /// connection.on_receive_request(async |req: ValidateRequest, responder, cx| {
5230 /// // Send initial request
5231 /// cx.send_request(ValidateRequest { data: req.data.clone() })
5232 /// .on_receiving_ok_result(responder, async |validation, responder| {
5233 /// // Only runs if validation succeeded
5234 /// if validation.is_valid {
5235 /// // Respond to original request
5236 /// responder.respond(ValidateResponse { is_valid: true, error: None })
5237 /// } else {
5238 /// responder.respond_with_error(agent_client_protocol::util::internal_error("validation failed"))
5239 /// }
5240 /// })?;
5241 ///
5242 /// Ok(())
5243 /// }, agent_client_protocol::on_receive_request!())
5244 /// # .connect_to(agent_client_protocol_test::MockTransport).await?;
5245 /// # Ok(())
5246 /// # }
5247 /// ```
5248 ///
5249 /// # Ordering
5250 ///
5251 /// Like [`on_receiving_result`](Self::on_receiving_result), response handling holds the
5252 /// dispatch loop through callback completion when ordered consumption is selected before a
5253 /// peer response is routed during its original dispatch. Pending-request failures delivered
5254 /// without an incoming response and delayed routes do not carry that barrier. The callback
5255 /// must not await later inbound traffic on the same connection. See the
5256 /// [`ordering`](crate::concepts::ordering) module for details.
5257 ///
5258 /// # When to Use
5259 ///
5260 /// Use this when:
5261 /// - You need to respond to a request based on another request's result
5262 /// - You want errors to automatically propagate to the request context
5263 /// - You only care about the success case
5264 ///
5265 /// For more control over error handling, use [`on_receiving_result`](Self::on_receiving_result).
5266 #[track_caller]
5267 pub fn on_receiving_ok_result<F>(
5268 self,
5269 responder: Responder<T>,
5270 task: impl FnOnce(T, Responder<T>) -> F + 'static + Send,
5271 ) -> Result<(), crate::Error>
5272 where
5273 F: Future<Output = Result<(), crate::Error>> + 'static + Send,
5274 T: JsonRpcResponse,
5275 {
5276 self.on_receiving_result(async move |result| match result {
5277 Ok(value) => task(value, responder).await,
5278 Err(err) => responder.respond_with_error(err),
5279 })
5280 }
5281
5282 /// Register an async callback to run when the response is received.
5283 ///
5284 /// This is the recommended way to select response handling from inside a handler because
5285 /// registration returns immediately. The response-consumption task waits concurrently for
5286 /// the response; once the response is dispatched, the ordered callback may hold the dispatch
5287 /// loop until it completes.
5288 ///
5289 /// # Example: Handle response in callback
5290 ///
5291 /// ```no_run
5292 /// # use agent_client_protocol_test::*;
5293 /// # async fn example() -> Result<(), agent_client_protocol::Error> {
5294 /// # let connection = mock_connection();
5295 /// connection.on_receive_request(async |req: MyRequest, responder, cx| {
5296 /// // Send a request and schedule a callback for the response
5297 /// cx.send_request(QueryRequest { id: 22 })
5298 /// .on_receiving_result({
5299 /// let connection = cx.clone();
5300 /// async move |result| {
5301 /// match result {
5302 /// Ok(response) => {
5303 /// println!("Got response: {:?}", response);
5304 /// // Can send more messages here
5305 /// connection.send_notification(QueryComplete {})?;
5306 /// Ok(())
5307 /// }
5308 /// Err(error) => {
5309 /// eprintln!("Request failed: {}", error);
5310 /// Err(error)
5311 /// }
5312 /// }
5313 /// }
5314 /// })?;
5315 ///
5316 /// // Handler continues immediately after registering the callback
5317 /// responder.respond(MyResponse { status: "processing".into() })
5318 /// }, agent_client_protocol::on_receive_request!())
5319 /// # .connect_to(agent_client_protocol_test::MockTransport).await?;
5320 /// # Ok(())
5321 /// # }
5322 /// ```
5323 ///
5324 /// # Ordering
5325 ///
5326 /// When ordered consumption is selected before a peer response is routed during its original
5327 /// dispatch, the callback runs in a connection-managed task and the dispatch loop waits for
5328 /// it to complete before processing the next message.
5329 ///
5330 /// The barrier does not apply when the pending request is failed without an incoming response,
5331 /// such as on EOF. If the response was already routed, or an interceptor routes a retained
5332 /// [`ResponseRouter`] after its original dispatch, the callback still runs but cannot
5333 /// retroactively block messages that were already released.
5334 ///
5335 /// While the barrier is held, the callback must not await a later response, notification, or
5336 /// other inbound traffic on the same connection: that traffic cannot be dispatched until the
5337 /// callback completes. Spawn follow-up work with [`ConnectionTo::spawn`] and return, or use
5338 /// [`block_task`](Self::block_task) from a task already outside the dispatch loop.
5339 ///
5340 /// This differs from [`block_task`](Self::block_task), which does not select ordered
5341 /// consumption: dispatch remains free while the caller processes the delivered response.
5342 ///
5343 /// See the [`ordering`](crate::concepts::ordering) module for details on ordering guarantees
5344 /// and how to avoid deadlocks.
5345 ///
5346 /// # Error Handling
5347 ///
5348 /// If the scheduled task returns `Err`, the entire server will shut down. Make sure to handle
5349 /// errors appropriately within your task.
5350 ///
5351 /// # When to Use
5352 ///
5353 /// Use this method when:
5354 /// - You need to register response handling from a handler callback
5355 /// - You want a peer response callback to complete before later messages are dispatched
5356 /// - The callback performs bounded work that does not depend on later inbound traffic
5357 ///
5358 /// When already outside the dispatch loop and you do not need ordering guarantees, consider
5359 /// [`block_task`](Self::block_task).
5360 #[track_caller]
5361 pub fn on_receiving_result<F>(
5362 self,
5363 task: impl FnOnce(Result<T, crate::Error>) -> F + 'static + Send,
5364 ) -> Result<(), crate::Error>
5365 where
5366 T: 'static,
5367 F: Future<Output = Result<(), crate::Error>> + 'static + Send,
5368 {
5369 self.consume_with(move |response| match response {
5370 // Invoke the callback before constructing its future so the
5371 // response value does not need to be `Send` across an await.
5372 Ok(result) => Either::Left(task(result)),
5373 // A response that was never delivered fails the consuming
5374 // task instead of invoking the callback.
5375 Err(err) => Either::Right(future::ready(Err(err))),
5376 })
5377 }
5378}
5379
5380// ============================================================================
5381// IntoJrConnectionTransport Implementations
5382// ============================================================================
5383
5384/// A component that communicates over line streams.
5385///
5386/// `Lines` implements the [`ConnectTo`] trait for any pair of line-based streams
5387/// (a `Stream<Item = io::Result<String>>` for incoming and a `Sink<String>` for outgoing),
5388/// handling serialization of JSON-RPC messages to/from newline-delimited JSON.
5389/// An incoming line may contain one JSON-RPC message or a non-empty batch array. Batch
5390/// entries are dispatched individually in source order, and responses to the batch are
5391/// collected into one response-array line. SDK-initiated requests and notifications remain
5392/// individual messages.
5393///
5394/// This is a lower-level primitive than [`ByteStreams`] that enables interception and
5395/// transformation of individual lines before they are parsed or after they are serialized.
5396/// This is particularly useful for debugging, logging, or implementing custom line-based
5397/// protocols.
5398///
5399/// # Use Cases
5400///
5401/// - **Line-by-line logging**: Intercept and log each line before parsing
5402/// - **Custom protocols**: Transform lines before/after JSON-RPC processing
5403/// - **Debugging**: Inspect raw message strings
5404/// - **Line filtering**: Skip or modify specific messages
5405///
5406/// Most users should use [`ByteStreams`] instead, which provides a simpler interface
5407/// for byte-based I/O.
5408///
5409/// [`ConnectTo`]: crate::ConnectTo
5410#[derive(Debug)]
5411pub struct Lines<OutgoingSink, IncomingStream> {
5412 outgoing: OutgoingSink,
5413 incoming: IncomingStream,
5414}
5415
5416impl<OutgoingSink, IncomingStream> Lines<OutgoingSink, IncomingStream>
5417where
5418 OutgoingSink: futures::Sink<String, Error = std::io::Error> + Send + 'static,
5419 IncomingStream: futures::Stream<Item = std::io::Result<String>> + Send + 'static,
5420{
5421 /// Create a new line stream transport.
5422 pub fn new(outgoing: OutgoingSink, incoming: IncomingStream) -> Self {
5423 Self { outgoing, incoming }
5424 }
5425
5426 fn into_channel_transport(self) -> (Channel, BoxFuture<'static, Result<(), crate::Error>>) {
5427 let Self { outgoing, incoming } = self;
5428 let (channel_for_caller, channel_for_lines) = Channel::duplex();
5429
5430 let server_future = Box::pin(async move {
5431 let Channel { rx, tx } = channel_for_lines;
5432 let outgoing_future = transport_actor::transport_outgoing_lines_actor(rx, outgoing);
5433 let incoming_future = transport_actor::transport_incoming_lines_actor(incoming, tx);
5434 futures::try_join!(outgoing_future, incoming_future)?;
5435 Ok(())
5436 });
5437
5438 (channel_for_caller, server_future)
5439 }
5440}
5441
5442impl<OutgoingSink, IncomingStream, R: Role> ConnectTo<R> for Lines<OutgoingSink, IncomingStream>
5443where
5444 OutgoingSink: futures::Sink<String, Error = std::io::Error> + Send + 'static,
5445 IncomingStream: futures::Stream<Item = std::io::Result<String>> + Send + 'static,
5446{
5447 async fn connect_to(self, client: impl ConnectTo<R::Counterpart>) -> Result<(), crate::Error> {
5448 let Self { outgoing, incoming } = self;
5449 let (Channel { rx, tx }, client_channel) = Channel::duplex();
5450 let close_client_output = client_channel.tx.clone();
5451 let client_future = Box::pin(async move {
5452 let result = client.connect_to(client_channel).await;
5453 close_client_output.close_channel();
5454 result
5455 });
5456
5457 // Once the client completes successfully, its incoming channel is
5458 // gone. Keep consuming successful messages from the physical read
5459 // half without forwarding them so a full-duplex peer cannot block our
5460 // outgoing sink while it is being drained. Transport errors must still
5461 // fail the connection.
5462 let discard_incoming = Arc::new(AtomicBool::new(false));
5463 let incoming = incoming.filter_map({
5464 let discard_incoming = discard_incoming.clone();
5465 move |item| {
5466 let discard_incoming = discard_incoming.load(Ordering::Acquire);
5467 future::ready((!discard_incoming || item.is_err()).then_some(item))
5468 }
5469 });
5470
5471 let outgoing = transport_actor::transport_outgoing_lines_actor(rx, outgoing)
5472 .boxed()
5473 .shared();
5474 let serve_self = Box::pin({
5475 let outgoing = outgoing.clone();
5476 async move {
5477 futures::try_join!(
5478 outgoing,
5479 transport_actor::transport_incoming_lines_actor(incoming, tx),
5480 )?;
5481 Ok(())
5482 }
5483 });
5484
5485 match futures::future::select(client_future, serve_self).await {
5486 Either::Left((result, serve_self)) => {
5487 result?;
5488 discard_incoming.store(true, Ordering::Release);
5489
5490 // Drive the read half while waiting for the write half, but do
5491 // not require the peer's independent incoming stream to reach
5492 // EOF. If incoming processing finishes successfully first,
5493 // the shared outgoing future still owns and drains the sink.
5494 // A successful `serve_self` result includes its shared
5495 // outgoing clone, while any error must remain authoritative
5496 // instead of being hidden behind the other handle. Poll it
5497 // first so a ready read error wins over clean outgoing
5498 // completion.
5499 match future::select(serve_self, outgoing).await {
5500 Either::Left((result, _)) | Either::Right((result, _)) => result,
5501 }
5502 }
5503 Either::Right((result, _)) => result,
5504 }
5505 }
5506
5507 fn into_channel_and_future(self) -> (Channel, BoxFuture<'static, Result<(), crate::Error>>) {
5508 self.into_channel_transport()
5509 }
5510}
5511
5512/// A component that communicates over byte streams (stdin/stdout, sockets, pipes, etc.).
5513///
5514/// `ByteStreams` implements the [`ConnectTo`] trait for any pair of `AsyncRead` and `AsyncWrite`
5515/// streams, handling serialization of JSON-RPC messages to/from newline-delimited JSON.
5516/// This is the standard way to communicate with external processes or network connections.
5517///
5518/// # Use Cases
5519///
5520/// - **Stdio communication**: Connect to agents or proxies via stdin/stdout
5521/// - **Network sockets**: TCP, Unix domain sockets, or other stream-based protocols
5522/// - **Named pipes**: Cross-process communication on the same machine
5523/// - **File I/O**: Reading from and writing to file descriptors
5524///
5525/// # Example
5526///
5527/// Connecting to an agent via stdio:
5528///
5529/// ```no_run
5530/// use agent_client_protocol::UntypedRole;
5531/// # use agent_client_protocol::{ByteStreams};
5532/// use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
5533///
5534/// # async fn example() -> Result<(), agent_client_protocol::Error> {
5535/// let component = ByteStreams::new(
5536/// tokio::io::stdout().compat_write(),
5537/// tokio::io::stdin().compat(),
5538/// );
5539///
5540/// // Use as a component in a connection
5541/// agent_client_protocol::UntypedRole.builder()
5542/// .name("my-client")
5543/// .connect_to(component)
5544/// .await?;
5545/// # Ok(())
5546/// # }
5547/// ```
5548///
5549/// [`ConnectTo`]: crate::ConnectTo
5550#[derive(Debug)]
5551pub struct ByteStreams<OB, IB> {
5552 outgoing: OB,
5553 incoming: IB,
5554}
5555
5556impl<OB, IB> ByteStreams<OB, IB>
5557where
5558 OB: AsyncWrite + Send + 'static,
5559 IB: AsyncRead + Send + 'static,
5560{
5561 /// Create a new byte stream transport.
5562 pub fn new(outgoing: OB, incoming: IB) -> Self {
5563 Self { outgoing, incoming }
5564 }
5565
5566 fn into_lines(
5567 self,
5568 ) -> Lines<
5569 impl futures::Sink<String, Error = std::io::Error> + Send + 'static,
5570 impl futures::Stream<Item = std::io::Result<String>> + Send + 'static,
5571 > {
5572 use futures::AsyncBufReadExt;
5573 use futures::io::BufReader;
5574 let Self { outgoing, incoming } = self;
5575
5576 let incoming_lines = Box::pin(BufReader::new(incoming).lines());
5577 let outgoing_lines =
5578 futures::sink::unfold(Box::pin(outgoing), async move |mut writer, line: String| {
5579 write_line(&mut writer, line).await?;
5580 Ok::<_, std::io::Error>(writer)
5581 });
5582
5583 Lines::new(outgoing_lines, incoming_lines)
5584 }
5585}
5586
5587pub(crate) async fn write_line<W>(writer: &mut W, line: String) -> std::io::Result<()>
5588where
5589 W: AsyncWrite + Unpin + ?Sized,
5590{
5591 use futures::AsyncWriteExt as _;
5592
5593 let mut bytes = line.into_bytes();
5594 bytes.push(b'\n');
5595 writer.write_all(&bytes).await?;
5596 writer.flush().await
5597}
5598
5599impl<OB, IB, R: Role> ConnectTo<R> for ByteStreams<OB, IB>
5600where
5601 OB: AsyncWrite + Send + 'static,
5602 IB: AsyncRead + Send + 'static,
5603{
5604 async fn connect_to(self, client: impl ConnectTo<R::Counterpart>) -> Result<(), crate::Error> {
5605 ConnectTo::<R>::connect_to(self.into_lines(), client).await
5606 }
5607
5608 fn into_channel_and_future(self) -> (Channel, BoxFuture<'static, Result<(), crate::Error>>) {
5609 ConnectTo::<R>::into_channel_and_future(self.into_lines())
5610 }
5611}
5612
5613/// A channel endpoint representing one side of a bidirectional JSON-RPC transport.
5614///
5615/// A channel carries complete TransportFrame values, preserving batch boundaries
5616/// across in-process components and transport adapters. Malformed wire input is an
5617/// explicit frame; failures while driving a physical transport are returned by that
5618/// transport's future.
5619///
5620/// # Example
5621///
5622/// ```no_run
5623/// # use agent_client_protocol::UntypedRole;
5624/// # use agent_client_protocol::Channel;
5625/// # async fn example() -> Result<(), agent_client_protocol::Error> {
5626/// let (channel_a, _channel_b) = Channel::duplex();
5627///
5628/// UntypedRole.builder()
5629/// .name("connection-a")
5630/// .connect_to(channel_a)
5631/// .await?;
5632/// # Ok(())
5633/// # }
5634/// ```
5635#[derive(Debug)]
5636pub struct Channel {
5637 /// Receives frames from the counterpart.
5638 pub rx: mpsc::UnboundedReceiver<TransportFrame>,
5639 /// Sends frames to the counterpart.
5640 pub tx: mpsc::UnboundedSender<TransportFrame>,
5641}
5642
5643impl Channel {
5644 /// Create a pair of connected channel endpoints.
5645 ///
5646 /// Frames sent through either endpoint are received by the other endpoint.
5647 #[must_use]
5648 pub fn duplex() -> (Self, Self) {
5649 let (a_tx, b_rx) = mpsc::unbounded();
5650 let (b_tx, a_rx) = mpsc::unbounded();
5651
5652 (Self { rx: a_rx, tx: a_tx }, Self { rx: b_rx, tx: b_tx })
5653 }
5654
5655 /// Copy frames from `rx` to `tx` until the input closes.
5656 ///
5657 /// # Errors
5658 ///
5659 /// Returns an error if the receiving endpoint closes before the input.
5660 pub(crate) async fn copy(mut self) -> Result<(), crate::Error> {
5661 while let Some(frame) = self.rx.next().await {
5662 self.tx
5663 .unbounded_send(frame)
5664 .map_err(crate::util::internal_error)?;
5665 }
5666 Ok(())
5667 }
5668
5669 /// Bridge two endpoints while inspecting every valid message.
5670 ///
5671 /// Observers are invoked in source order, including for each valid member of
5672 /// a batch. The original frame is forwarded unchanged after inspection.
5673 ///
5674 /// # Errors
5675 ///
5676 /// Returns an observer error or an error if a destination closes before its
5677 /// source.
5678 pub async fn bridge_with_inspection(
5679 left: Self,
5680 right: Self,
5681 mut left_to_right: impl FnMut(&RawJsonRpcMessage) -> Result<(), crate::Error> + Send,
5682 mut right_to_left: impl FnMut(&RawJsonRpcMessage) -> Result<(), crate::Error> + Send,
5683 ) -> Result<(), crate::Error> {
5684 let Self {
5685 rx: mut left_rx,
5686 tx: left_tx,
5687 } = left;
5688 let Self {
5689 rx: mut right_rx,
5690 tx: right_tx,
5691 } = right;
5692
5693 let left_to_right = async move {
5694 while let Some(frame) = left_rx.next().await {
5695 frame.inspect_messages(&mut left_to_right)?;
5696 right_tx
5697 .unbounded_send(frame)
5698 .map_err(crate::util::internal_error)?;
5699 }
5700 Ok::<(), crate::Error>(())
5701 };
5702 let right_to_left = async move {
5703 while let Some(frame) = right_rx.next().await {
5704 frame.inspect_messages(&mut right_to_left)?;
5705 left_tx
5706 .unbounded_send(frame)
5707 .map_err(crate::util::internal_error)?;
5708 }
5709 Ok::<(), crate::Error>(())
5710 };
5711
5712 futures::try_join!(left_to_right, right_to_left)?;
5713 Ok(())
5714 }
5715}
5716
5717impl<R: Role> ConnectTo<R> for Channel {
5718 async fn connect_to(self, client: impl ConnectTo<R::Counterpart>) -> Result<(), crate::Error> {
5719 let (client_channel, client_future) = client.into_channel_and_future();
5720
5721 let ((), (), ()) = futures::try_join!(
5722 Channel {
5723 rx: client_channel.rx,
5724 tx: self.tx,
5725 }
5726 .copy(),
5727 Channel {
5728 rx: self.rx,
5729 tx: client_channel.tx,
5730 }
5731 .copy(),
5732 client_future,
5733 )?;
5734 Ok(())
5735 }
5736
5737 fn into_channel_and_future(self) -> (Channel, BoxFuture<'static, Result<(), crate::Error>>) {
5738 (self, Box::pin(future::ready(Ok(()))))
5739 }
5740}
5741
5742#[cfg(test)]
5743mod tests {
5744 use super::*;
5745
5746 fn connection_with_dynamic_handler_receiver() -> (
5747 ConnectionTo<crate::role::UntypedRole>,
5748 mpsc::UnboundedReceiver<DynamicHandlerMessage<crate::role::UntypedRole>>,
5749 ) {
5750 let (message_tx, _message_rx) = mpsc::unbounded();
5751 let (task_tx, _task_rx) = mpsc::unbounded();
5752 let (dynamic_handler_tx, dynamic_handler_rx) = mpsc::unbounded();
5753 let transport_completion: SharedTransportCompletion =
5754 future::ready(Ok::<(), crate::Error>(())).boxed().shared();
5755 let pending_replies = PendingReplies::default();
5756
5757 (
5758 ConnectionTo::new(
5759 crate::role::UntypedRole,
5760 message_tx,
5761 task_tx,
5762 dynamic_handler_tx,
5763 transport_completion,
5764 pending_replies.registrar(),
5765 ),
5766 dynamic_handler_rx,
5767 )
5768 }
5769
5770 fn next_dynamic_handler_message(
5771 receiver: &mut mpsc::UnboundedReceiver<DynamicHandlerMessage<crate::role::UntypedRole>>,
5772 ) -> Option<DynamicHandlerMessage<crate::role::UntypedRole>> {
5773 futures::FutureExt::now_or_never(futures::StreamExt::next(receiver))
5774 .expect("dynamic-handler receiver should be ready")
5775 }
5776
5777 #[test]
5778 fn dropping_dynamic_handler_guard_unregisters_handler() {
5779 let (connection, mut receiver) = connection_with_dynamic_handler_receiver();
5780 let guard = connection.add_dynamic_handler(NullHandler).unwrap();
5781
5782 let added_uuid = match next_dynamic_handler_message(&mut receiver) {
5783 Some(DynamicHandlerMessage::AddDynamicHandler(uuid, _)) => uuid,
5784 other => panic!("expected handler registration, got {other:?}"),
5785 };
5786
5787 drop(guard);
5788
5789 match next_dynamic_handler_message(&mut receiver) {
5790 Some(DynamicHandlerMessage::RemoveDynamicHandler(uuid)) => {
5791 assert_eq!(uuid, added_uuid);
5792 }
5793 other => panic!("expected handler removal, got {other:?}"),
5794 }
5795 }
5796
5797 #[test]
5798 fn detaching_dynamic_handler_guard_does_not_leak_connection() {
5799 let (connection, mut receiver) = connection_with_dynamic_handler_receiver();
5800 let guard = connection.add_dynamic_handler(NullHandler).unwrap();
5801
5802 assert!(matches!(
5803 next_dynamic_handler_message(&mut receiver),
5804 Some(DynamicHandlerMessage::AddDynamicHandler(_, _))
5805 ));
5806
5807 drop(connection);
5808 guard.detach();
5809
5810 assert!(
5811 next_dynamic_handler_message(&mut receiver).is_none(),
5812 "detach should retain the handler without retaining a connection sender"
5813 );
5814 }
5815
5816 #[tokio::test]
5817 async fn write_line_flushes_buffered_writers() {
5818 let mut writer =
5819 futures::io::BufWriter::with_capacity(4096, futures::io::Cursor::new(Vec::new()));
5820
5821 write_line(&mut writer, "message".into()).await.unwrap();
5822
5823 assert_eq!(writer.into_inner().into_inner(), b"message\n");
5824 }
5825
5826 #[test]
5827 fn peel_successor_envelopes_returns_plain_messages_unchanged() {
5828 let params = serde_json::json!({ "key": "value" });
5829 let (method, peeled) = peel_successor_envelopes("session/update", ¶ms);
5830 assert_eq!(method, "session/update");
5831 assert_eq!(peeled, ¶ms);
5832 }
5833
5834 #[test]
5835 fn peel_successor_envelopes_unwraps_nested_envelopes() {
5836 let params = serde_json::json!({
5837 "method": "_proxy/successor",
5838 "params": {
5839 "method": "$/cancel_request",
5840 "params": { "requestId": "req-1" }
5841 }
5842 });
5843 let (method, peeled) = peel_successor_envelopes("_proxy/successor", ¶ms);
5844 assert_eq!(method, "$/cancel_request");
5845 assert_eq!(peeled, &serde_json::json!({ "requestId": "req-1" }));
5846 }
5847
5848 #[test]
5849 fn peel_successor_envelopes_leaves_malformed_envelopes_intact() {
5850 // No string `method` field: the envelope cannot be peeled, so the
5851 // message is returned as-is for the handler chain to deal with.
5852 let params = serde_json::json!({ "unexpected": true });
5853 let (method, peeled) = peel_successor_envelopes("_proxy/successor", ¶ms);
5854 assert_eq!(method, "_proxy/successor");
5855 assert_eq!(peeled, ¶ms);
5856 }
5857
5858 mod cancel_request {
5859 use super::super::*;
5860
5861 fn notification(method: &str, params: serde_json::Value) -> UntypedMessage {
5862 UntypedMessage::new(method, params).expect("well-formed JSON")
5863 }
5864
5865 #[test]
5866 fn cancellation_request_id_is_extracted_from_wrapped_notifications() {
5867 let message = notification(
5868 "_proxy/successor",
5869 serde_json::json!({
5870 "method": "$/cancel_request",
5871 "params": { "requestId": "req-1" }
5872 }),
5873 );
5874 let request_id = cancellation_request_id_from_message(&message)
5875 .expect("wrapped cancel should parse");
5876 assert_eq!(request_id, Some(RequestId::Str("req-1".into())));
5877 }
5878
5879 #[test]
5880 fn malformed_successor_envelope_is_not_treated_as_cancellation() {
5881 // The envelope cannot be peeled; the message must flow on to the
5882 // handler chain instead of erroring the dispatch.
5883 let message = notification("_proxy/successor", serde_json::json!({ "bogus": true }));
5884 let request_id = cancellation_request_id_from_message(&message)
5885 .expect("malformed envelope should be left to the handler chain");
5886 assert_eq!(request_id, None);
5887 }
5888
5889 #[test]
5890 fn cancel_request_notifications_are_detected_even_when_wrapped() {
5891 let plain = notification("$/cancel_request", serde_json::json!({ "requestId": 1 }));
5892 assert!(is_cancel_request_notification(&plain));
5893
5894 let wrapped = notification(
5895 "_proxy/successor",
5896 serde_json::json!({
5897 "method": "$/cancel_request",
5898 "params": { "requestId": 1 }
5899 }),
5900 );
5901 assert!(is_cancel_request_notification(&wrapped));
5902
5903 let other_wrapped = notification(
5904 "_proxy/successor",
5905 serde_json::json!({
5906 "method": "session/update",
5907 "params": {}
5908 }),
5909 );
5910 assert!(!is_cancel_request_notification(&other_wrapped));
5911
5912 let malformed_envelope =
5913 notification("_proxy/successor", serde_json::json!({ "bogus": true }));
5914 assert!(!is_cancel_request_notification(&malformed_envelope));
5915 }
5916
5917 #[test]
5918 fn malformed_cancel_request_params_error() {
5919 let message = notification(
5920 "$/cancel_request",
5921 serde_json::json!({ "requestId": { "not": "an id" } }),
5922 );
5923 cancellation_request_id_from_message(&message)
5924 .expect_err("malformed cancel params should error");
5925 }
5926
5927 #[test]
5928 fn registry_marks_and_removes_requests() {
5929 let registry = RequestCancellationRegistry::new();
5930 let id = RequestId::Str("req-1".into());
5931
5932 let responder_cancellation = registry.register(&id);
5933 let marker = responder_cancellation.cancellation();
5934 assert!(!marker.is_cancelled());
5935
5936 assert!(registry.cancel(&id));
5937 assert!(marker.is_cancelled());
5938 assert!(responder_cancellation.cancellation().is_cancelled());
5939
5940 drop(responder_cancellation);
5941 assert!(!registry.cancel(&id), "slot should be removed on drop");
5942 }
5943
5944 #[test]
5945 fn reused_request_id_does_not_cross_wire_cancellation_state() {
5946 let registry = RequestCancellationRegistry::new();
5947 let id = RequestId::Str("dup".into());
5948
5949 // A protocol-violating peer reuses an in-flight request ID.
5950 let first = registry.register(&id);
5951 let first_marker = first.cancellation();
5952 let second = registry.register(&id);
5953 let second_marker = second.cancellation();
5954
5955 // A cancellation targets whichever request currently owns the ID.
5956 assert!(registry.cancel(&id));
5957 assert!(second_marker.is_cancelled());
5958 assert!(
5959 !first_marker.is_cancelled(),
5960 "the stale request must not observe the newer request's cancellation"
5961 );
5962
5963 // The stale responder must hand out detached markers, not the
5964 // newer request's marker.
5965 assert!(!first.cancellation().is_cancelled());
5966
5967 // Dropping the stale responder must not remove the newer
5968 // request's slot.
5969 drop(first);
5970 assert!(registry.cancel(&id), "newer slot should still be present");
5971
5972 drop(second);
5973 assert!(!registry.cancel(&id), "slot should be removed on drop");
5974 }
5975 }
5976}