use crate::envelope::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
use crate::error::A2aError;
use crate::types::{CancelTaskParams, GetTaskParams, Message, SendMessageResult, Task};
use bytes::Bytes;
use klieo_core::{DurableName, Headers, Pubsub};
use serde::de::DeserializeOwned;
use serde_json::{json, Value};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio_stream::StreamExt;
const JSONRPC_VERSION: &str = "2.0";
const A2A_VERSION: &str = "1.0";
static CLIENT_INSTANCE_SEQ: AtomicU64 = AtomicU64::new(0);
pub struct A2aClient {
transport: Arc<dyn Pubsub>,
app_prefix: String,
reply_subject: String,
durable: DurableName,
next_id: AtomicU64,
in_flight: tokio::sync::Mutex<()>,
}
impl A2aClient {
pub fn new(transport: Arc<dyn Pubsub>, app_prefix: impl Into<String>) -> Self {
let app_prefix = app_prefix.into();
let instance = CLIENT_INSTANCE_SEQ.fetch_add(1, Ordering::Relaxed);
Self {
reply_subject: format!("{app_prefix}.a2a.reply.{instance}"),
durable: DurableName::new(format!("a2a-client-{instance}")),
app_prefix,
transport,
next_id: AtomicU64::new(1),
in_flight: tokio::sync::Mutex::new(()),
}
}
pub async fn send_message(
&self,
agent_id: &str,
message: Message,
timeout: Duration,
) -> Result<SendMessageResult, A2aError> {
let params = json!({ "message": message });
let response = self
.request_one(agent_id, "SendMessage", params, timeout)
.await?;
decode_result(response)
}
pub async fn get_task(
&self,
agent_id: &str,
params: GetTaskParams,
timeout: Duration,
) -> Result<Task, A2aError> {
let raw = serde_json::to_value(params)?;
let response = self.request_one(agent_id, "GetTask", raw, timeout).await?;
decode_result(response)
}
pub async fn cancel_task(
&self,
agent_id: &str,
params: CancelTaskParams,
timeout: Duration,
) -> Result<Task, A2aError> {
let raw = serde_json::to_value(params)?;
let response = self
.request_one(agent_id, "CancelTask", raw, timeout)
.await?;
decode_result(response)
}
async fn request_one(
&self,
agent_id: &str,
method: &str,
params: Value,
timeout: Duration,
) -> Result<JsonRpcResponse, A2aError> {
let _guard = self.in_flight.lock().await;
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let mut reply_stream = self
.transport
.subscribe(&self.reply_subject, self.durable.clone())
.await?;
let request = JsonRpcRequest {
jsonrpc: JSONRPC_VERSION.to_string(),
id: json!(id),
method: method.to_string(),
params,
};
self.publish_request(agent_id, &request).await?;
let reply = await_reply(&mut reply_stream, timeout, id).await?;
check_for_error(reply)
}
async fn publish_request(
&self,
agent_id: &str,
request: &JsonRpcRequest,
) -> Result<(), A2aError> {
let subject = format!("{}.a2a.{agent_id}.rpc", self.app_prefix);
let payload = Bytes::from(serde_json::to_vec(request)?);
let mut headers = Headers::new();
headers.insert("Reply-To".into(), self.reply_subject.clone());
headers.insert("A2A-Version".into(), A2A_VERSION.into());
self.transport.publish(&subject, payload, headers).await?;
Ok(())
}
}
async fn await_reply(
stream: &mut klieo_core::MsgStream,
timeout: Duration,
id: u64,
) -> Result<JsonRpcResponse, A2aError> {
let matched = tokio::time::timeout(timeout, async {
loop {
let msg = match stream.next().await {
Some(Ok(msg)) => msg,
Some(Err(bus_err)) => return Err(A2aError::Bus(bus_err)),
None => return Err(A2aError::Server("reply stream closed before reply".into())),
};
let response: JsonRpcResponse = serde_json::from_slice(&msg.payload)?;
if response.id == json!(id) {
return Ok(response);
}
}
})
.await;
matched.unwrap_or_else(|_| Err(A2aError::Timeout(timeout)))
}
fn check_for_error(response: JsonRpcResponse) -> Result<JsonRpcResponse, A2aError> {
if let Some(JsonRpcError { code, message, .. }) = response.error {
return Err(A2aError::Rpc { code, message });
}
Ok(response)
}
fn decode_result<T: DeserializeOwned>(response: JsonRpcResponse) -> Result<T, A2aError> {
let result = response
.result
.ok_or_else(|| A2aError::Server("reply had neither result nor error".into()))?;
Ok(serde_json::from_value(result)?)
}