Skip to main content

agent_client_protocol_schema/
rpc.rs

1//! JSON-RPC envelope types shared by ACP clients and agents.
2//!
3//! These types model the JSON-RPC 2.0 request, response, notification, and
4//! batch envelopes that wrap ACP method-specific payloads.
5
6use std::sync::Arc;
7
8use derive_more::Display;
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12// Kept here for compatibility; these are the protocol version 1 types.
13pub use crate::v1::{Request, RequestId, Response};
14
15/// A JSON-RPC notification object.
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
18#[allow(
19    clippy::exhaustive_structs,
20    reason = "This comes from the JSON-RPC specification itself"
21)]
22#[cfg_attr(feature = "schemars", schemars(rename = "{Params}", extend("x-docs-ignore" = true)))]
23#[skip_serializing_none]
24pub struct Notification<Params> {
25    /// The notification method name.
26    pub method: Arc<str>,
27    /// Method-specific notification parameters.
28    pub params: Option<Params>,
29}
30
31#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "schemars", schemars(inline))]
34enum JsonRpcVersion {
35    #[serde(rename = "2.0")]
36    V2,
37}
38
39/// A message (request, response, or notification) with `"jsonrpc": "2.0"` specified as
40/// [required by JSON-RPC 2.0 Specification][1].
41///
42/// [1]: https://www.jsonrpc.org/specification#compatibility
43#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[cfg_attr(feature = "schemars", schemars(inline))]
46pub struct JsonRpcMessage<M> {
47    jsonrpc: JsonRpcVersion,
48    #[serde(flatten)]
49    message: M,
50}
51
52impl<M> JsonRpcMessage<M> {
53    /// Wraps the provided message into a versioned [`JsonRpcMessage`].
54    #[must_use]
55    pub fn wrap(message: M) -> Self {
56        Self {
57            jsonrpc: JsonRpcVersion::V2,
58            message,
59        }
60    }
61
62    /// Returns the contained message.
63    #[must_use]
64    pub fn inner(&self) -> &M {
65        &self.message
66    }
67
68    /// Unwraps the contained message.
69    #[must_use]
70    pub fn into_inner(self) -> M {
71        self.message
72    }
73}
74
75/// Error returned when constructing an empty JSON-RPC batch.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
77#[display("JSON-RPC batch must contain at least one message")]
78#[non_exhaustive]
79pub struct EmptyJsonRpcBatch;
80
81impl std::error::Error for EmptyJsonRpcBatch {}
82
83/// A non-empty JSON-RPC 2.0 batch message.
84#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
85#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
86#[cfg_attr(feature = "schemars", schemars(inline))]
87#[serde(transparent)]
88#[allow(
89    clippy::exhaustive_structs,
90    reason = "This comes from the JSON-RPC specification itself"
91)]
92pub struct JsonRpcBatch<M>(
93    #[cfg_attr(feature = "schemars", schemars(length(min = 1)))] Vec<JsonRpcMessage<M>>,
94);
95
96impl<M> JsonRpcBatch<M> {
97    /// Creates a non-empty JSON-RPC batch.
98    ///
99    /// Returns an error if `messages` is empty, because JSON-RPC 2.0 treats an
100    /// empty batch array as an invalid request.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`EmptyJsonRpcBatch`] when `messages` is empty.
105    pub fn new(messages: Vec<JsonRpcMessage<M>>) -> Result<Self, EmptyJsonRpcBatch> {
106        if messages.is_empty() {
107            Err(EmptyJsonRpcBatch)
108        } else {
109            Ok(Self(messages))
110        }
111    }
112
113    /// Returns the messages in this batch.
114    #[must_use]
115    pub fn as_slice(&self) -> &[JsonRpcMessage<M>] {
116        &self.0
117    }
118
119    /// Consumes this batch and returns its messages.
120    #[must_use]
121    pub fn into_vec(self) -> Vec<JsonRpcMessage<M>> {
122        self.0
123    }
124}
125
126impl<M> TryFrom<Vec<JsonRpcMessage<M>>> for JsonRpcBatch<M> {
127    type Error = EmptyJsonRpcBatch;
128
129    fn try_from(messages: Vec<JsonRpcMessage<M>>) -> Result<Self, Self::Error> {
130        Self::new(messages)
131    }
132}
133
134impl<'de, M> Deserialize<'de> for JsonRpcBatch<M>
135where
136    M: Deserialize<'de>,
137{
138    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
139    where
140        D: serde::Deserializer<'de>,
141    {
142        let messages = Vec::<JsonRpcMessage<M>>::deserialize(deserializer)?;
143        Self::new(messages).map_err(serde::de::Error::custom)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    use crate::v1::{
152        AgentNotification, CancelNotification, ClientNotification, ContentBlock, ContentChunk,
153        SessionId, SessionNotification, SessionUpdate, TextContent,
154    };
155    use serde_json::{Value, json};
156
157    #[test]
158    fn batch_deserialization_requires_at_least_one_message() {
159        let err = serde_json::from_value::<JsonRpcBatch<Notification<ClientNotification>>>(
160            Value::Array(Vec::new()),
161        )
162        .unwrap_err();
163        assert!(err.to_string().contains("at least one message"));
164    }
165
166    #[test]
167    fn batch_serialization_round_trips_non_empty_messages() {
168        let notification = JsonRpcMessage::wrap(Notification {
169            method: "cancel".into(),
170            params: Some(ClientNotification::CancelNotification(CancelNotification {
171                session_id: SessionId("test-123".into()),
172                meta: None,
173            })),
174        });
175
176        let batch = JsonRpcBatch::new(vec![notification]).unwrap();
177        let serialized = serde_json::to_value(&batch).unwrap();
178        assert_eq!(
179            serialized,
180            json!([{
181                "jsonrpc": "2.0",
182                "method": "cancel",
183                "params": {
184                    "sessionId": "test-123"
185                },
186            }])
187        );
188
189        let deserialized =
190            serde_json::from_value::<JsonRpcBatch<Notification<ClientNotification>>>(serialized)
191                .unwrap();
192        assert_eq!(deserialized.as_slice().len(), 1);
193        assert_eq!(deserialized.as_slice()[0].inner().method.as_ref(), "cancel");
194    }
195
196    #[test]
197    fn notification_wire_format() {
198        // Test client -> agent notification wire format
199        let outgoing_msg = JsonRpcMessage::wrap(Notification {
200            method: "cancel".into(),
201            params: Some(ClientNotification::CancelNotification(CancelNotification {
202                session_id: SessionId("test-123".into()),
203                meta: None,
204            })),
205        });
206
207        let serialized: Value = serde_json::to_value(&outgoing_msg).unwrap();
208        assert_eq!(
209            serialized,
210            json!({
211                "jsonrpc": "2.0",
212                "method": "cancel",
213                "params": {
214                    "sessionId": "test-123"
215                },
216            })
217        );
218
219        // Test agent -> client notification wire format
220        let outgoing_msg = JsonRpcMessage::wrap(Notification {
221            method: "sessionUpdate".into(),
222            params: Some(AgentNotification::SessionNotification(
223                SessionNotification {
224                    session_id: SessionId("test-456".into()),
225                    update: SessionUpdate::AgentMessageChunk(ContentChunk {
226                        content: ContentBlock::Text(TextContent {
227                            annotations: None,
228                            text: "Hello".to_string(),
229                            meta: None,
230                        }),
231                        message_id: None,
232                        meta: None,
233                    }),
234                    meta: None,
235                },
236            )),
237        });
238
239        let serialized: Value = serde_json::to_value(&outgoing_msg).unwrap();
240        assert_eq!(
241            serialized,
242            json!({
243                "jsonrpc": "2.0",
244                "method": "sessionUpdate",
245                "params": {
246                    "sessionId": "test-456",
247                    "update": {
248                        "sessionUpdate": "agent_message_chunk",
249                        "content": {
250                            "type": "text",
251                            "text": "Hello"
252                        }
253                    }
254                }
255            })
256        );
257    }
258}