use std::error::Error;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use protocol_droid::adapters::secondary::a2a::A2aMethod;
use protocol_droid::domain::a2a::agent_card::AgentCard;
use protocol_droid::domain::a2a::agent_card::JSONRPC;
use protocol_droid::domain::a2a::list_tasks::{ListTasksRequest, ListTasksResponse};
use protocol_droid::domain::a2a::message::{Message, MessageRole, Part, TextPart};
use protocol_droid::domain::a2a::push_notification::TaskPushNotificationConfig;
use protocol_droid::domain::a2a::push_notification_methods::{
CreateTaskPushNotificationConfigRequest, CreateTaskPushNotificationConfigResponse,
DeleteTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigResponse,
GetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigResponse,
ListTaskPushNotificationConfigsRequest, ListTaskPushNotificationConfigsResponse,
};
use protocol_droid::domain::a2a::send_message::{SendMessageRequest, SendMessageResponse};
use protocol_droid::domain::a2a::streaming_events::StreamingEvent;
use protocol_droid::domain::a2a::task::TaskState;
use protocol_droid::domain::a2a::task_lifecycle::{
CancelTaskRequest, CancelTaskResponse, GetTaskRequest, GetTaskResponse,
};
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
let peer = std::env::var("A2A_PEER").unwrap_or_else(|_| "http://127.0.0.1:8080".to_owned());
let client = Client::builder().timeout(Duration::from_secs(10)).build()?;
let card: AgentCard =
fetch_json(&client, &format!("{peer}/.well-known/agent-card.json")).await?;
eprintln!("peer: {} v{}", card.name, card.version);
eprintln!(
"capabilities: streaming={} pushNotifications={} extendedAgentCard={}",
card.capabilities.streaming,
card.capabilities.push_notifications,
card.capabilities.extended_agent_card,
);
let jsonrpc_url = card
.supported_interfaces
.iter()
.find(|i| i.protocol_binding.as_str() == JSONRPC.as_str())
.ok_or("peer advertises no JSON-RPC interface")?
.url
.clone();
let stream_url = jsonrpc_url.as_str().replace("/a2a", "/a2a/stream");
let task_id = send_message(&client, jsonrpc_url.as_str(), "hello from a2a_client").await?;
eprintln!("SendMessage -> task id {task_id}");
get_task(&client, jsonrpc_url.as_str(), &task_id).await?;
list_tasks(&client, jsonrpc_url.as_str()).await?;
cancel_task(&client, jsonrpc_url.as_str(), &task_id).await?;
send_streaming_message(&client, &stream_url, "hello via streaming").await?;
subscribe_to_task(&client, &stream_url, &task_id).await?;
let pc_id = create_push_notification_config(
&client,
jsonrpc_url.as_str(),
&task_id,
"http://1.2.3.4:9999/wh",
"demo-token",
)
.await?;
get_push_notification_config(&client, jsonrpc_url.as_str(), &task_id, &pc_id).await?;
list_push_notification_configs(&client, jsonrpc_url.as_str(), &task_id).await?;
delete_push_notification_config(&client, jsonrpc_url.as_str(), &task_id, &pc_id).await?;
Ok(())
}
async fn send_message(client: &Client, url: &str, text: &str) -> Result<String, Box<dyn Error>> {
let message = Message::new(MessageRole::User).with_part(Part::Text(TextPart {
text: text.to_owned(),
metadata: std::collections::BTreeMap::new(),
}));
let request = SendMessageRequest {
message,
configuration: None,
metadata: std::collections::BTreeMap::new(),
};
let value = serde_json::to_value(&request)?;
let result: Value = json_rpc(client, url, A2aMethod::SendMessage, value).await?;
let response: SendMessageResponse = serde_json::from_value(result)?;
match response {
SendMessageResponse::Task(task) => Ok(task.id.to_string()),
SendMessageResponse::Message(_) => Err("peer returned a Message instead of a Task".into()),
}
}
async fn get_task(client: &Client, url: &str, task_id: &str) -> Result<(), Box<dyn Error>> {
let request = GetTaskRequest {
id: task_id.to_owned(),
history_length: None,
metadata: std::collections::BTreeMap::new(),
};
let value = serde_json::to_value(&request)?;
let result: Value = json_rpc(client, url, A2aMethod::GetTask, value).await?;
let _response: GetTaskResponse = serde_json::from_value(result.clone())?;
eprintln!("GetTask({task_id}) -> {}", get_status(&result));
Ok(())
}
async fn list_tasks(client: &Client, url: &str) -> Result<(), Box<dyn Error>> {
let request = ListTasksRequest {
context_id: None,
status: None,
page_size: None,
page_token: None,
status_timestamp_after: None,
history_length: None,
metadata: std::collections::BTreeMap::new(),
};
let value = serde_json::to_value(&request)?;
let result: Value = json_rpc(client, url, A2aMethod::ListTasks, value).await?;
let response: ListTasksResponse = serde_json::from_value(result)?;
eprintln!("ListTasks -> {} task(s)", response.tasks.len());
Ok(())
}
async fn cancel_task(client: &Client, url: &str, task_id: &str) -> Result<(), Box<dyn Error>> {
let request = CancelTaskRequest {
id: task_id.to_owned(),
metadata: std::collections::BTreeMap::new(),
};
let value = serde_json::to_value(&request)?;
let result: Value = json_rpc(client, url, A2aMethod::CancelTask, value).await?;
let _response: CancelTaskResponse = serde_json::from_value(result.clone())?;
eprintln!("CancelTask({task_id}) -> {}", get_status(&result));
Ok(())
}
async fn create_push_notification_config(
client: &Client,
url: &str,
task_id: &str,
webhook_url: &str,
token: &str,
) -> Result<String, Box<dyn Error>> {
let config = TaskPushNotificationConfig {
id: "placeholder".to_owned(),
task_id: task_id.to_owned(),
url: webhook_url.to_owned(),
token: Some(token.to_owned()),
authentication: None,
metadata: std::collections::BTreeMap::new(),
};
let request = CreateTaskPushNotificationConfigRequest::from(config);
let value = serde_json::to_value(&request)?;
let result: Value = json_rpc(
client,
url,
A2aMethod::CreateTaskPushNotificationConfig,
value,
)
.await?;
let response: CreateTaskPushNotificationConfigResponse = serde_json::from_value(result)?;
let config_id = response.0.id;
eprintln!("CreateTaskPushNotificationConfig({task_id}) -> {config_id}");
Ok(config_id)
}
async fn get_push_notification_config(
client: &Client,
url: &str,
task_id: &str,
config_id: &str,
) -> Result<(), Box<dyn Error>> {
let request =
GetTaskPushNotificationConfigRequest::new(config_id.to_owned(), task_id.to_owned());
let value = serde_json::to_value(&request)?;
let result: Value =
json_rpc(client, url, A2aMethod::GetTaskPushNotificationConfig, value).await?;
let _response: GetTaskPushNotificationConfigResponse = serde_json::from_value(result)?;
eprintln!("GetTaskPushNotificationConfig({task_id}, {config_id}) -> ok");
Ok(())
}
async fn list_push_notification_configs(
client: &Client,
url: &str,
task_id: &str,
) -> Result<(), Box<dyn Error>> {
let request = ListTaskPushNotificationConfigsRequest {
task_id: task_id.to_owned(),
page_size: None,
page_token: None,
};
let value = serde_json::to_value(&request)?;
let result: Value = json_rpc(
client,
url,
A2aMethod::ListTaskPushNotificationConfigs,
value,
)
.await?;
let response: ListTaskPushNotificationConfigsResponse = serde_json::from_value(result)?;
eprintln!(
"ListTaskPushNotificationConfigs({task_id}) -> {} config(s)",
response.configs.len()
);
Ok(())
}
async fn delete_push_notification_config(
client: &Client,
url: &str,
task_id: &str,
config_id: &str,
) -> Result<(), Box<dyn Error>> {
let request =
DeleteTaskPushNotificationConfigRequest::new(config_id.to_owned(), task_id.to_owned());
let value = serde_json::to_value(&request)?;
let result: Value = json_rpc(
client,
url,
A2aMethod::DeleteTaskPushNotificationConfig,
value,
)
.await?;
let _response: DeleteTaskPushNotificationConfigResponse = serde_json::from_value(result)?;
eprintln!("DeleteTaskPushNotificationConfig({task_id}, {config_id}) -> ok");
Ok(())
}
async fn send_streaming_message(
client: &Client,
url: &str,
text: &str,
) -> Result<(), Box<dyn Error>> {
let message = Message::new(MessageRole::User).with_part(Part::Text(TextPart {
text: text.to_owned(),
metadata: std::collections::BTreeMap::new(),
}));
let request = SendMessageRequest {
message,
configuration: None,
metadata: std::collections::BTreeMap::new(),
};
let value = serde_json::to_value(&request)?;
let events: Vec<StreamingEvent> =
sse_post(client, url, A2aMethod::SendStreamingMessage, value).await?;
let names = events
.iter()
.map(|e| match e {
StreamingEvent::Status(s) => format!(
"{} ({})",
state_name(s.status.state),
if s.is_final { "final" } else { "more" }
),
StreamingEvent::Artifact(_) => "artifact".to_owned(),
})
.collect::<Vec<_>>()
.join(", ");
eprintln!("SendStreamingMessage -> {names}");
Ok(())
}
async fn subscribe_to_task(
client: &Client,
url: &str,
task_id: &str,
) -> Result<(), Box<dyn Error>> {
let request = GetTaskRequest {
id: task_id.to_owned(),
history_length: None,
metadata: std::collections::BTreeMap::new(),
};
let value = serde_json::to_value(&request)?;
let events: Vec<StreamingEvent> =
sse_post(client, url, A2aMethod::SubscribeToTask, value).await?;
if let Some(StreamingEvent::Status(s)) = events.first() {
eprintln!(
"SubscribeToTask({task_id}) -> {} (final={})",
state_name(s.status.state),
s.is_final
);
} else {
eprintln!("SubscribeToTask({task_id}) -> (no events)");
}
Ok(())
}
async fn json_rpc<T: DeserializeOwned>(
client: &Client,
url: &str,
method: A2aMethod,
params: Value,
) -> Result<T, Box<dyn Error>> {
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let envelope = json!({
"jsonrpc": "2.0",
"id": id,
"method": method.as_str(),
"params": params,
});
let body = client
.post(url)
.body(serde_json::to_vec(&envelope)?)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.send()
.await?
.error_for_status()?
.text()
.await?;
let response: Value = serde_json::from_str(&body)?;
if let Some(error) = response.get("error") {
return Err(format!("JSON-RPC error: {error}").into());
}
let Some(result) = response.get("result") else {
return Err(format!("missing result in response: {response}").into());
};
Ok(serde_json::from_value(result.clone())?)
}
async fn sse_post<T: DeserializeOwned>(
client: &Client,
url: &str,
method: A2aMethod,
params: Value,
) -> Result<Vec<T>, Box<dyn Error>> {
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let envelope = json!({
"jsonrpc": "2.0",
"id": id,
"method": method.as_str(),
"params": params,
});
let body = client
.post(url)
.body(serde_json::to_vec(&envelope)?)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "text/event-stream")
.send()
.await?
.error_for_status()?
.text()
.await?;
let mut events = Vec::new();
for block in body.split("\n\n") {
for line in block.lines() {
let Some(data) = line.strip_prefix("data: ") else {
continue;
};
let envelope: Value = serde_json::from_str(data)?;
if let Some(error) = envelope.get("error") {
return Err(format!("JSON-RPC error: {error}").into());
}
let Some(result) = envelope.get("result") else {
continue;
};
events.push(serde_json::from_value(result.clone())?);
}
}
Ok(events)
}
fn get_status(result: &Value) -> String {
result
.pointer("/status/state")
.and_then(Value::as_str)
.map_or_else(|| "(no status)".to_owned(), std::borrow::ToOwned::to_owned)
}
fn state_name(state: TaskState) -> String {
serde_json::to_value(state)
.ok()
.and_then(|v| v.as_str().map(std::borrow::ToOwned::to_owned))
.unwrap_or_else(|| "(unknown)".to_owned())
}
async fn fetch_json<T: DeserializeOwned>(client: &Client, url: &str) -> Result<T, Box<dyn Error>> {
let body = client
.get(url)
.send()
.await?
.error_for_status()?
.text()
.await?;
Ok(serde_json::from_str(&body)?)
}