a2a_protocol_client/methods/
send_message.rs1use a2a_protocol_types::params::SendMessageConfiguration;
9use a2a_protocol_types::{MessageSendParams, SendMessageResponse};
10
11use crate::client::A2aClient;
12use crate::config::ClientConfig;
13use crate::error::{ClientError, ClientResult};
14use crate::interceptor::{ClientRequest, ClientResponse};
15use crate::streaming::EventStream;
16
17fn apply_client_config(params: &mut MessageSendParams, config: &ClientConfig) {
22 let cfg = params
23 .configuration
24 .get_or_insert_with(SendMessageConfiguration::default);
25
26 if cfg.return_immediately.is_none() && config.return_immediately {
28 cfg.return_immediately = Some(true);
29 }
30 if cfg.history_length.is_none()
31 && let Some(hl) = config.history_length
32 {
33 cfg.history_length = Some(hl);
34 }
35 if cfg.accepted_output_modes.is_empty() && !config.accepted_output_modes.is_empty() {
36 cfg.accepted_output_modes
37 .clone_from(&config.accepted_output_modes);
38 }
39 if params.tenant.is_none() {
41 params.tenant.clone_from(&config.tenant);
42 }
43}
44
45impl A2aClient {
46 pub async fn send_message(
78 &self,
79 mut params: MessageSendParams,
80 ) -> ClientResult<SendMessageResponse> {
81 const METHOD: &str = "SendMessage";
82
83 apply_client_config(&mut params, &self.config);
84
85 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
86
87 let mut req = ClientRequest::new(METHOD, params_value);
88 self.interceptors.run_before(&mut req).await?;
89
90 let result = self.send_intercepted(METHOD, &mut req).await?;
91
92 let resp = ClientResponse {
93 method: METHOD.to_owned(),
94 result,
95 status_code: 200,
96 };
97 self.interceptors.run_after(&resp).await?;
98
99 serde_json::from_value::<SendMessageResponse>(resp.result)
100 .map_err(ClientError::Serialization)
101 }
102
103 pub async fn stream_message(&self, mut params: MessageSendParams) -> ClientResult<EventStream> {
113 const METHOD: &str = "SendStreamingMessage";
114
115 apply_client_config(&mut params, &self.config);
116
117 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
118
119 let mut req = ClientRequest::new(METHOD, params_value);
120 self.interceptors.run_before(&mut req).await?;
121
122 let stream = self.send_streaming_intercepted(METHOD, &mut req).await?;
123 let stream = self.configure_stream(stream);
124
125 let resp = ClientResponse {
133 method: METHOD.to_owned(),
134 result: serde_json::Value::Null,
135 status_code: stream.status_code(),
136 };
137 self.interceptors.run_after(&resp).await?;
138
139 Ok(stream)
140 }
141}
142
143#[cfg(test)]
146mod tests {
147 use super::*;
148 use a2a_protocol_types::{Message, MessageId, MessageRole, Part};
149
150 fn make_params() -> MessageSendParams {
151 MessageSendParams {
152 tenant: None,
153 message: Message {
154 id: MessageId::new("msg-1"),
155 role: MessageRole::User,
156 parts: vec![Part::text("test")],
157 task_id: None,
158 context_id: None,
159 reference_task_ids: None,
160 extensions: None,
161 metadata: None,
162 },
163 configuration: None,
164 metadata: None,
165 }
166 }
167
168 #[test]
169 fn apply_config_sets_return_immediately_when_absent() {
170 let config = ClientConfig {
171 return_immediately: true,
172 ..ClientConfig::default()
173 };
174
175 let mut params = make_params();
176 apply_client_config(&mut params, &config);
177
178 let cfg = params.configuration.unwrap();
179 assert_eq!(cfg.return_immediately, Some(true));
180 }
181
182 #[test]
183 fn apply_config_does_not_override_per_request_return_immediately() {
184 let config = ClientConfig {
185 return_immediately: true,
186 ..ClientConfig::default()
187 };
188
189 let mut params = make_params();
190 params.configuration = Some(SendMessageConfiguration {
191 return_immediately: Some(false),
192 ..Default::default()
193 });
194 apply_client_config(&mut params, &config);
195
196 let cfg = params.configuration.unwrap();
197 assert_eq!(
198 cfg.return_immediately,
199 Some(false),
200 "per-request value should take precedence"
201 );
202 }
203
204 #[test]
205 fn apply_config_does_not_set_return_immediately_when_config_false() {
206 let config = ClientConfig::default(); let mut params = make_params();
209 apply_client_config(&mut params, &config);
210
211 let cfg = params.configuration.unwrap();
212 assert_eq!(
213 cfg.return_immediately, None,
214 "should not set return_immediately when config is false"
215 );
216 }
217
218 #[test]
219 fn apply_config_sets_history_length_when_absent() {
220 let config = ClientConfig {
221 history_length: Some(10),
222 ..ClientConfig::default()
223 };
224
225 let mut params = make_params();
226 apply_client_config(&mut params, &config);
227
228 let cfg = params.configuration.unwrap();
229 assert_eq!(cfg.history_length, Some(10));
230 }
231
232 #[test]
233 fn apply_config_does_not_override_per_request_history_length() {
234 let config = ClientConfig {
235 history_length: Some(10),
236 ..ClientConfig::default()
237 };
238
239 let mut params = make_params();
240 params.configuration = Some(SendMessageConfiguration {
241 history_length: Some(5),
242 ..Default::default()
243 });
244 apply_client_config(&mut params, &config);
245
246 let cfg = params.configuration.unwrap();
247 assert_eq!(cfg.history_length, Some(5));
248 }
249
250 #[test]
251 fn apply_config_sets_accepted_output_modes_when_empty() {
252 let config = ClientConfig {
253 accepted_output_modes: vec!["audio/wav".into()],
254 ..ClientConfig::default()
255 };
256
257 let mut params = make_params();
258 params.configuration = Some(SendMessageConfiguration {
261 accepted_output_modes: vec![],
262 task_push_notification_config: None,
263 history_length: None,
264 return_immediately: None,
265 });
266 apply_client_config(&mut params, &config);
267
268 let cfg = params.configuration.unwrap();
269 assert_eq!(cfg.accepted_output_modes, vec!["audio/wav"]);
270 }
271
272 #[test]
273 fn apply_config_does_not_override_per_request_output_modes() {
274 let config = ClientConfig {
275 accepted_output_modes: vec!["text/plain".into()],
276 ..ClientConfig::default()
277 };
278
279 let mut params = make_params();
280 params.configuration = Some(SendMessageConfiguration {
281 accepted_output_modes: vec!["application/json".into()],
282 ..Default::default()
283 });
284 apply_client_config(&mut params, &config);
285
286 let cfg = params.configuration.unwrap();
287 assert_eq!(cfg.accepted_output_modes, vec!["application/json"]);
288 }
289
290 #[test]
291 fn apply_config_no_op_when_config_has_no_overrides() {
292 let config = ClientConfig::default();
293 let mut params = make_params();
297 params.configuration = Some(SendMessageConfiguration::default());
299 apply_client_config(&mut params, &config);
300
301 let cfg = params.configuration.unwrap();
302 assert_eq!(cfg.return_immediately, None);
304 assert_eq!(cfg.history_length, None);
306 }
307
308 #[tokio::test]
309 async fn stream_message_applies_config_and_calls_transport() {
310 use std::collections::HashMap;
311 use std::future::Future;
312 use std::pin::Pin;
313
314 use crate::ClientBuilder;
315 use crate::error::{ClientError, ClientResult};
316 use crate::streaming::EventStream;
317 use crate::transport::Transport;
318
319 struct StreamCapture;
322
323 impl Transport for StreamCapture {
324 fn send_request<'a>(
325 &'a self,
326 _method: &'a str,
327 _params: serde_json::Value,
328 _extra_headers: &'a HashMap<String, String>,
329 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>>
330 {
331 Box::pin(async move { Ok(serde_json::Value::Null) })
332 }
333
334 fn send_streaming_request<'a>(
335 &'a self,
336 _method: &'a str,
337 _params: serde_json::Value,
338 _extra_headers: &'a HashMap<String, String>,
339 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
340 Box::pin(
341 async move { Err(ClientError::Transport("mock: streaming called".into())) },
342 )
343 }
344 }
345
346 let client = ClientBuilder::new("http://localhost:8080")
347 .with_custom_transport(StreamCapture)
348 .with_return_immediately(true)
349 .build()
350 .expect("build");
351
352 let params = make_params();
353 let err = client.stream_message(params).await.unwrap_err();
354 assert!(
355 matches!(err, ClientError::Transport(ref msg) if msg.contains("streaming called")),
356 "expected Transport error, got {err:?}"
357 );
358 }
359
360 #[allow(clippy::too_many_lines)]
362 #[allow(clippy::unused_async_trait_impl)]
365 #[tokio::test]
366 async fn stream_message_calls_after_interceptor() {
367 use std::collections::HashMap;
368 use std::future::Future;
369 use std::pin::Pin;
370 use std::sync::Arc;
371 use std::sync::atomic::{AtomicUsize, Ordering};
372
373 use crate::ClientBuilder;
374 use crate::error::ClientResult;
375 use crate::interceptor::{CallInterceptor, ClientRequest, ClientResponse};
376 use crate::streaming::EventStream;
377 use crate::transport::Transport;
378
379 struct StreamingOkTransport;
380
381 impl Transport for StreamingOkTransport {
382 fn send_request<'a>(
383 &'a self,
384 _method: &'a str,
385 _params: serde_json::Value,
386 _extra_headers: &'a HashMap<String, String>,
387 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>>
388 {
389 Box::pin(async move { Ok(serde_json::Value::Null) })
390 }
391
392 fn send_streaming_request<'a>(
393 &'a self,
394 _method: &'a str,
395 _params: serde_json::Value,
396 _extra_headers: &'a HashMap<String, String>,
397 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
398 Box::pin(async move {
399 let (tx, rx) = tokio::sync::mpsc::channel(8);
400 drop(tx);
401 Ok(EventStream::new(rx))
402 })
403 }
404 }
405
406 struct CountingInterceptor {
407 before_count: Arc<AtomicUsize>,
408 after_count: Arc<AtomicUsize>,
409 }
410
411 impl CallInterceptor for CountingInterceptor {
412 async fn before<'a>(&'a self, _req: &'a mut ClientRequest) -> ClientResult<()> {
413 self.before_count.fetch_add(1, Ordering::SeqCst);
414 Ok(())
415 }
416 async fn after<'a>(&'a self, _resp: &'a ClientResponse) -> ClientResult<()> {
417 self.after_count.fetch_add(1, Ordering::SeqCst);
418 Ok(())
419 }
420 }
421
422 let before = Arc::new(AtomicUsize::new(0));
423 let after = Arc::new(AtomicUsize::new(0));
424 let interceptor = CountingInterceptor {
425 before_count: Arc::clone(&before),
426 after_count: Arc::clone(&after),
427 };
428
429 let client = ClientBuilder::new("http://localhost:8080")
430 .with_custom_transport(StreamingOkTransport)
431 .with_interceptor(interceptor)
432 .build()
433 .expect("build");
434
435 let result = client.stream_message(make_params()).await;
436 assert!(result.is_ok(), "stream_message should succeed");
437 assert_eq!(before.load(Ordering::SeqCst), 1, "before should be called");
438 assert_eq!(
439 after.load(Ordering::SeqCst),
440 1,
441 "after should be called for streaming"
442 );
443 }
444
445 #[test]
446 fn apply_config_does_not_set_modes_when_config_modes_empty() {
447 let config = ClientConfig {
448 accepted_output_modes: vec![],
449 ..ClientConfig::default()
450 };
451
452 let mut params = make_params();
453 params.configuration = Some(SendMessageConfiguration {
455 accepted_output_modes: vec![],
456 task_push_notification_config: None,
457 history_length: None,
458 return_immediately: None,
459 });
460 apply_client_config(&mut params, &config);
461
462 let cfg = params.configuration.unwrap();
463 assert!(
464 cfg.accepted_output_modes.is_empty(),
465 "should not set modes when config modes are empty"
466 );
467 }
468}