1pub(crate) mod signing;
34mod sse;
35
36use std::sync::atomic::{AtomicU64, Ordering};
37use std::time::Duration;
38
39use super::protocol::{
40 A2AErrorData, A2AMessage, A2ARequest, A2AResponse, A2ATask, A2ATaskDetails, A2ATaskResult,
41 AgentCard, TaskStatus, TraceContext,
42};
43
44pub use signing::{
45 canonical_json, sign_agent_card, sign_card_jws, verify_card_jws, verify_card_signature,
46};
47pub use sse::A2ASseStream;
48
49#[derive(Debug, thiserror::Error)]
51#[non_exhaustive]
52pub enum A2AError {
53 #[error("HTTP error: {0}")]
55 Http(String),
56
57 #[error("Parse error: {0}")]
59 Parse(String),
60
61 #[error("API error [{code}]: {message}")]
63 Api {
64 code: i32,
66 message: String,
68 },
69
70 #[error("Timeout: {0}")]
72 Timeout(String),
73
74 #[error("Agent card signature: {0}")]
77 Signature(String),
78
79 #[error("Task {task_id} requires more input: {prompt}")]
83 InputRequired {
84 task_id: String,
86 prompt: String,
88 },
89}
90
91impl From<reqwest::Error> for A2AError {
92 fn from(err: reqwest::Error) -> Self {
93 if err.is_timeout() {
94 A2AError::Timeout(err.to_string())
95 } else {
96 A2AError::Http(err.to_string())
97 }
98 }
99}
100
101impl From<A2AErrorData> for A2AError {
102 fn from(err: A2AErrorData) -> Self {
103 A2AError::Api {
104 code: err.code,
105 message: err.message,
106 }
107 }
108}
109
110pub struct A2AClient {
112 base_url: String,
114 http: reqwest::Client,
116 stream_http: reqwest::Client,
120 next_id: AtomicU64,
122 auth_token: Option<String>,
124 trace_id: Option<String>,
126 trace_context: Option<TraceContext>,
128 card_secret: Option<Vec<u8>>,
130 require_card_signature: bool,
132}
133
134impl A2AClient {
135 pub fn new(base_url: impl Into<String>) -> Result<Self, A2AError> {
144 let base_url = base_url.into();
145 if !base_url.starts_with("https://") {
146 log::warn!(
147 "A2A client connecting over non-HTTPS URL: {} (use TLS in production)",
148 base_url
149 );
150 }
151 let http = reqwest::Client::builder()
152 .timeout(Duration::from_secs(30))
153 .connect_timeout(Duration::from_secs(10))
154 .build()
155 .map_err(|e| A2AError::Http(format!("failed to build HTTP client: {e}")))?;
156 let stream_http = reqwest::Client::builder()
161 .connect_timeout(Duration::from_secs(10))
162 .build()
163 .unwrap_or_else(|_| http.clone());
164 Ok(Self::with_http_client(base_url, http).with_stream_client(stream_http))
165 }
166
167 pub fn with_http_client(base_url: impl Into<String>, http: reqwest::Client) -> Self {
173 Self {
174 base_url: base_url.into().trim_end_matches('/').to_string(),
175 stream_http: http.clone(),
176 http,
177 next_id: AtomicU64::new(1),
178 auth_token: None,
179 trace_id: None,
180 trace_context: None,
181 card_secret: None,
182 require_card_signature: false,
183 }
184 }
185
186 fn with_stream_client(mut self, stream_http: reqwest::Client) -> Self {
189 self.stream_http = stream_http;
190 self
191 }
192
193 pub fn builder(base_url: impl Into<String>) -> A2AClientBuilder {
195 A2AClientBuilder::new(base_url)
196 }
197
198 fn alloc_id(&self) -> u64 {
200 self.next_id.fetch_add(1, Ordering::SeqCst)
201 }
202
203 pub async fn get_agent_card(&self) -> Result<AgentCard, A2AError> {
216 let url = format!("{}/.well-known/agent-card.json", self.base_url);
217 let resp = self.with_traceparent(self.http.get(&url)).send().await?;
218 let status = resp.status();
219 if !status.is_success() {
220 return Err(A2AError::Http(format!(
221 "Agent card request failed with status {}",
222 status
223 )));
224 }
225 let card: AgentCard = resp
226 .json()
227 .await
228 .map_err(|e| A2AError::Parse(format!("Failed to parse agent card: {}", e)))?;
229
230 if !card.url.trim_end_matches('/').is_empty()
232 && card.url.trim_end_matches('/') != self.base_url.trim_end_matches('/')
233 {
234 log::warn!(
235 "Agent card URL mismatch: card.url={}, base_url={}",
236 card.url,
237 self.base_url
238 );
239 }
240
241 if card.signature.is_some() {
243 match &self.card_secret {
244 Some(secret) => {
245 verify_card_signature(&card, secret)?;
246 }
247 None if self.require_card_signature => {
248 return Err(A2AError::Signature(
249 "agent card is signed but no verification secret is configured".to_string(),
250 ));
251 }
252 None => {
253 log::warn!(
254 "agent card is signed but no verification secret is configured; \
255 skipping signature verification"
256 );
257 }
258 }
259 }
260
261 Ok(card)
262 }
263
264 pub async fn send_task(&self, message: A2AMessage) -> Result<A2ATask, A2AError> {
268 let id = self.alloc_id();
269 let req = self.with_context(A2ARequest::send_task(id, &message));
270 self.send_task_req(req).await
271 }
272
273 pub async fn send_task_with_message_id(
276 &self,
277 message: A2AMessage,
278 message_id: &str,
279 ) -> Result<A2ATask, A2AError> {
280 let id = self.alloc_id();
281 let req = self.with_context(A2ARequest::send_task_with_message_id(
282 id, &message, message_id,
283 ));
284 self.send_task_req(req).await
285 }
286
287 pub async fn resume_task(
292 &self,
293 task_id: &str,
294 message: A2AMessage,
295 ) -> Result<A2ATask, A2AError> {
296 let id = self.alloc_id();
297 let req = self.with_context(A2ARequest::continue_task(id, task_id, &message));
298 self.send_task_req(req).await
299 }
300
301 pub async fn get_task(&self, task_id: &str) -> Result<A2ATask, A2AError> {
303 let id = self.alloc_id();
304 let req = self.with_context(A2ARequest::get_task(id, task_id));
305 let resp = self.post_request(req).await?;
306 self.task_from_response(resp)
307 }
308
309 pub async fn get_task_details(&self, task_id: &str) -> Result<A2ATaskDetails, A2AError> {
311 let id = self.alloc_id();
312 let req = self.with_context(A2ARequest::get_task(id, task_id));
313 let resp = self.post_request(req).await?;
314
315 let result = resp.into_result().map_err(A2AError::from)?;
316 let task: A2ATask = result
317 .get("task")
318 .ok_or_else(|| A2AError::Parse("Missing 'task' in response".to_string()))
319 .and_then(|v| {
320 serde_json::from_value(v.clone())
321 .map_err(|e| A2AError::Parse(format!("Failed to parse task: {}", e)))
322 })?;
323 let task_result: Option<A2ATaskResult> = result
324 .get("result")
325 .map(|v| {
326 serde_json::from_value(v.clone())
327 .map_err(|e| A2AError::Parse(format!("Failed to parse task result: {}", e)))
328 })
329 .transpose()?;
330 let error = result
331 .get("error")
332 .and_then(|v| v.as_str())
333 .map(|s| s.to_string());
334
335 Ok(A2ATaskDetails {
336 task,
337 result: task_result,
338 error,
339 })
340 }
341
342 pub async fn cancel_task(&self, task_id: &str) -> Result<A2ATask, A2AError> {
344 let id = self.alloc_id();
345 let req = self.with_context(A2ARequest::cancel_task(id, task_id));
346 let resp = self.post_request(req).await?;
347 self.task_from_response(resp)
348 }
349
350 fn with_context(&self, req: A2ARequest) -> A2ARequest {
352 match &self.trace_id {
353 Some(tid) => req.with_trace_id(tid.as_str()),
354 None => req,
355 }
356 }
357
358 fn with_traceparent(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
361 match &self.trace_context {
362 Some(ctx) => request.header("traceparent", ctx.to_traceparent()),
363 None => request,
364 }
365 }
366
367 async fn send_task_req(&self, req: A2ARequest) -> Result<A2ATask, A2AError> {
369 let resp = self.post_request(req).await?;
370 self.task_from_response(resp)
371 }
372
373 fn task_from_response(&self, resp: A2AResponse) -> Result<A2ATask, A2AError> {
375 let result = resp.into_result().map_err(A2AError::from)?;
376 result
377 .get("task")
378 .ok_or_else(|| A2AError::Parse("Missing 'task' in response".to_string()))
379 .and_then(|v| {
380 serde_json::from_value(v.clone())
381 .map_err(|e| A2AError::Parse(format!("Failed to parse task: {}", e)))
382 })
383 }
384
385 pub async fn send_task_and_wait(
393 &self,
394 message: A2AMessage,
395 timeout: Duration,
396 ) -> Result<A2ATaskResult, A2AError> {
397 let task = self.send_task(message).await?;
398 self.wait_for_task(&task.id, timeout).await
399 }
400
401 pub async fn send_task_and_wait_with_message_id(
404 &self,
405 message: A2AMessage,
406 message_id: &str,
407 timeout: Duration,
408 ) -> Result<A2ATaskResult, A2AError> {
409 let task = self.send_task_with_message_id(message, message_id).await?;
410 self.wait_for_task(&task.id, timeout).await
411 }
412
413 async fn wait_for_task(
416 &self,
417 task_id: &str,
418 timeout: Duration,
419 ) -> Result<A2ATaskResult, A2AError> {
420 let start = std::time::Instant::now();
421 let poll_interval = Duration::from_secs(1);
422
423 loop {
424 let mut details = None;
428 let mut last_err: Option<A2AError> = None;
429 for attempt in 0..3u32 {
430 match self.get_task_details(task_id).await {
431 Ok(d) => {
432 details = Some(d);
433 break;
434 }
435 Err(e) => {
436 last_err = Some(e);
437 if attempt < 2 {
438 tokio::time::sleep(Duration::from_millis(100 << attempt)).await;
439 }
440 }
441 }
442 }
443 let details = match details {
444 Some(d) => d,
445 None => {
446 return Err(last_err.unwrap_or_else(|| {
447 A2AError::Http("task poll failed without an error".to_string())
448 }))
449 }
450 };
451 match details.task.status {
452 TaskStatus::Completed => {
453 return details.result.ok_or_else(|| {
454 A2AError::Parse(format!("Task {} completed without a result", task_id))
455 })
456 }
457 TaskStatus::Failed => {
458 return Err(A2AError::Api {
459 code: -32000,
460 message: details.error.unwrap_or_else(|| "Task failed".to_string()),
461 })
462 }
463 TaskStatus::Cancelled => {
464 return Err(A2AError::Api {
465 code: -32000,
466 message: format!("Task {} was cancelled", task_id),
467 })
468 }
469 TaskStatus::Rejected => {
470 return Err(A2AError::Api {
471 code: -32000,
472 message: format!("Task {} was rejected", task_id),
473 })
474 }
475 TaskStatus::Expired => {
476 return Err(A2AError::Api {
477 code: -32000,
478 message: format!("Task {} expired", task_id),
479 })
480 }
481 TaskStatus::AuthRequired => {
482 return Err(A2AError::Api {
483 code: 401,
484 message: format!("Task {} requires authentication", task_id),
485 })
486 }
487 TaskStatus::InputRequired => {
488 return Err(A2AError::InputRequired {
491 task_id: task_id.to_string(),
492 prompt: details
493 .error
494 .unwrap_or_else(|| "Input required".to_string()),
495 });
496 }
497 TaskStatus::Submitted | TaskStatus::Working => {
498 if start.elapsed() > timeout {
499 return Err(A2AError::Timeout(format!(
500 "Task {} did not complete within {:?}",
501 task_id, timeout
502 )));
503 }
504 tokio::time::sleep(poll_interval).await;
505 }
506 }
507 }
508 }
509
510 pub async fn post_request(&self, req: A2ARequest) -> Result<A2AResponse, A2AError> {
512 let url = format!("{}/", self.base_url);
513 let mut request = self.with_traceparent(self.http.post(&url).json(&req));
514 if let Some(token) = &self.auth_token {
515 request = request.bearer_auth(token);
516 }
517 let resp = request.send().await?;
518 let status = resp.status();
519 if !status.is_success() {
520 if let Ok(a2a_resp) = resp.json::<A2AResponse>().await {
524 if let Some(err) = a2a_resp.error {
525 return Err(A2AError::from(err));
526 }
527 }
528 return Err(A2AError::Http(format!(
529 "A2A request failed with status {}",
530 status
531 )));
532 }
533 let a2a_resp: A2AResponse = resp
534 .json()
535 .await
536 .map_err(|e| A2AError::Parse(format!("Failed to parse A2A response: {}", e)))?;
537 Ok(a2a_resp)
538 }
539
540 pub async fn connect_sse(&self, sse_url: &str) -> Result<A2ASseStream, A2AError> {
547 let mut request = self.with_traceparent(self.stream_http.get(sse_url));
551 if let Some(token) = &self.auth_token {
552 request = request.bearer_auth(token);
553 }
554 let resp = request.send().await?;
555 let status = resp.status();
556 if !status.is_success() {
557 return Err(A2AError::Http(format!(
558 "SSE request failed with status {}",
559 status
560 )));
561 }
562 Ok(A2ASseStream::new(resp))
563 }
564
565 pub async fn send_task_streaming(
571 &self,
572 sse_url: &str,
573 message: A2AMessage,
574 ) -> Result<A2ASseStream, A2AError> {
575 let stream = self.connect_sse(sse_url).await?;
576 let _ = self.send_task(message).await?;
577 Ok(stream)
578 }
579}
580
581pub struct A2AClientBuilder {
583 base_url: String,
584 http_client: Option<reqwest::Client>,
585 bearer_token: Option<String>,
586 enforce_https: bool,
587 timeout: Duration,
588 connect_timeout: Duration,
589 trace_id: Option<String>,
590 trace_context: Option<TraceContext>,
591 card_secret: Option<Vec<u8>>,
592 require_card_signature: bool,
593}
594
595impl A2AClientBuilder {
596 pub fn new(base_url: impl Into<String>) -> Self {
598 Self {
599 base_url: base_url.into().trim_end_matches('/').to_string(),
600 http_client: None,
601 bearer_token: None,
602 enforce_https: false,
603 timeout: Duration::from_secs(30),
604 connect_timeout: Duration::from_secs(10),
605 trace_id: None,
606 trace_context: None,
607 card_secret: None,
608 require_card_signature: false,
609 }
610 }
611
612 pub fn http_client(mut self, client: reqwest::Client) -> Self {
614 self.http_client = Some(client);
615 self
616 }
617
618 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
620 self.bearer_token = Some(token.into());
621 self
622 }
623
624 pub fn enforce_https(mut self, enforce: bool) -> Self {
626 self.enforce_https = enforce;
627 self
628 }
629
630 pub fn timeout(mut self, timeout: Duration) -> Self {
632 self.timeout = timeout;
633 self
634 }
635
636 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
638 self.connect_timeout = timeout;
639 self
640 }
641
642 pub fn trace_id(mut self, trace_id: impl Into<String>) -> Self {
644 self.trace_id = Some(trace_id.into());
645 self
646 }
647
648 pub fn with_traceparent(mut self, context: TraceContext) -> Self {
654 self.trace_context = Some(context.clone());
655 self.trace_id = Some(context.trace_id.clone());
656 self
657 }
658
659 pub fn card_verification_secret(mut self, secret: impl Into<Vec<u8>>) -> Self {
662 self.card_secret = Some(secret.into());
663 self
664 }
665
666 pub fn require_card_signature(mut self, require: bool) -> Self {
672 self.require_card_signature = require;
673 self
674 }
675
676 pub fn build(self) -> Result<A2AClient, A2AError> {
678 if !self.base_url.starts_with("https://") {
679 if self.enforce_https {
680 return Err(A2AError::Http(format!(
681 "HTTPS is required for A2A, got insecure URL: {}",
682 self.base_url
683 )));
684 }
685 log::warn!(
686 "A2A client connecting over non-HTTPS URL: {} (use TLS in production)",
687 self.base_url
688 );
689 }
690 let (http, stream_http) = match self.http_client {
691 Some(client) => (client.clone(), client),
692 None => {
693 let http = reqwest::Client::builder()
694 .timeout(self.timeout)
695 .connect_timeout(self.connect_timeout)
696 .build()
697 .map_err(|e| A2AError::Http(format!("failed to build HTTP client: {}", e)))?;
698 let stream_http = reqwest::Client::builder()
701 .connect_timeout(self.connect_timeout)
702 .build()
703 .unwrap_or_else(|_| http.clone());
704 (http, stream_http)
705 }
706 };
707 Ok(A2AClient {
708 base_url: self.base_url,
709 http,
710 stream_http,
711 next_id: AtomicU64::new(1),
712 auth_token: self.bearer_token,
713 trace_id: self.trace_id,
714 trace_context: self.trace_context,
715 card_secret: self.card_secret,
716 require_card_signature: self.require_card_signature,
717 })
718 }
719}
720
721#[cfg(test)]
722mod tests;