Skip to main content

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}