agent_client_protocol_schema/v2/proxy.rs
1//! Proxy chain types: messages exchanged between a conductor and the proxies it manages.
2//!
3//! Ordinary clients and agents never send or receive these messages.
4//! See the [Proxy Chains RFD](https://agentclientprotocol.com/rfds/proxy-chains).
5
6use std::sync::Arc;
7
8use derive_more::From;
9use serde::{Deserialize, Serialize};
10use serde_json::value::RawValue;
11use serde_with::{DefaultOnError, serde_as, skip_serializing_none};
12
13use super::{InitializeRequest, InitializeResponse, Meta};
14use crate::IntoOption;
15
16// Initialize proxy
17
18/// **UNSTABLE**
19///
20/// This capability is not part of the spec yet, and may be removed or changed at any point.
21///
22/// Request parameters for `proxy/initialize`.
23///
24/// A conductor sends `proxy/initialize` instead of `initialize` to every component that has a
25/// successor, which tells the component to run as a proxy. The params and the response are
26/// the same as for `initialize`. A conductor sends `initialize` to the final agent.
27///
28/// The params carry the protocol version the conductor selected: the client's requested
29/// version, unless the conductor supports only a lower one. Before answering successfully,
30/// the proxy MUST forward an `initialize` request to its successor through `proxy/successor`.
31///
32/// See the [Proxy Chains RFD](https://agentclientprotocol.com/rfds/proxy-chains).
33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "proxy", "x-method" = PROXY_INITIALIZE_METHOD_NAME)))]
36#[serde(transparent)]
37#[non_exhaustive]
38pub struct InitializeProxyRequest {
39 /// The `initialize` params, sent unchanged.
40 pub initialize: InitializeRequest,
41}
42
43impl InitializeProxyRequest {
44 /// Builds [`InitializeProxyRequest`] from the `initialize` params to send.
45 #[must_use]
46 pub fn new(initialize: InitializeRequest) -> Self {
47 Self { initialize }
48 }
49}
50
51impl From<InitializeRequest> for InitializeProxyRequest {
52 fn from(initialize: InitializeRequest) -> Self {
53 Self::new(initialize)
54 }
55}
56
57/// **UNSTABLE**
58///
59/// This capability is not part of the spec yet, and may be removed or changed at any point.
60///
61/// Response to `proxy/initialize`.
62///
63/// The same as the `initialize` response. The proxy builds it from its successor's
64/// `initialize` response and answers with the capabilities of the chain from itself onward.
65/// It MUST NOT change `protocolVersion`.
66#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "proxy", "x-method" = PROXY_INITIALIZE_METHOD_NAME)))]
69#[serde(transparent)]
70#[non_exhaustive]
71pub struct InitializeProxyResponse {
72 /// The `initialize` response, sent unchanged.
73 pub initialize: InitializeResponse,
74}
75
76impl InitializeProxyResponse {
77 /// Builds [`InitializeProxyResponse`] from the `initialize` response to send.
78 #[must_use]
79 pub fn new(initialize: InitializeResponse) -> Self {
80 Self { initialize }
81 }
82}
83
84impl From<InitializeResponse> for InitializeProxyResponse {
85 fn from(initialize: InitializeResponse) -> Self {
86 Self::new(initialize)
87 }
88}
89
90// Successor messages
91
92/// **UNSTABLE**
93///
94/// This capability is not part of the spec yet, and may be removed or changed at any point.
95///
96/// Parameters of a `proxy/successor` request.
97///
98/// A proxy sends it to forward a message to its successor, and the conductor sends it to
99/// deliver a message from the successor to the proxy. The envelope wraps one request: `method`
100/// and `params` are the wrapped request's method and params.
101///
102/// The result or error of the wrapped request is returned unchanged, as the same JSON value,
103/// as the result or error of the `proxy/successor` request. Errors the conductor returns
104/// itself, for example because the successor has exited, are not wrapped results.
105///
106/// To cancel it, the sender sends `$/cancel_request` with the ID of this `proxy/successor`
107/// request on the same connection. Cancellation is never forwarded to another hop.
108///
109/// See the [Proxy Chains RFD](https://agentclientprotocol.com/rfds/proxy-chains).
110#[serde_as]
111#[skip_serializing_none]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "proxy", "x-method" = PROXY_SUCCESSOR_METHOD_NAME)))]
115#[serde(rename_all = "camelCase")]
116#[non_exhaustive]
117pub struct ProxySuccessorRequest {
118 /// Method name of the wrapped request.
119 pub method: Arc<str>,
120 /// Params of the wrapped request, forwarded unchanged.
121 ///
122 /// Optional. Omitted and `null` are equivalent and mean the wrapped request has no params.
123 /// When present, it MUST be a JSON object or array, as JSON-RPC requires.
124 /// The wrapped request's own `_meta` stays inside these params.
125 #[cfg_attr(feature = "schemars", schemars(with = "Option<serde_json::Value>"))]
126 #[serde(default)]
127 pub params: Option<Arc<RawValue>>,
128 /// Metadata for the `proxy/successor` envelope itself, separate from the wrapped
129 /// message's `_meta`. It is hop-local: the conductor does not copy it into the message
130 /// it unwraps.
131 ///
132 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
133 /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
134 /// Implementations MUST NOT assign meaning to keys they do not recognize.
135 ///
136 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
137 #[serde_as(deserialize_as = "DefaultOnError")]
138 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
139 #[serde(default)]
140 #[serde(rename = "_meta")]
141 pub meta: Option<Meta>,
142}
143
144impl ProxySuccessorRequest {
145 /// Builds [`ProxySuccessorRequest`] with the required fields set. Optional fields start unset or empty.
146 #[must_use]
147 pub fn new(method: impl Into<Arc<str>>) -> Self {
148 Self {
149 method: method.into(),
150 params: None,
151 meta: None,
152 }
153 }
154
155 /// Params of the wrapped request, forwarded unchanged.
156 #[must_use]
157 pub fn params(mut self, params: impl IntoOption<Arc<RawValue>>) -> Self {
158 self.params = params.into_option();
159 self
160 }
161
162 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
163 /// metadata to their interactions. Implementations MUST NOT assign meaning to keys they
164 /// do not recognize.
165 ///
166 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
167 #[must_use]
168 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
169 self.meta = meta.into_option();
170 self
171 }
172}
173
174/// **UNSTABLE**
175///
176/// This capability is not part of the spec yet, and may be removed or changed at any point.
177///
178/// Parameters of a `proxy/successor` notification.
179///
180/// A proxy sends it to forward a message to its successor, and the conductor sends it to
181/// deliver a message from the successor to the proxy. The envelope wraps one notification: `method`
182/// and `params` are the wrapped notification's method and params.
183///
184/// See the [Proxy Chains RFD](https://agentclientprotocol.com/rfds/proxy-chains).
185#[serde_as]
186#[skip_serializing_none]
187#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "proxy", "x-method" = PROXY_SUCCESSOR_METHOD_NAME)))]
190#[serde(rename_all = "camelCase")]
191#[non_exhaustive]
192pub struct ProxySuccessorNotification {
193 /// Method name of the wrapped notification.
194 pub method: Arc<str>,
195 /// Params of the wrapped notification, forwarded unchanged.
196 ///
197 /// Optional. Omitted and `null` are equivalent and mean the wrapped notification has no params.
198 /// When present, it MUST be a JSON object or array, as JSON-RPC requires.
199 /// The wrapped notification's own `_meta` stays inside these params.
200 #[cfg_attr(feature = "schemars", schemars(with = "Option<serde_json::Value>"))]
201 #[serde(default)]
202 pub params: Option<Arc<RawValue>>,
203 /// Metadata for the `proxy/successor` envelope itself, separate from the wrapped
204 /// message's `_meta`. It is hop-local: the conductor does not copy it into the message
205 /// it unwraps.
206 ///
207 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
208 /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
209 /// Implementations MUST NOT assign meaning to keys they do not recognize.
210 ///
211 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
212 #[serde_as(deserialize_as = "DefaultOnError")]
213 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
214 #[serde(default)]
215 #[serde(rename = "_meta")]
216 pub meta: Option<Meta>,
217}
218
219impl ProxySuccessorNotification {
220 /// Builds [`ProxySuccessorNotification`] with the required fields set. Optional fields start unset or empty.
221 #[must_use]
222 pub fn new(method: impl Into<Arc<str>>) -> Self {
223 Self {
224 method: method.into(),
225 params: None,
226 meta: None,
227 }
228 }
229
230 /// Params of the wrapped notification, forwarded unchanged.
231 #[must_use]
232 pub fn params(mut self, params: impl IntoOption<Arc<RawValue>>) -> Self {
233 self.params = params.into_option();
234 self
235 }
236
237 /// The _meta property is reserved by ACP to allow clients and agents to attach additional
238 /// metadata to their interactions. Implementations MUST NOT assign meaning to keys they
239 /// do not recognize.
240 ///
241 /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
242 #[must_use]
243 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
244 self.meta = meta.into_option();
245 self
246 }
247}
248
249/// **UNSTABLE**
250///
251/// This capability is not part of the spec yet, and may be removed or changed at any point.
252///
253/// Result of a `proxy/successor` request: the wrapped request's result, forwarded unchanged.
254#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
255#[derive(Debug, Clone, Serialize, Deserialize, From)]
256#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "proxy", "x-method" = PROXY_SUCCESSOR_METHOD_NAME)))]
257#[serde(transparent)]
258#[non_exhaustive]
259pub struct ProxySuccessorResponse(
260 #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc<RawValue>,
261);
262
263impl ProxySuccessorResponse {
264 /// Wraps the successor's result.
265 #[must_use]
266 pub fn new(result: Arc<RawValue>) -> Self {
267 Self(result)
268 }
269}
270
271// Method schema
272
273/// **UNSTABLE**
274///
275/// This capability is not part of the spec yet, and may be removed or changed at any point.
276///
277/// Names of the methods exchanged between a conductor and its proxies.
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
279#[non_exhaustive]
280pub struct ProxyMethodNames {
281 /// Method for initializing a component as a proxy.
282 pub proxy_initialize: &'static str,
283 /// Method for forwarding a message to or from a proxy's successor.
284 pub proxy_successor: &'static str,
285}
286
287/// Constant containing all proxy chain method names.
288pub const PROXY_METHOD_NAMES: ProxyMethodNames = ProxyMethodNames {
289 proxy_initialize: PROXY_INITIALIZE_METHOD_NAME,
290 proxy_successor: PROXY_SUCCESSOR_METHOD_NAME,
291};
292
293/// Method name for initializing a component as a proxy.
294pub(crate) const PROXY_INITIALIZE_METHOD_NAME: &str = "proxy/initialize";
295/// Method name for forwarding a message to or from a proxy's successor.
296pub(crate) const PROXY_SUCCESSOR_METHOD_NAME: &str = "proxy/successor";
297
298/// **UNSTABLE**
299///
300/// This capability is not part of the spec yet, and may be removed or changed at any point.
301///
302/// Requests exchanged between a conductor and its proxies.
303///
304/// A conductor sends `proxy/initialize` and `proxy/successor` to a proxy. A proxy sends
305/// `proxy/successor` to its conductor.
306#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
307#[derive(Clone, Debug, Serialize, Deserialize)]
308#[serde(untagged)]
309#[cfg_attr(feature = "schemars", schemars(inline))]
310#[non_exhaustive]
311pub enum ProxyRequest {
312 /// Initializes a component as a proxy.
313 ///
314 /// Sent by a conductor to each component that has a successor, instead of `initialize`.
315 /// The params and response are the same as for `initialize`.
316 InitializeProxyRequest(Box<InitializeProxyRequest>),
317 /// Forwards a request to or from a proxy's successor.
318 ///
319 /// A proxy sends it to its conductor to forward a request downstream. The conductor sends
320 /// it to the proxy to deliver a request from the successor. The wrapped request's result
321 /// or error is returned unchanged.
322 ProxySuccessorRequest(Box<ProxySuccessorRequest>),
323}
324
325impl ProxyRequest {
326 /// Returns the corresponding method name of the request.
327 #[must_use]
328 pub fn method(&self) -> &str {
329 match self {
330 Self::InitializeProxyRequest(_) => PROXY_METHOD_NAMES.proxy_initialize,
331 Self::ProxySuccessorRequest(_) => PROXY_METHOD_NAMES.proxy_successor,
332 }
333 }
334}
335
336/// **UNSTABLE**
337///
338/// This capability is not part of the spec yet, and may be removed or changed at any point.
339///
340/// Responses to requests exchanged between a conductor and its proxies.
341#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
342#[derive(Clone, Debug, Serialize, Deserialize)]
343#[serde(untagged)]
344#[cfg_attr(feature = "schemars", schemars(inline))]
345#[non_exhaustive]
346pub enum ProxyResponse {
347 /// Successful result returned for a `proxy/initialize` request.
348 InitializeProxyResponse(Box<InitializeProxyResponse>),
349 /// Successful result returned for a `proxy/successor` request.
350 ProxySuccessorResponse(Box<ProxySuccessorResponse>),
351}
352
353/// **UNSTABLE**
354///
355/// This capability is not part of the spec yet, and may be removed or changed at any point.
356///
357/// Notifications exchanged between a conductor and its proxies.
358#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
359#[derive(Clone, Debug, Serialize, Deserialize)]
360#[serde(untagged)]
361#[cfg_attr(feature = "schemars", schemars(inline))]
362#[non_exhaustive]
363pub enum ProxyNotification {
364 /// Forwards a notification to or from a proxy's successor.
365 ///
366 /// A proxy sends it to its conductor to forward a notification downstream. The conductor
367 /// sends it to the proxy to deliver a notification from the successor.
368 ProxySuccessorNotification(Box<ProxySuccessorNotification>),
369}
370
371impl ProxyNotification {
372 /// Returns the corresponding method name of the notification.
373 #[must_use]
374 pub fn method(&self) -> &str {
375 match self {
376 Self::ProxySuccessorNotification(_) => PROXY_METHOD_NAMES.proxy_successor,
377 }
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use serde_json::json;
384
385 use super::*;
386 use crate::ProtocolVersion;
387
388 fn raw(value: &serde_json::Value) -> Arc<RawValue> {
389 RawValue::from_string(value.to_string()).unwrap().into()
390 }
391
392 #[test]
393 fn test_initialize_proxy_request_matches_initialize_params() {
394 let request = InitializeProxyRequest::new(InitializeRequest::new(
395 ProtocolVersion::V2,
396 crate::v2::Implementation::new("conductor", "1.0.0"),
397 ));
398 let value = serde_json::to_value(&request).unwrap();
399 assert_eq!(value, serde_json::to_value(&request.initialize).unwrap());
400 assert_eq!(
401 ProxyRequest::InitializeProxyRequest(Box::new(request)).method(),
402 "proxy/initialize"
403 );
404 let response = InitializeProxyResponse::new(InitializeResponse::new(
405 ProtocolVersion::V2,
406 crate::v2::Implementation::new("proxy", "1.0.0"),
407 ));
408 assert_eq!(
409 serde_json::to_value(&response).unwrap(),
410 serde_json::to_value(&response.initialize).unwrap()
411 );
412 let parsed: InitializeProxyRequest = serde_json::from_value(value).unwrap();
413 assert_eq!(parsed.initialize.protocol_version, ProtocolVersion::V2);
414 }
415
416 #[test]
417 fn test_proxy_successor_round_trip() {
418 let request = ProxySuccessorRequest::new("session/prompt")
419 .params(raw(
420 &json!({"sessionId": "sess_abc123", "_meta": {"inner": true}}),
421 ))
422 .meta(json!({"outer": true}).as_object().cloned());
423 let value = serde_json::to_value(&request).unwrap();
424 assert_eq!(
425 value,
426 json!({
427 "method": "session/prompt",
428 "params": {"sessionId": "sess_abc123", "_meta": {"inner": true}},
429 "_meta": {"outer": true}
430 })
431 );
432 assert_eq!(
433 ProxyRequest::ProxySuccessorRequest(Box::new(request)).method(),
434 "proxy/successor"
435 );
436
437 for params in [
438 json!({"method": "session/cancel"}),
439 json!({"method": "session/cancel", "params": null}),
440 ] {
441 let notification: ProxySuccessorNotification = serde_json::from_value(params).unwrap();
442 assert_eq!(&*notification.method, "session/cancel");
443 assert!(notification.params.is_none());
444 }
445 assert!(
446 serde_json::from_value::<ProxySuccessorNotification>(json!({"params": {}})).is_err()
447 );
448
449 let response = ProxySuccessorResponse::new(raw(&json!({"stopReason": "end_turn"})));
450 assert_eq!(
451 serde_json::to_value(&response).unwrap(),
452 json!({"stopReason": "end_turn"})
453 );
454 }
455}