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