use crate::core::jsonrpc::{
JsonRpcRequest, JsonRpcResponse, METHOD_TASKS_CANCEL, METHOD_TASKS_SEND,
};
use crate::core::{AgentCard, TaskRequest, TaskResponse, TransportError};
pub struct A2AClient {
base_url: String,
http: reqwest::Client,
#[cfg(feature = "transport-log")]
logger: Option<super::super::log::TransportLogger>,
}
impl A2AClient {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
http: reqwest::Client::new(),
#[cfg(feature = "transport-log")]
logger: None,
}
}
#[must_use]
#[cfg(feature = "transport-log")]
pub fn with_logger(mut self, logger: super::super::log::TransportLogger) -> Self {
self.logger = Some(logger);
self
}
#[cfg(feature = "transport-log")]
pub fn set_logger(&mut self, logger: &super::super::log::TransportLogger) {
self.logger = Some(logger.clone());
}
pub async fn agent_card(&self) -> Result<AgentCard, TransportError> {
let url = format!("{}/.well-known/agent.json", self.base_url);
let resp = self
.http
.get(&url)
.send()
.await
.map_err(|e| TransportError::Request(e.to_string()))?;
let card: AgentCard = resp
.json()
.await
.map_err(|e| TransportError::Request(e.to_string()))?;
Ok(card)
}
pub async fn send_task(&self, request: TaskRequest) -> Result<TaskResponse, TransportError> {
#[cfg(feature = "transport-log")]
let (log_task_id, log_session_id, log_start) = (
request.id.clone(),
request.session_id.clone(),
std::time::Instant::now(),
);
let result = self.do_send_task(request).await;
#[cfg(feature = "transport-log")]
if let Some(ref logger) = self.logger {
#[allow(clippy::cast_possible_truncation)]
let duration_us = log_start.elapsed().as_micros() as u64;
let llm_us = result.as_ref().ok().and_then(|resp| {
let meta = resp.metadata.as_ref()?;
meta.get("llm_us")
.and_then(|v| v.parse::<u64>().ok())
.or_else(|| {
meta.get("llm_ms")
.and_then(|v| v.parse::<u64>().ok())
.map(|ms| ms * 1_000)
})
});
let transport_us = llm_us.map(|l| duration_us.saturating_sub(l));
let (status, error) = match &result {
Ok(_) => ("ok", None),
Err(e) => ("error", Some(e.to_string())),
};
logger.record(super::super::log::LogEntry {
ts: super::super::log::now_iso8601(),
transport: super::super::log::TransportKind::A2a,
direction: super::super::log::Direction::Outbound,
task_id: log_task_id,
session_id: log_session_id,
duration_us,
llm_us,
transport_us,
status,
error,
payload_bytes: None,
});
}
result
}
async fn do_send_task(&self, request: TaskRequest) -> Result<TaskResponse, TransportError> {
let params =
serde_json::to_value(&request).map_err(|e| TransportError::Request(e.to_string()))?;
let rpc_request = JsonRpcRequest::new(METHOD_TASKS_SEND, params);
let rpc_response: JsonRpcResponse = self
.http
.post(&self.base_url)
.json(&rpc_request)
.send()
.await
.map_err(|e| TransportError::Request(e.to_string()))?
.json()
.await
.map_err(|e| TransportError::Request(e.to_string()))?;
if let Some(err) = rpc_response.error {
return Err(TransportError::Request(format!(
"JSON-RPC error {}: {}",
err.code, err.message
)));
}
let result = rpc_response
.result
.ok_or_else(|| TransportError::Request("empty result".into()))?;
let task_response: TaskResponse =
serde_json::from_value(result).map_err(|e| TransportError::Request(e.to_string()))?;
Ok(task_response)
}
pub async fn cancel_task(&self, task_id: &str) -> Result<(), TransportError> {
let params = serde_json::json!({ "id": task_id });
let rpc_request = JsonRpcRequest::new(METHOD_TASKS_CANCEL, params);
let rpc_response: JsonRpcResponse = self
.http
.post(&self.base_url)
.json(&rpc_request)
.send()
.await
.map_err(|e| TransportError::Request(e.to_string()))?
.json()
.await
.map_err(|e| TransportError::Request(e.to_string()))?;
if let Some(err) = rpc_response.error {
return Err(TransportError::Request(format!(
"JSON-RPC error {}: {}",
err.code, err.message
)));
}
Ok(())
}
}