Skip to main content

agent_client_protocol_schema/v1/
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. Implementations MUST NOT make assumptions about values at
123    /// these keys.
124    ///
125    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/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 make assumptions about values at
153    /// these keys.
154    ///
155    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/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. Implementations MUST NOT make assumptions about values at
196    /// these keys.
197    ///
198    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/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 make assumptions about values at
226    /// these keys.
227    ///
228    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/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#[allow(clippy::large_enum_variant)]
296#[serde(untagged)]
297#[cfg_attr(feature = "schemars", schemars(inline))]
298#[non_exhaustive]
299pub enum ProxyRequest {
300    /// Initializes a component as a proxy.
301    ///
302    /// Sent by a conductor to each component that has a successor, instead of `initialize`.
303    /// The params and response are the same as for `initialize`.
304    InitializeProxyRequest(InitializeProxyRequest),
305    /// Forwards a request to or from a proxy's successor.
306    ///
307    /// A proxy sends it to its conductor to forward a request downstream. The conductor sends
308    /// it to the proxy to deliver a request from the successor. The wrapped request's result
309    /// or error is returned unchanged.
310    ProxySuccessorRequest(ProxySuccessorRequest),
311}
312
313impl ProxyRequest {
314    /// Returns the corresponding method name of the request.
315    #[must_use]
316    pub fn method(&self) -> &str {
317        match self {
318            Self::InitializeProxyRequest(_) => PROXY_METHOD_NAMES.proxy_initialize,
319            Self::ProxySuccessorRequest(_) => PROXY_METHOD_NAMES.proxy_successor,
320        }
321    }
322}
323
324/// **UNSTABLE**
325///
326/// This capability is not part of the spec yet, and may be removed or changed at any point.
327///
328/// Responses to requests exchanged between a conductor and its proxies.
329#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
330#[derive(Clone, Debug, Serialize, Deserialize)]
331#[allow(clippy::large_enum_variant)]
332#[serde(untagged)]
333#[cfg_attr(feature = "schemars", schemars(inline))]
334#[non_exhaustive]
335pub enum ProxyResponse {
336    /// Successful result returned for a `proxy/initialize` request.
337    InitializeProxyResponse(InitializeProxyResponse),
338    /// Successful result returned for a `proxy/successor` request.
339    ProxySuccessorResponse(ProxySuccessorResponse),
340}
341
342/// **UNSTABLE**
343///
344/// This capability is not part of the spec yet, and may be removed or changed at any point.
345///
346/// Notifications exchanged between a conductor and its proxies.
347#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
348#[derive(Clone, Debug, Serialize, Deserialize)]
349#[serde(untagged)]
350#[cfg_attr(feature = "schemars", schemars(inline))]
351#[non_exhaustive]
352pub enum ProxyNotification {
353    /// Forwards a notification to or from a proxy's successor.
354    ///
355    /// A proxy sends it to its conductor to forward a notification downstream. The conductor
356    /// sends it to the proxy to deliver a notification from the successor.
357    ProxySuccessorNotification(ProxySuccessorNotification),
358}
359
360impl ProxyNotification {
361    /// Returns the corresponding method name of the notification.
362    #[must_use]
363    pub fn method(&self) -> &str {
364        match self {
365            Self::ProxySuccessorNotification(_) => PROXY_METHOD_NAMES.proxy_successor,
366        }
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use serde_json::json;
373
374    use super::*;
375    use crate::ProtocolVersion;
376
377    fn raw(value: &serde_json::Value) -> Arc<RawValue> {
378        RawValue::from_string(value.to_string()).unwrap().into()
379    }
380
381    #[test]
382    fn test_initialize_proxy_request_matches_initialize_params() {
383        let request = InitializeProxyRequest::new(InitializeRequest::new(ProtocolVersion::V1));
384        let value = serde_json::to_value(&request).unwrap();
385        assert_eq!(value, serde_json::to_value(&request.initialize).unwrap());
386        assert_eq!(
387            ProxyRequest::InitializeProxyRequest(request).method(),
388            "proxy/initialize"
389        );
390        let response = InitializeProxyResponse::new(InitializeResponse::new(ProtocolVersion::V1));
391        assert_eq!(
392            serde_json::to_value(&response).unwrap(),
393            serde_json::to_value(&response.initialize).unwrap()
394        );
395        let parsed: InitializeProxyRequest = serde_json::from_value(value).unwrap();
396        assert_eq!(parsed.initialize.protocol_version, ProtocolVersion::V1);
397    }
398
399    #[test]
400    fn test_proxy_successor_round_trip() {
401        let request = ProxySuccessorRequest::new("session/prompt")
402            .params(raw(
403                &json!({"sessionId": "sess_abc123", "_meta": {"inner": true}}),
404            ))
405            .meta(json!({"outer": true}).as_object().cloned());
406        let value = serde_json::to_value(&request).unwrap();
407        assert_eq!(
408            value,
409            json!({
410                "method": "session/prompt",
411                "params": {"sessionId": "sess_abc123", "_meta": {"inner": true}},
412                "_meta": {"outer": true}
413            })
414        );
415        assert_eq!(
416            ProxyRequest::ProxySuccessorRequest(request).method(),
417            "proxy/successor"
418        );
419
420        for params in [
421            json!({"method": "session/cancel"}),
422            json!({"method": "session/cancel", "params": null}),
423        ] {
424            let notification: ProxySuccessorNotification = serde_json::from_value(params).unwrap();
425            assert_eq!(&*notification.method, "session/cancel");
426            assert!(notification.params.is_none());
427        }
428        assert!(
429            serde_json::from_value::<ProxySuccessorNotification>(json!({"params": {}})).is_err()
430        );
431
432        let response = ProxySuccessorResponse::new(raw(&json!({"stopReason": "end_turn"})));
433        assert_eq!(
434            serde_json::to_value(&response).unwrap(),
435            json!({"stopReason": "end_turn"})
436        );
437    }
438}