agent_client_protocol/jsonrpc/v2_connection.rs
1//! The protocol-v2 connection type, `agent_client_protocol::v2::ConnectionTo`.
2
3use super::ConnectionTo as RawConnectionTo;
4use super::{
5 Builder, ConnectTo, ConnectionTermination, Dispatch, DynamicHandlerGuard,
6 HandleConnectionClose, HandleDispatchFrom, JsonRpcNotification, JsonRpcRequest,
7 PreparedRequest, Protocol, SentRequest, V2, protocol_compat, run::RunWithConnectionTo,
8};
9use crate::Agent;
10use crate::role::{HasPeer, Role};
11use crate::v2::InitializeOutcome;
12
13/// A protocol-v2 connection context.
14///
15/// Values of this type are supplied to callbacks registered on a
16/// [`v2::Builder`](crate::v2::Builder). It exposes the general connection operations that are valid
17/// for protocol v2 while keeping version-specific high-level helpers for other
18/// protocol versions out of the typed context. The generic JSON-RPC send
19/// methods remain intentionally schema-agnostic.
20///
21/// This is a thin, cheaply cloneable handle to the underlying JSON-RPC
22/// connection. It intentionally does not implement [`Deref`](std::ops::Deref)
23/// to the version-neutral [`ConnectionTo`](crate::ConnectionTo); convert
24/// explicitly with [`into_raw`](Self::into_raw) and
25/// [`ConnectionTo::into_protocol`](crate::ConnectionTo::into_protocol).
26#[derive(Clone, Debug)]
27pub struct ConnectionTo<Counterpart: Role> {
28 pub(crate) inner: RawConnectionTo<Counterpart>,
29}
30
31impl ConnectionTo<Agent> {
32 /// Send the client's `initialize` request and report how the agent
33 /// negotiated.
34 ///
35 /// Unlike [`send_request`](Self::send_request), a v1-only agent's valid v1
36 /// response resolves successfully as [`InitializeOutcome::LowerVersion`]
37 /// with the agent's typed response, so the caller can decide whether to
38 /// fall back to a v1 client implementation. The connection stays
39 /// uninitialized in that case; the SDK never converts it to v1.
40 ///
41 /// The request still resolves with an error for errors returned by the
42 /// agent (including the `-32022` version mismatch), malformed responses,
43 /// and transport failures.
44 ///
45 /// After an initialization completes, `initialize` may be sent again
46 /// before any other request; the agent treats the latest one as
47 /// authoritative. Once another request has been sent, a repeat resolves
48 /// with an `-32600` error without being sent.
49 ///
50 /// ```no_run
51 /// # use agent_client_protocol::{schema::ProtocolVersion, v2::schema};
52 /// # use agent_client_protocol::{Client, ConnectTo};
53 /// use agent_client_protocol::SelectedInitializeResponse;
54 /// use agent_client_protocol::v2::InitializeOutcome;
55 /// # async fn example(agent: impl ConnectTo<Client>) -> agent_client_protocol::Result<()> {
56 /// let outcome = Client
57 /// .v2()
58 /// .connect_with(agent, async |cx| {
59 /// let request = schema::InitializeRequest::new(
60 /// ProtocolVersion::V2,
61 /// schema::Implementation::new("example", "0.1.0"),
62 /// );
63 /// match cx.send_initialize(request).block_task().await? {
64 /// InitializeOutcome::Initialized(_response) => {
65 /// // Continue with v2 traffic.
66 /// Ok(None)
67 /// }
68 /// InitializeOutcome::LowerVersion(mismatch) => Ok(Some(mismatch)),
69 /// _ => Err(agent_client_protocol::Error::internal_error().data("unexpected outcome")),
70 /// }
71 /// })
72 /// .await?;
73 /// if let Some(mismatch) = outcome
74 /// && let SelectedInitializeResponse::V1(_v1_response) = mismatch.response
75 /// {
76 /// // Reconnect with a v1 client implementation.
77 /// }
78 /// # Ok(())
79 /// # }
80 /// ```
81 pub fn send_initialize(
82 &self,
83 request: crate::v2::schema::InitializeRequest,
84 ) -> SentRequest<InitializeOutcome, V2> {
85 let mut prepared = self.inner.prepare_request_to_with_options::<V2, _, _>(
86 self.inner.counterpart(),
87 protocol_compat::InitializeNegotiation(request),
88 None,
89 None,
90 );
91 prepared
92 .publication
93 .pending_reply
94 .accepts_lower_initialize_version = true;
95 prepared.into_sent_request(false)
96 }
97}
98
99impl<Counterpart: Role> From<ConnectionTo<Counterpart>> for RawConnectionTo<Counterpart> {
100 /// The same as [`ConnectionTo::into_raw`]. Generic code over
101 /// [`Protocol`] uses it to turn a `P::Connection<Counterpart>` back into
102 /// the version-neutral connection.
103 fn from(connection: ConnectionTo<Counterpart>) -> Self {
104 connection.into_raw()
105 }
106}
107
108impl<Counterpart: Role> ConnectionTo<Counterpart> {
109 /// Access the underlying version-neutral connection inside the SDK.
110 pub(crate) fn raw_connection(&self) -> &RawConnectionTo<Counterpart> {
111 &self.inner
112 }
113
114 /// The underlying version-neutral connection.
115 ///
116 /// Requests sent through it return [`Raw`](crate::Raw) handles. Convert
117 /// back with [`ConnectionTo::into_protocol`](crate::ConnectionTo::into_protocol).
118 #[must_use]
119 pub fn into_raw(self) -> RawConnectionTo<Counterpart> {
120 self.inner
121 }
122
123 /// Return the counterpart role this connection is talking to.
124 pub fn counterpart(&self) -> Counterpart {
125 self.inner.counterpart()
126 }
127
128 /// Wait until the incoming transport reaches clean EOF.
129 pub async fn incoming_closed(&self) {
130 self.inner.incoming_closed().await;
131 }
132
133 /// Return whether clean incoming-EOF processing has completed.
134 #[must_use]
135 pub fn is_incoming_closed(&self) -> bool {
136 self.inner.is_incoming_closed()
137 }
138
139 /// Wait until the connection driver has terminated, for any reason.
140 ///
141 /// See [`ConnectionTo::terminated`](crate::ConnectionTo::terminated).
142 pub async fn terminated(&self) -> ConnectionTermination {
143 self.inner.terminated().await
144 }
145
146 /// Return the termination reason if the connection driver has terminated.
147 #[must_use]
148 pub fn termination(&self) -> Option<ConnectionTermination> {
149 self.inner.termination()
150 }
151
152 /// Spawn a task that runs for as long as the JSON-RPC connection is served.
153 #[track_caller]
154 pub fn spawn(
155 &self,
156 task: impl IntoFuture<Output = Result<(), crate::Error>, IntoFuture: Send + 'static>,
157 ) -> Result<(), crate::Error> {
158 self.inner.spawn(task)
159 }
160
161 /// Spawn a JSON-RPC connection in the background.
162 ///
163 /// The returned connection context is selected by `builder`; spawning a
164 /// [`v2::Builder`](crate::v2::Builder) therefore returns another
165 /// [`v2::ConnectionTo`](ConnectionTo).
166 ///
167 /// ```no_run
168 /// # use agent_client_protocol::{
169 /// # Agent, Client, ConnectTo, Error,
170 /// # };
171 /// # use agent_client_protocol::v2::ConnectionTo;
172 /// # fn example(
173 /// # connection: ConnectionTo<Agent>,
174 /// # transport: impl ConnectTo<Client> + 'static,
175 /// # ) -> Result<(), Error> {
176 /// let child: ConnectionTo<Agent> =
177 /// connection.spawn_connection(Client.v2(), transport)?;
178 /// # drop(child);
179 /// # Ok(())
180 /// # }
181 /// ```
182 #[track_caller]
183 pub fn spawn_connection<R: Role, P: Protocol>(
184 &self,
185 builder: Builder<
186 R,
187 impl HandleDispatchFrom<R::Counterpart> + 'static,
188 impl RunWithConnectionTo<R::Counterpart> + 'static,
189 impl HandleConnectionClose<R::Counterpart> + 'static,
190 P,
191 >,
192 transport: impl ConnectTo<R> + 'static,
193 ) -> Result<P::Connection<R::Counterpart>, crate::Error> {
194 self.inner.spawn_connection(builder, transport)
195 }
196
197 /// Send a request or notification and forward its response appropriately.
198 pub fn send_proxied_message<
199 Req: JsonRpcRequest<Response: Send>,
200 Notif: JsonRpcNotification,
201 Q: Protocol,
202 >(
203 &self,
204 message: Dispatch<Req, Notif, Q>,
205 ) -> Result<(), crate::Error>
206 where
207 Counterpart: HasPeer<Counterpart>,
208 {
209 self.inner.send_proxied_message(message)
210 }
211
212 /// Send a request or notification to a specific peer and forward its
213 /// response appropriately.
214 pub fn send_proxied_message_to<
215 Peer: Role,
216 Req: JsonRpcRequest<Response: Send>,
217 Notif: JsonRpcNotification,
218 Q: Protocol,
219 >(
220 &self,
221 peer: Peer,
222 message: Dispatch<Req, Notif, Q>,
223 ) -> Result<(), crate::Error>
224 where
225 Counterpart: HasPeer<Peer>,
226 {
227 self.inner.send_proxied_message_to(peer, message)
228 }
229
230 /// Send an outgoing request to the default counterpart peer.
231 pub fn send_request<Req: JsonRpcRequest>(&self, request: Req) -> SentRequest<Req::Response, V2>
232 where
233 Counterpart: HasPeer<Counterpart>,
234 {
235 self.send_request_to(self.inner.counterpart(), request)
236 }
237
238 /// Send an outgoing request to a specific peer.
239 pub fn send_request_to<Peer: Role, Req: JsonRpcRequest>(
240 &self,
241 peer: Peer,
242 request: Req,
243 ) -> SentRequest<Req::Response, V2>
244 where
245 Counterpart: HasPeer<Peer>,
246 {
247 self.inner
248 .send_request_to_with_options(peer, request, false, None, None)
249 }
250
251 /// Prepare a request without sending it until response handling is selected.
252 ///
253 /// See [`ConnectionTo::prepare_request`](crate::ConnectionTo::prepare_request) for publication and ordering semantics.
254 pub fn prepare_request<Req: JsonRpcRequest>(
255 &self,
256 request: Req,
257 ) -> PreparedRequest<Req::Response, V2>
258 where
259 Counterpart: HasPeer<Counterpart>,
260 {
261 self.prepare_request_to(self.inner.counterpart(), request)
262 }
263
264 /// Prepare a request to a specific peer without sending it.
265 ///
266 /// See [`ConnectionTo::prepare_request_to`](crate::ConnectionTo::prepare_request_to) for publication and ordering semantics.
267 pub fn prepare_request_to<Peer: Role, Req: JsonRpcRequest>(
268 &self,
269 peer: Peer,
270 request: Req,
271 ) -> PreparedRequest<Req::Response, V2>
272 where
273 Counterpart: HasPeer<Peer>,
274 {
275 self.inner
276 .prepare_request_to_with_options(peer, request, None, None)
277 }
278
279 /// Send an outgoing notification to the default counterpart peer.
280 pub fn send_notification<N: JsonRpcNotification>(
281 &self,
282 notification: N,
283 ) -> Result<(), crate::Error>
284 where
285 Counterpart: HasPeer<Counterpart>,
286 {
287 self.inner.send_notification(notification)
288 }
289
290 /// Send an outgoing notification to a specific peer.
291 pub fn send_notification_to<Peer: Role, N: JsonRpcNotification>(
292 &self,
293 peer: Peer,
294 notification: N,
295 ) -> Result<(), crate::Error>
296 where
297 Counterpart: HasPeer<Peer>,
298 {
299 self.inner.send_notification_to(peer, notification)
300 }
301
302 /// Send a `$/cancel_request` notification to the default counterpart peer.
303 ///
304 /// Prefer [`SentRequest::cancel`](crate::SentRequest::cancel) when you
305 /// have the request handle. `request_id` is a v2 request ID, such as the
306 /// [`id`](crate::SentRequest::id) of a request sent through this
307 /// connection. The raw [`ConnectionTo::send_cancel_request`](crate::ConnectionTo::send_cancel_request)
308 /// takes any [`RawJsonRpcId`](crate::RawJsonRpcId).
309 pub fn send_cancel_request(
310 &self,
311 request_id: impl Into<crate::v2::schema::RequestId>,
312 ) -> Result<(), crate::Error>
313 where
314 Counterpart: HasPeer<Counterpart>,
315 {
316 self.inner.send_cancel_request(request_id.into())
317 }
318
319 /// Send a `$/cancel_request` notification to a specific peer.
320 ///
321 /// The notification is always sent plain, never wrapped in
322 /// `proxy/successor`, as the raw
323 /// [`ConnectionTo::send_cancel_request_to`](crate::ConnectionTo::send_cancel_request_to)
324 /// describes. See [`send_cancel_request`](Self::send_cancel_request).
325 pub fn send_cancel_request_to<Peer: Role>(
326 &self,
327 peer: Peer,
328 request_id: impl Into<crate::v2::schema::RequestId>,
329 ) -> Result<(), crate::Error>
330 where
331 Counterpart: HasPeer<Peer>,
332 {
333 self.inner.send_cancel_request_to(peer, request_id.into())
334 }
335
336 /// Register a low-level dynamic message handler.
337 ///
338 /// Dynamic handlers use the version-neutral [`HandleDispatchFrom`] trait,
339 /// so their callback receives the underlying version-neutral
340 /// [`ConnectionTo`](crate::ConnectionTo). Prefer typed handlers on
341 /// [`v2::Builder`](crate::v2::Builder) when registration can happen before the
342 /// connection starts.
343 ///
344 /// ```no_run
345 /// # use agent_client_protocol::{
346 /// # Agent, Client, ConnectTo, DynamicHandlerGuard, Error, NullHandler,
347 /// # };
348 /// # async fn example(
349 /// # transport: impl ConnectTo<Client> + 'static,
350 /// # ) -> Result<(), Error> {
351 /// Client.v2().connect_with(transport, async |connection| {
352 /// let guard: DynamicHandlerGuard<Agent> =
353 /// connection.add_dynamic_handler(NullHandler)?;
354 ///
355 /// // Keep `guard` alive for as long as the modal handler is needed.
356 /// drop(guard);
357 /// Ok(())
358 /// }).await
359 /// # }
360 /// ```
361 pub fn add_dynamic_handler(
362 &self,
363 handler: impl HandleDispatchFrom<Counterpart> + 'static,
364 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error> {
365 self.inner.add_dynamic_handler(handler)
366 }
367}