use crate::dm::{DmError, DmSendConfig};
use crate::identity::AgentId;
use crate::Agent;
use dashmap::DashMap;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::future::Future;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::oneshot;
use tokio::task::JoinSet;
use tracing::{debug, trace, warn};
use uuid::Uuid;
pub const X0X_BINDING_VERSION: &str = "a2a/1";
pub const KIND_REQUEST: &str = "request";
pub const KIND_RESPONSE: &str = "response";
const JSONRPC_2_0: &str = "2.0";
pub const JSONRPC_PARSE_ERROR: i64 = -32700;
pub const JSONRPC_INVALID_REQUEST: i64 = -32600;
pub const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;
pub const JSONRPC_INTERNAL_ERROR: i64 = -32603;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BindingEnvelope {
#[serde(rename = "x0xBinding")]
pub binding: String,
#[serde(rename = "corrId")]
pub corr_id: String,
pub kind: String,
pub jsonrpc: Value,
}
impl BindingEnvelope {
pub fn new(
kind: &str,
corr_id: String,
jsonrpc: &impl Serialize,
) -> Result<Self, BindingError> {
Ok(Self {
binding: X0X_BINDING_VERSION.to_string(),
corr_id,
kind: kind.to_string(),
jsonrpc: serde_json::to_value(jsonrpc)
.map_err(|e| BindingError::Encode(e.to_string()))?,
})
}
}
pub fn encode_envelope(envelope: &BindingEnvelope) -> Result<Vec<u8>, BindingError> {
let bytes = serde_json::to_vec(envelope).map_err(|e| BindingError::Encode(e.to_string()))?;
if bytes.len() > crate::direct::MAX_DIRECT_PAYLOAD_SIZE {
return Err(BindingError::PayloadTooLarge {
len: bytes.len(),
max: crate::direct::MAX_DIRECT_PAYLOAD_SIZE,
});
}
Ok(bytes)
}
pub fn decode_envelope(bytes: &[u8]) -> Result<BindingEnvelope, BindingError> {
let envelope: BindingEnvelope =
serde_json::from_slice(bytes).map_err(|e| BindingError::Malformed(e.to_string()))?;
if envelope.binding != X0X_BINDING_VERSION {
return Err(BindingError::UnsupportedVersion(envelope.binding));
}
Ok(envelope)
}
#[must_use]
pub fn probe_binding_version(bytes: &[u8]) -> Option<String> {
#[derive(Deserialize)]
struct Probe {
#[serde(rename = "x0xBinding")]
binding: Option<String>,
}
serde_json::from_slice::<Probe>(bytes).ok()?.binding
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
pub id: Value,
}
impl JsonRpcRequest {
#[must_use]
pub fn new(method: impl Into<String>, params: Option<Value>, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_2_0.to_string(),
method: method.into(),
params,
id,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
pub id: Value,
}
impl JsonRpcResponse {
#[must_use]
pub fn result(result: Value, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_2_0.to_string(),
result: Some(result),
error: None,
id,
}
}
#[must_use]
pub fn error(error: JsonRpcError, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_2_0.to_string(),
result: None,
error: Some(error),
id,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl JsonRpcError {
#[must_use]
pub fn new(code: i64, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
#[must_use]
pub fn method_not_found(method: &str) -> Self {
Self::new(
JSONRPC_METHOD_NOT_FOUND,
format!("method not found: {method}"),
)
}
#[must_use]
pub fn invalid_request(reason: impl Into<String>) -> Self {
Self::new(JSONRPC_INVALID_REQUEST, reason)
}
#[must_use]
pub fn internal(reason: impl Into<String>) -> Self {
Self::new(JSONRPC_INTERNAL_ERROR, reason)
}
}
#[derive(Debug, thiserror::Error)]
pub enum BindingError {
#[error("malformed binding envelope: {0}")]
Malformed(String),
#[error("unsupported x0xBinding version {0:?}")]
UnsupportedVersion(String),
#[error("envelope encode failed: {0}")]
Encode(String),
#[error("envelope too large: {len} bytes (max {max})")]
PayloadTooLarge {
len: usize,
max: usize,
},
#[error("direct send failed: {0}")]
Send(#[from] DmError),
#[error("request timed out after {0:?}")]
Timeout(Duration),
#[error("remote error {code}: {message}")]
Remote {
code: i64,
message: String,
data: Option<Value>,
},
#[error("invalid JSON-RPC response: {0}")]
InvalidResponse(String),
#[error("binding session closed")]
Closed,
}
pub type BindingHandler =
Arc<dyn Fn(Option<Value>) -> BoxFuture<'static, Result<Value, JsonRpcError>> + Send + Sync>;
#[derive(Debug, Clone)]
pub struct BindingConfig {
pub request_timeout: Duration,
pub send_config: DmSendConfig,
}
impl Default for BindingConfig {
fn default() -> Self {
Self {
request_timeout: Duration::from_secs(30),
send_config: DmSendConfig {
prefer_raw_quic_if_connected: true,
raw_quic_receive_ack_timeout: Some(Duration::from_secs(4)),
..DmSendConfig::default()
},
}
}
}
struct SessionInner {
agent: Arc<Agent>,
handlers: DashMap<String, BindingHandler>,
in_flight: DashMap<String, oneshot::Sender<JsonRpcResponse>>,
handler_tasks: Mutex<JoinSet<()>>,
config: BindingConfig,
}
pub struct BindingSession {
inner: Arc<SessionInner>,
receive_task: Mutex<Option<tokio::task::JoinHandle<()>>>,
}
impl BindingSession {
#[must_use]
pub fn start(agent: Arc<Agent>, config: BindingConfig) -> Self {
let inner = Arc::new(SessionInner {
agent,
handlers: DashMap::new(),
in_flight: DashMap::new(),
handler_tasks: Mutex::new(JoinSet::new()),
config,
});
let rx = inner.agent.subscribe_direct();
let loop_inner = Arc::clone(&inner);
let receive_task = tokio::spawn(async move { receive_loop(loop_inner, rx).await });
Self {
inner,
receive_task: Mutex::new(Some(receive_task)),
}
}
pub fn register_handler<F, Fut>(&self, method: &str, handler: F)
where
F: Fn(Option<Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Value, JsonRpcError>> + Send + 'static,
{
self.inner.handlers.insert(
method.to_string(),
Arc::new(move |params| Box::pin(handler(params)) as BoxFuture<'static, _>),
);
}
#[must_use]
pub fn in_flight_len(&self) -> usize {
self.inner.in_flight.len()
}
pub async fn call(
&self,
peer: &AgentId,
method: &str,
params: Option<Value>,
) -> Result<Value, BindingError> {
let corr_id = Uuid::new_v4().to_string();
let request = JsonRpcRequest::new(method, params, Value::String(corr_id.clone()));
let envelope = BindingEnvelope::new(KIND_REQUEST, corr_id.clone(), &request)?;
let bytes = encode_envelope(&envelope)?;
let (tx, rx) = oneshot::channel();
self.inner.in_flight.insert(corr_id.clone(), tx);
let _cleanup = InFlightCleanup {
in_flight: &self.inner.in_flight,
corr_id: corr_id.clone(),
};
if let Err(err) = self
.inner
.agent
.send_direct_with_config(peer, bytes, self.inner.config.send_config.clone())
.await
{
return Err(BindingError::Send(err));
}
debug!(%corr_id, method, "a2a request sent");
let response = match tokio::time::timeout(self.inner.config.request_timeout, rx).await {
Ok(Ok(response)) => response,
Ok(Err(_sender_dropped)) => return Err(BindingError::Closed),
Err(_elapsed) => {
return Err(BindingError::Timeout(self.inner.config.request_timeout));
}
};
response_into_result(response)
}
}
impl Drop for BindingSession {
fn drop(&mut self) {
if let Ok(Some(task)) = self.receive_task.lock().map(|mut slot| slot.take()) {
task.abort();
}
if let Ok(mut tasks) = self.inner.handler_tasks.lock() {
tasks.abort_all();
}
}
}
struct InFlightCleanup<'a> {
in_flight: &'a DashMap<String, oneshot::Sender<JsonRpcResponse>>,
corr_id: String,
}
impl Drop for InFlightCleanup<'_> {
fn drop(&mut self) {
self.in_flight.remove(&self.corr_id);
}
}
fn response_into_result(response: JsonRpcResponse) -> Result<Value, BindingError> {
match (response.result, response.error) {
(Some(value), None) => Ok(value),
(None, Some(error)) => Err(BindingError::Remote {
code: error.code,
message: error.message,
data: error.data,
}),
(result, error) => Err(BindingError::InvalidResponse(format!(
"exactly one of result/error required (result present: {}, error present: {})",
result.is_some(),
error.is_some()
))),
}
}
async fn receive_loop(inner: Arc<SessionInner>, mut rx: crate::direct::DirectMessageReceiver) {
while let Some(msg) = rx.recv().await {
let Some(version) = probe_binding_version(&msg.payload) else {
continue;
};
if version != X0X_BINDING_VERSION {
debug!(%version, "skipping A2A binding envelope with unsupported version");
continue;
}
let envelope = match decode_envelope(&msg.payload) {
Ok(envelope) => envelope,
Err(err) => {
warn!(%err, "dropping malformed A2A binding envelope");
continue;
}
};
match envelope.kind.as_str() {
KIND_REQUEST => {
let handler_inner = Arc::clone(&inner);
let sender = msg.sender;
let Ok(mut tasks) = inner.handler_tasks.lock() else {
warn!("handler task set poisoned; dropping request");
continue;
};
while tasks.try_join_next().is_some() {}
tasks.spawn(async move {
handler_inner.handle_request(sender, envelope).await;
});
}
KIND_RESPONSE => inner.handle_response(envelope),
other => {
trace!(kind = %other, "skipping unknown A2A binding envelope kind");
}
}
}
debug!("a2a binding receive loop ended (DM channel closed)");
}
impl SessionInner {
async fn handle_request(&self, peer: AgentId, envelope: BindingEnvelope) {
let response = match serde_json::from_value::<JsonRpcRequest>(envelope.jsonrpc) {
Ok(request) => self.dispatch(request).await,
Err(err) => JsonRpcResponse::error(
JsonRpcError::invalid_request(format!("not a JSON-RPC 2.0 request: {err}")),
Value::Null,
),
};
let result = BindingEnvelope::new(KIND_RESPONSE, envelope.corr_id, &response)
.and_then(|response_envelope| encode_envelope(&response_envelope));
let bytes = match result {
Ok(bytes) => bytes,
Err(err) => {
warn!(%err, "failed to encode A2A response; dropping");
return;
}
};
if let Err(err) = self
.agent
.send_direct_with_config(&peer, bytes, self.config.send_config.clone())
.await
{
warn!(%err, "failed to send A2A response");
}
}
async fn dispatch(&self, request: JsonRpcRequest) -> JsonRpcResponse {
let handler = self
.handlers
.get(&request.method)
.map(|entry| Arc::clone(entry.value()));
let Some(handler) = handler else {
return JsonRpcResponse::error(
JsonRpcError::method_not_found(&request.method),
request.id,
);
};
match handler(request.params).await {
Ok(result) => JsonRpcResponse::result(result, request.id),
Err(error) => JsonRpcResponse::error(error, request.id),
}
}
fn handle_response(&self, envelope: BindingEnvelope) {
let Some((_, waiter)) = self.in_flight.remove(&envelope.corr_id) else {
trace!(
corr_id = %envelope.corr_id,
"no in-flight request for A2A response; skipping"
);
return;
};
let response = match serde_json::from_value::<JsonRpcResponse>(envelope.jsonrpc) {
Ok(response) => response,
Err(err) => JsonRpcResponse::error(
JsonRpcError::internal(format!("unparseable JSON-RPC response: {err}")),
Value::Null,
),
};
let _ = waiter.send(response);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_request() -> JsonRpcRequest {
JsonRpcRequest::new(
"message/send",
Some(json!({"message": {"parts": [{"kind": "text", "text": "ping"}]}})),
Value::String("corr-1".to_string()),
)
}
#[test]
fn envelope_round_trips_request() {
let envelope = BindingEnvelope::new(KIND_REQUEST, "corr-1".to_string(), &sample_request())
.expect("build envelope");
assert_eq!(envelope.binding, X0X_BINDING_VERSION);
let bytes = encode_envelope(&envelope).expect("encode");
let decoded = decode_envelope(&bytes).expect("decode");
assert_eq!(decoded, envelope);
let request: JsonRpcRequest =
serde_json::from_value(decoded.jsonrpc).expect("parse jsonrpc");
assert_eq!(request.method, "message/send");
assert_eq!(request.jsonrpc, "2.0");
}
#[test]
fn envelope_round_trips_error_response() {
let response = JsonRpcResponse::error(
JsonRpcError::method_not_found("tasks/resubscribe"),
Value::String("corr-9".to_string()),
);
let envelope = BindingEnvelope::new(KIND_RESPONSE, "corr-9".to_string(), &response)
.expect("build envelope");
let decoded =
decode_envelope(&encode_envelope(&envelope).expect("encode")).expect("decode");
let parsed: JsonRpcResponse =
serde_json::from_value(decoded.jsonrpc).expect("parse jsonrpc");
let error = parsed.error.expect("error object");
assert_eq!(error.code, JSONRPC_METHOD_NOT_FOUND);
assert!(parsed.result.is_none());
}
#[test]
fn wire_shape_matches_design_sketch() {
let envelope = BindingEnvelope::new(KIND_REQUEST, "corr-1".to_string(), &sample_request())
.expect("build envelope");
let value: Value =
serde_json::from_slice(&encode_envelope(&envelope).expect("encode")).expect("json");
assert_eq!(value["x0xBinding"], json!("a2a/1"));
assert_eq!(value["corrId"], json!("corr-1"));
assert_eq!(value["kind"], json!("request"));
assert_eq!(value["jsonrpc"]["jsonrpc"], json!("2.0"));
assert_eq!(value["jsonrpc"]["method"], json!("message/send"));
}
#[test]
fn decode_preserves_unknown_kind_and_members() {
let bytes = br#"{"x0xBinding":"a2a/1","corrId":"c","kind":"stream","seq":7,"jsonrpc":{}}"#;
let envelope = decode_envelope(bytes).expect("decode");
assert_eq!(envelope.kind, "stream");
assert_eq!(envelope.corr_id, "c");
}
#[test]
fn decode_rejects_unsupported_version() {
let bytes = br#"{"x0xBinding":"a2a/2","corrId":"c","kind":"request","jsonrpc":{}}"#;
let err = decode_envelope(bytes).expect_err("must reject");
assert!(matches!(err, BindingError::UnsupportedVersion(v) if v == "a2a/2"));
}
#[test]
fn decode_rejects_malformed_payload() {
let err = decode_envelope(b"not json at all").expect_err("must reject");
assert!(matches!(err, BindingError::Malformed(_)));
}
#[test]
fn probe_distinguishes_binding_payloads() {
assert_eq!(probe_binding_version(b"plain-text-dm"), None);
assert_eq!(probe_binding_version(br#"{"other":"json"}"#), None);
assert_eq!(
probe_binding_version(
br#"{"x0xBinding":"a2a/1","corrId":"c","kind":"request","jsonrpc":{}}"#
),
Some("a2a/1".to_string())
);
}
#[test]
fn response_into_result_variants() {
let ok = response_into_result(JsonRpcResponse::result(json!({"ok": true}), Value::Null));
assert_eq!(ok.expect("result"), json!({"ok": true}));
let err = response_into_result(JsonRpcResponse::error(
JsonRpcError::method_not_found("nope"),
Value::Null,
))
.expect_err("remote error");
assert!(matches!(
err,
BindingError::Remote {
code: JSONRPC_METHOD_NOT_FOUND,
..
}
));
let both = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: Some(Value::Null),
error: Some(JsonRpcError::internal("x")),
id: Value::Null,
};
assert!(matches!(
response_into_result(both),
Err(BindingError::InvalidResponse(_))
));
let neither = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
result: None,
error: None,
id: Value::Null,
};
assert!(matches!(
response_into_result(neither),
Err(BindingError::InvalidResponse(_))
));
}
}