use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::domain::{A2AError, ErrorDetail, ErrorInfo};
pub mod methods {
pub const SEND_MESSAGE: &str = "SendMessage";
pub const SEND_STREAMING_MESSAGE: &str = "SendStreamingMessage";
pub const GET_TASK: &str = "GetTask";
pub const LIST_TASKS: &str = "ListTasks";
pub const CANCEL_TASK: &str = "CancelTask";
pub const SUBSCRIBE_TO_TASK: &str = "SubscribeToTask";
pub const CREATE_PUSH_CONFIG: &str = "CreateTaskPushNotificationConfig";
pub const GET_PUSH_CONFIG: &str = "GetTaskPushNotificationConfig";
pub const LIST_PUSH_CONFIGS: &str = "ListTaskPushNotificationConfigs";
pub const DELETE_PUSH_CONFIG: &str = "DeleteTaskPushNotificationConfig";
pub const GET_EXTENDED_AGENT_CARD: &str = "GetExtendedAgentCard";
pub fn is_streaming(method: &str) -> bool {
matches!(method, SEND_STREAMING_MESSAGE | SUBSCRIBE_TO_TASK)
}
}
pub mod error_code {
pub const PARSE_ERROR: i32 = -32700;
pub const INVALID_REQUEST: i32 = -32600;
pub const METHOD_NOT_FOUND: i32 = -32601;
pub const INVALID_PARAMS: i32 = -32602;
pub const INTERNAL_ERROR: i32 = -32603;
pub const TASK_NOT_FOUND: i32 = -32001;
pub const TASK_NOT_CANCELABLE: i32 = -32002;
pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
pub const UNSUPPORTED_OPERATION: i32 = -32004;
pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
pub const INVALID_AGENT_RESPONSE: i32 = -32006;
pub const EXTENDED_CARD_NOT_CONFIGURED: i32 = -32007;
pub const VERSION_CONFLICT: i32 = -32101;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: JsonRpcId,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
#[serde(default)]
pub id: JsonRpcId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
impl JsonRpcResponse {
pub fn ok(id: JsonRpcId, result: Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: Some(result),
error: None,
}
}
pub fn err(id: JsonRpcId, error: JsonRpcError) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(error),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JsonRpcId {
Str(String),
Num(i64),
#[default]
Null,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
fn a2a_error_code(err: &A2AError) -> i32 {
use error_code::*;
match err {
A2AError::JsonRpc { code, .. } => *code,
A2AError::JsonParse(_) => PARSE_ERROR,
A2AError::InvalidRequest(_) => INVALID_REQUEST,
A2AError::MethodNotFound(_) => METHOD_NOT_FOUND,
A2AError::InvalidParams(_) | A2AError::ValidationError { .. } => INVALID_PARAMS,
A2AError::TaskNotFound(_) => TASK_NOT_FOUND,
A2AError::TaskNotCancelable(_) => TASK_NOT_CANCELABLE,
A2AError::PushNotificationNotSupported => PUSH_NOTIFICATION_NOT_SUPPORTED,
A2AError::UnsupportedOperation(_) => UNSUPPORTED_OPERATION,
A2AError::ContentTypeNotSupported(_) => CONTENT_TYPE_NOT_SUPPORTED,
A2AError::InvalidAgentResponse(_) => INVALID_AGENT_RESPONSE,
A2AError::AuthenticatedExtendedCardNotConfigured => EXTENDED_CARD_NOT_CONFIGURED,
A2AError::VersionConflict { .. } => VERSION_CONFLICT,
_ => INTERNAL_ERROR,
}
}
pub fn a2a_to_jsonrpc(err: &A2AError) -> JsonRpcError {
let message = match err {
A2AError::ValidationError { field, message } => format!("{field}: {message}"),
other => other.to_string(),
};
let details = err.error_details();
let data = (!details.is_empty()).then(|| serde_json::json!(details));
JsonRpcError {
code: a2a_error_code(err),
message,
data,
}
}
pub fn jsonrpc_to_a2a(err: &JsonRpcError) -> A2AError {
use error_code::*;
match err.code {
TASK_NOT_FOUND => A2AError::TaskNotFound(err.message.clone()),
INVALID_PARAMS => A2AError::InvalidParams(err.message.clone()),
METHOD_NOT_FOUND => A2AError::MethodNotFound(err.message.clone()),
UNSUPPORTED_OPERATION => A2AError::UnsupportedOperation(err.message.clone()),
EXTENDED_CARD_NOT_CONFIGURED => A2AError::AuthenticatedExtendedCardNotConfigured,
VERSION_CONFLICT => version_conflict_from_data(err)
.unwrap_or_else(|| A2AError::Internal(err.message.clone())),
code => A2AError::JsonRpc {
code,
message: err.message.clone(),
data: err.data.clone(),
},
}
}
fn version_conflict_from_data(err: &JsonRpcError) -> Option<A2AError> {
let details: Vec<ErrorDetail> = serde_json::from_value(err.data.clone()?).ok()?;
let ErrorInfo { metadata, .. } = details.into_iter().find_map(|d| match d {
ErrorDetail::ErrorInfo(info) => Some(info),
_ => None,
})?;
Some(A2AError::VersionConflict {
id: metadata.get("task_id").cloned().unwrap_or_default(),
expected: metadata.get("expected").and_then(|s| s.parse().ok())?,
actual: metadata.get("actual").and_then(|s| s.parse().ok())?,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_error_surfaces_bad_request_details() {
let err = A2AError::ValidationError {
field: "history_length".to_string(),
message: "too large".to_string(),
};
let wire = a2a_to_jsonrpc(&err);
assert_eq!(wire.code, error_code::INVALID_PARAMS);
let data = wire.data.expect("validation errors carry data");
assert_eq!(
data[0]["@type"],
"type.googleapis.com/google.rpc.BadRequest"
);
assert_eq!(data[0]["fieldViolations"][0]["field"], "history_length");
assert_eq!(data[1]["reason"], "VALIDATION_ERROR");
}
#[test]
fn version_conflict_round_trips_through_the_wire() {
let err = A2AError::VersionConflict {
id: "task-42".to_string(),
expected: 3,
actual: 5,
};
let wire = a2a_to_jsonrpc(&err);
assert_eq!(wire.code, error_code::VERSION_CONFLICT);
match jsonrpc_to_a2a(&wire) {
A2AError::VersionConflict {
id,
expected,
actual,
} => {
assert_eq!(id, "task-42");
assert_eq!(expected, 3);
assert_eq!(actual, 5);
}
other => panic!("expected VersionConflict, got {other:?}"),
}
}
#[test]
fn every_error_carries_a_reason_code() {
let wire = a2a_to_jsonrpc(&A2AError::TaskNotFound("x".to_string()));
let data = wire.data.expect("errors carry an ErrorInfo reason");
assert_eq!(data[0]["reason"], "TASK_NOT_FOUND");
assert_eq!(data[0]["domain"], "a2a-rs");
}
}