agent_client_protocol/jsonrpc/protocol.rs
1//! The protocol markers that type request handles and builder callbacks by ACP
2//! version.
3
4use std::fmt::{self, Debug, Display};
5use std::hash::Hash;
6
7use serde::Serialize;
8
9use crate::jsonrpc::{ConnectionTo, RawJsonRpcId};
10use crate::role::Role;
11use crate::v1::schema as v1;
12#[cfg(feature = "unstable_protocol_v2")]
13use crate::v2::schema as v2;
14
15/// The protocol version of a request handle or builder, as a type.
16///
17/// The request handles ([`Responder`](crate::Responder),
18/// [`ResponseRouter`](crate::ResponseRouter), [`SentRequest`](crate::SentRequest),
19/// [`PreparedRequest`](crate::PreparedRequest) and [`Dispatch`](crate::Dispatch))
20/// and the connection [`Builder`](crate::Builder) take a protocol marker as their
21/// last type parameter. It selects the request ID type their `id()` returns and
22/// the connection type builder callbacks receive:
23///
24/// | Marker | `RequestId` | `Connection<Counterpart>` |
25/// | --- | --- | --- |
26/// | [`Raw`] (the default) | [`RawJsonRpcId`] | [`ConnectionTo`] |
27/// | [`V1`] | [`v1::schema::RequestId`](crate::v1::schema::RequestId) | [`v1::ConnectionTo`](crate::v1::ConnectionTo) |
28/// | `V2` (`unstable_protocol_v2`) | `v2::schema::RequestId` | `v2::ConnectionTo` |
29///
30/// - Typed handlers on `Client.v1()`, `Agent.v1()` and
31/// `Proxy.v1()`, and requests sent through `v1::ConnectionTo`, use [`V1`].
32/// - Typed handlers on `Client.v2()`, `Agent.v2()` and `Proxy.v2()`, and
33/// requests sent through `v2::ConnectionTo`, use `V2`.
34/// - Everything that is not tied to one version uses [`Raw`]: the
35/// [`HandleDispatchFrom`](crate::HandleDispatchFrom) layer,
36/// [`MatchDispatch`](crate::util::MatchDispatch), builders whose version
37/// guard is disabled, and requests sent through [`ConnectionTo`].
38///
39/// The [`v1`](crate::v1) and `v2` modules name the typed handles:
40/// `v1::Responder<T>` is `Responder<T, V1>`, `v2::SentRequest<T>` is
41/// `SentRequest<T, V2>`, `v1::Builder<Host>` is `Builder<Host, .., V1>`, and so
42/// on.
43///
44/// Every handle also has `raw_id()`, which returns the wire ID as a
45/// [`RawJsonRpcId`] whatever the marker. A [`Raw`] handle converts to a typed
46/// one with `into_protocol`, and any handle converts back with `into_raw`.
47///
48/// ```
49/// use agent_client_protocol::{RawJsonRpcId, Responder, V1, v1::schema};
50///
51/// fn log_request(responder: &Responder<schema::PromptResponse, V1>) {
52/// let id: &schema::RequestId = responder.id();
53/// let wire_id: &RawJsonRpcId = responder.raw_id();
54/// assert_eq!(RawJsonRpcId::from(id.clone()), *wire_id);
55/// }
56///
57/// fn log_raw_request(responder: &Responder<schema::PromptResponse>) {
58/// // `Responder<T>` is `Responder<T, Raw>`.
59/// let id: &RawJsonRpcId = responder.id();
60/// # let _ = id;
61/// }
62/// ```
63///
64/// Generic code can accept handles of any version:
65///
66/// ```
67/// use agent_client_protocol::{Protocol, Responder, v1::schema};
68///
69/// fn reject<P: Protocol>(responder: Responder<schema::PromptResponse, P>) -> agent_client_protocol::Result<()> {
70/// responder.respond_with_error(agent_client_protocol::Error::invalid_params())
71/// }
72/// ```
73///
74/// This trait is sealed: the SDK defines one marker per supported protocol
75/// version, plus [`Raw`].
76#[allow(private_bounds)]
77pub trait Protocol: sealed::Sealed + Copy + Debug + Default + Send + Sync + 'static {
78 /// The request ID type of this protocol version.
79 type RequestId: Clone
80 + Debug
81 + Display
82 + Eq
83 + Hash
84 + Serialize
85 + Into<RawJsonRpcId>
86 + Send
87 + Sync
88 + 'static;
89
90 /// The connection type that builder callbacks receive for `Counterpart`.
91 ///
92 /// Every connection type converts back to the version-neutral
93 /// [`ConnectionTo`] with `Into`, so code generic over `P` can reach the
94 /// raw connection:
95 ///
96 /// ```
97 /// use agent_client_protocol::{Agent, ConnectionTo, Protocol};
98 ///
99 /// fn raw<P: Protocol>(connection: P::Connection<Agent>) -> ConnectionTo<Agent> {
100 /// connection.into()
101 /// }
102 /// ```
103 type Connection<Counterpart: Role>: Clone
104 + Send
105 + Sync
106 + Into<ConnectionTo<Counterpart>>
107 + 'static;
108}
109
110pub(crate) mod sealed {
111 use super::{ConnectionTo, Protocol, RawJsonRpcId, Role};
112
113 pub trait Sealed {
114 /// The ACP protocol version this marker types, or `None` for
115 /// [`Raw`](super::Raw).
116 fn protocol_version() -> Option<crate::schema::ProtocolVersion>;
117
118 /// Wraps the version-neutral connection in this version's connection
119 /// type.
120 fn connection_from_raw<Counterpart: Role>(
121 connection: ConnectionTo<Counterpart>,
122 ) -> <Self as Protocol>::Connection<Counterpart>
123 where
124 Self: Protocol;
125
126 /// Converts a wire ID to this version's request ID. Fails only for
127 /// IDs the version does not allow.
128 fn request_id_from_raw(
129 id: &RawJsonRpcId,
130 ) -> Result<<Self as Protocol>::RequestId, crate::Error>
131 where
132 Self: Protocol;
133
134 /// The request ID for a string ID the SDK generated.
135 fn request_id_from_string(id: String) -> <Self as Protocol>::RequestId
136 where
137 Self: Protocol;
138 }
139}
140
141/// The ACP protocol version `P` types, or `None` for [`Raw`].
142pub(crate) fn protocol_version<P: Protocol>() -> Option<crate::schema::ProtocolVersion> {
143 <P as sealed::Sealed>::protocol_version()
144}
145
146pub(crate) fn connection_from_raw<P: Protocol, Counterpart: Role>(
147 connection: ConnectionTo<Counterpart>,
148) -> P::Connection<Counterpart> {
149 <P as sealed::Sealed>::connection_from_raw(connection)
150}
151
152/// The version-neutral protocol marker of the raw dispatch layer, and the
153/// default.
154///
155/// `id()` on a `Raw` handle returns the [`RawJsonRpcId`] exactly as it
156/// appears on the wire, which may be `null`. See [`Protocol`].
157#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
158pub struct Raw;
159
160impl sealed::Sealed for Raw {
161 fn protocol_version() -> Option<crate::schema::ProtocolVersion> {
162 None
163 }
164
165 fn connection_from_raw<Counterpart: Role>(
166 connection: ConnectionTo<Counterpart>,
167 ) -> ConnectionTo<Counterpart> {
168 connection
169 }
170
171 fn request_id_from_raw(id: &RawJsonRpcId) -> Result<RawJsonRpcId, crate::Error> {
172 Ok(id.clone())
173 }
174
175 fn request_id_from_string(id: String) -> RawJsonRpcId {
176 RawJsonRpcId::Str(id)
177 }
178}
179
180impl Protocol for Raw {
181 type RequestId = RawJsonRpcId;
182 type Connection<Counterpart: Role> = ConnectionTo<Counterpart>;
183}
184
185/// ACP protocol version 1.
186///
187/// See [`Protocol`].
188#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
189pub struct V1;
190
191impl sealed::Sealed for V1 {
192 fn protocol_version() -> Option<crate::schema::ProtocolVersion> {
193 Some(crate::schema::ProtocolVersion::V1)
194 }
195
196 fn connection_from_raw<Counterpart: Role>(
197 connection: ConnectionTo<Counterpart>,
198 ) -> super::v1_connection::ConnectionTo<Counterpart> {
199 super::v1_connection::ConnectionTo { inner: connection }
200 }
201
202 fn request_id_from_raw(id: &RawJsonRpcId) -> Result<v1::RequestId, crate::Error> {
203 Ok(id.clone().into())
204 }
205
206 fn request_id_from_string(id: String) -> v1::RequestId {
207 v1::RequestId::Str(id)
208 }
209}
210
211impl Protocol for V1 {
212 type RequestId = v1::RequestId;
213 type Connection<Counterpart: Role> = super::v1_connection::ConnectionTo<Counterpart>;
214}
215
216/// ACP protocol version 2.
217///
218/// See [`Protocol`]. Request IDs on v2 connections are never `null`: the SDK
219/// rejects such requests before dispatch, so `id()` on a `V2` handle is
220/// infallible.
221#[cfg(feature = "unstable_protocol_v2")]
222#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
223pub struct V2;
224
225#[cfg(feature = "unstable_protocol_v2")]
226impl sealed::Sealed for V2 {
227 fn protocol_version() -> Option<crate::schema::ProtocolVersion> {
228 Some(crate::schema::ProtocolVersion::V2)
229 }
230
231 fn connection_from_raw<Counterpart: Role>(
232 connection: ConnectionTo<Counterpart>,
233 ) -> super::v2_connection::ConnectionTo<Counterpart> {
234 super::v2_connection::ConnectionTo { inner: connection }
235 }
236
237 fn request_id_from_raw(id: &RawJsonRpcId) -> Result<v2::RequestId, crate::Error> {
238 id.clone().try_into()
239 }
240
241 fn request_id_from_string(id: String) -> v2::RequestId {
242 v2::RequestId::Str(id)
243 }
244}
245
246#[cfg(feature = "unstable_protocol_v2")]
247impl Protocol for V2 {
248 type RequestId = v2::RequestId;
249 type Connection<Counterpart: Role> = super::v2_connection::ConnectionTo<Counterpart>;
250}
251
252/// The ID of a request handle: the wire ID and the same ID as the handle's
253/// protocol version types it.
254pub(crate) struct HandleId<P: Protocol> {
255 raw: RawJsonRpcId,
256 typed: P::RequestId,
257}
258
259impl<P: Protocol> HandleId<P> {
260 /// The ID of a request the SDK sends.
261 pub(crate) fn generated(id: String) -> Self {
262 Self {
263 typed: P::request_id_from_string(id.clone()),
264 raw: RawJsonRpcId::Str(id),
265 }
266 }
267
268 /// The ID of a received request or response. Fails only for IDs `P` does
269 /// not allow.
270 pub(crate) fn from_raw(raw: RawJsonRpcId) -> Result<Self, crate::Error> {
271 Ok(Self {
272 typed: P::request_id_from_raw(&raw)?,
273 raw,
274 })
275 }
276
277 pub(crate) fn raw(&self) -> &RawJsonRpcId {
278 &self.raw
279 }
280
281 pub(crate) fn typed(&self) -> &P::RequestId {
282 &self.typed
283 }
284
285 /// The same ID for protocol `Q`. Fails only for IDs `Q` does not allow.
286 pub(crate) fn into_protocol<Q: Protocol>(self) -> Result<HandleId<Q>, crate::Error> {
287 HandleId::from_raw(self.raw)
288 }
289
290 /// The same ID in the raw dispatch layer.
291 pub(crate) fn into_raw(self) -> HandleId<Raw> {
292 HandleId::raw_layer(self.raw)
293 }
294}
295
296impl HandleId<Raw> {
297 /// The ID of a message in the raw dispatch layer, which allows every
298 /// JSON-RPC ID.
299 pub(crate) fn raw_layer(raw: RawJsonRpcId) -> Self {
300 Self {
301 typed: raw.clone(),
302 raw,
303 }
304 }
305}
306
307impl<P: Protocol> Clone for HandleId<P> {
308 fn clone(&self) -> Self {
309 Self {
310 raw: self.raw.clone(),
311 typed: self.typed.clone(),
312 }
313 }
314}
315
316impl<P: Protocol> Debug for HandleId<P> {
317 /// Formats as the wire ID, as the handles did before they were typed.
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 Debug::fmt(&self.raw, f)
320 }
321}