agent_client_protocol_schema/
rpc.rs1use std::sync::Arc;
7
8use derive_more::Display;
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12pub use crate::v1::{Request, RequestId, Response};
14
15#[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 pub method: Arc<str>,
27 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#[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 #[must_use]
55 pub fn wrap(message: M) -> Self {
56 Self {
57 jsonrpc: JsonRpcVersion::V2,
58 message,
59 }
60 }
61
62 #[must_use]
64 pub fn inner(&self) -> &M {
65 &self.message
66 }
67
68 #[must_use]
70 pub fn into_inner(self) -> M {
71 self.message
72 }
73}
74
75#[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#[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 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 #[must_use]
115 pub fn as_slice(&self) -> &[JsonRpcMessage<M>] {
116 &self.0
117 }
118
119 #[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 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 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}