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