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