use crate::error::A2aError;
use serde::{Deserialize, Serialize};
pub mod codes {
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 SERVER_ERROR: i32 = -32000;
pub const UNAUTHENTICATED: i32 = -32001;
pub const RESUME_BUFFER_EXPIRED: i32 = -32010;
pub const LEADER_DIED: i32 = -32099;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: serde_json::Value,
pub method: String,
#[serde(default)]
pub params: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
#[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<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum A2aMethod {
SendMessage,
SendStreamingMessage,
GetTask,
ListTasks,
CancelTask,
SubscribeToTask,
CreateTaskPushNotificationConfig,
GetTaskPushNotificationConfig,
ListTaskPushNotificationConfigs,
DeleteTaskPushNotificationConfig,
GetExtendedAgentCard,
}
impl A2aMethod {
#[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Result<Self, A2aError> {
Ok(match s {
"SendMessage" | "message/send" => Self::SendMessage,
"SendStreamingMessage" | "message/stream" => Self::SendStreamingMessage,
"GetTask" | "tasks/get" => Self::GetTask,
"ListTasks" => Self::ListTasks,
"CancelTask" | "tasks/cancel" => Self::CancelTask,
"SubscribeToTask" | "tasks/resubscribe" => Self::SubscribeToTask,
"CreateTaskPushNotificationConfig" | "tasks/pushNotificationConfig/set" => {
Self::CreateTaskPushNotificationConfig
}
"GetTaskPushNotificationConfig" | "tasks/pushNotificationConfig/get" => {
Self::GetTaskPushNotificationConfig
}
"ListTaskPushNotificationConfigs" | "tasks/pushNotificationConfig/list" => {
Self::ListTaskPushNotificationConfigs
}
"DeleteTaskPushNotificationConfig" | "tasks/pushNotificationConfig/delete" => {
Self::DeleteTaskPushNotificationConfig
}
"GetExtendedAgentCard" | "agent/getAuthenticatedExtendedCard" => {
Self::GetExtendedAgentCard
}
other => return Err(A2aError::MethodNotFound(other.to_string())),
})
}
pub fn wire_name(&self) -> &'static str {
match self {
Self::SendMessage => "SendMessage",
Self::SendStreamingMessage => "SendStreamingMessage",
Self::GetTask => "GetTask",
Self::ListTasks => "ListTasks",
Self::CancelTask => "CancelTask",
Self::SubscribeToTask => "SubscribeToTask",
Self::CreateTaskPushNotificationConfig => "CreateTaskPushNotificationConfig",
Self::GetTaskPushNotificationConfig => "GetTaskPushNotificationConfig",
Self::ListTaskPushNotificationConfigs => "ListTaskPushNotificationConfigs",
Self::DeleteTaskPushNotificationConfig => "DeleteTaskPushNotificationConfig",
Self::GetExtendedAgentCard => "GetExtendedAgentCard",
}
}
}
#[derive(Debug, Clone)]
pub struct A2aHeaders {
pub authorization: Option<String>,
pub content_type: Option<String>,
pub a2a_version: String,
pub a2a_extensions: Vec<String>,
}
impl klieo_auth_common::Headers for A2aHeaders {
fn get(&self, name: &str) -> Option<&str> {
match name.to_ascii_lowercase().as_str() {
"authorization" => self.authorization.as_deref(),
"content-type" => self.content_type.as_deref(),
"a2a-version" => Some(self.a2a_version.as_str()),
_ => None,
}
}
}
impl A2aHeaders {
pub fn decode_from(headers: &klieo_core::Headers) -> Self {
let get = |k: &str| {
headers
.iter()
.find_map(|(name, value)| name.eq_ignore_ascii_case(k).then(|| value.clone()))
};
Self {
authorization: get("Authorization"),
content_type: get("Content-Type"),
a2a_version: get("A2A-Version").unwrap_or_else(|| "1.0".to_string()),
a2a_extensions: get("A2A-Extensions")
.map(|v| {
v.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn envelope_request_round_trips() {
let req = JsonRpcRequest {
jsonrpc: "2.0".into(),
id: json!(42),
method: "SendMessage".into(),
params: json!({"message": {"messageId": "m-1", "role": "user", "parts": []}}),
};
let v = serde_json::to_value(&req).unwrap();
let back: JsonRpcRequest = serde_json::from_value(v).unwrap();
assert_eq!(back.method, "SendMessage");
assert_eq!(back.id, json!(42));
}
#[test]
fn method_send_message_parses() {
let m = A2aMethod::from_str("SendMessage").unwrap();
assert_eq!(m, A2aMethod::SendMessage);
}
#[test]
fn legacy_slash_form_aliases_parse_to_the_same_variant_as_current_camel_case() {
let pairs: [(&str, &str, A2aMethod); 10] = [
("message/send", "SendMessage", A2aMethod::SendMessage),
(
"message/stream",
"SendStreamingMessage",
A2aMethod::SendStreamingMessage,
),
("tasks/get", "GetTask", A2aMethod::GetTask),
("tasks/cancel", "CancelTask", A2aMethod::CancelTask),
(
"tasks/resubscribe",
"SubscribeToTask",
A2aMethod::SubscribeToTask,
),
(
"tasks/pushNotificationConfig/set",
"CreateTaskPushNotificationConfig",
A2aMethod::CreateTaskPushNotificationConfig,
),
(
"tasks/pushNotificationConfig/get",
"GetTaskPushNotificationConfig",
A2aMethod::GetTaskPushNotificationConfig,
),
(
"tasks/pushNotificationConfig/list",
"ListTaskPushNotificationConfigs",
A2aMethod::ListTaskPushNotificationConfigs,
),
(
"tasks/pushNotificationConfig/delete",
"DeleteTaskPushNotificationConfig",
A2aMethod::DeleteTaskPushNotificationConfig,
),
(
"agent/getAuthenticatedExtendedCard",
"GetExtendedAgentCard",
A2aMethod::GetExtendedAgentCard,
),
];
for (legacy, current, expected) in pairs {
let via_legacy = A2aMethod::from_str(legacy)
.unwrap_or_else(|e| panic!("legacy `{legacy}` failed to parse: {e}"));
let via_current = A2aMethod::from_str(current)
.unwrap_or_else(|e| panic!("current `{current}` failed to parse: {e}"));
assert_eq!(via_legacy, expected, "legacy `{legacy}`");
assert_eq!(via_current, expected, "current `{current}`");
}
}
#[test]
fn list_tasks_has_no_legacy_alias() {
assert!(A2aMethod::from_str("tasks/list").is_err());
}
#[test]
fn wire_name_returns_current_camel_case_form_for_every_variant() {
let variants = [
(A2aMethod::SendMessage, "SendMessage"),
(A2aMethod::SendStreamingMessage, "SendStreamingMessage"),
(A2aMethod::GetTask, "GetTask"),
(A2aMethod::ListTasks, "ListTasks"),
(A2aMethod::CancelTask, "CancelTask"),
(A2aMethod::SubscribeToTask, "SubscribeToTask"),
(
A2aMethod::CreateTaskPushNotificationConfig,
"CreateTaskPushNotificationConfig",
),
(
A2aMethod::GetTaskPushNotificationConfig,
"GetTaskPushNotificationConfig",
),
(
A2aMethod::ListTaskPushNotificationConfigs,
"ListTaskPushNotificationConfigs",
),
(
A2aMethod::DeleteTaskPushNotificationConfig,
"DeleteTaskPushNotificationConfig",
),
(A2aMethod::GetExtendedAgentCard, "GetExtendedAgentCard"),
];
for (variant, expected) in variants {
assert_eq!(variant.wire_name(), expected);
}
}
#[test]
fn wire_name_round_trips_through_from_str() {
for method in [
A2aMethod::SendMessage,
A2aMethod::SendStreamingMessage,
A2aMethod::GetTask,
A2aMethod::ListTasks,
A2aMethod::CancelTask,
A2aMethod::SubscribeToTask,
A2aMethod::CreateTaskPushNotificationConfig,
A2aMethod::GetTaskPushNotificationConfig,
A2aMethod::ListTaskPushNotificationConfigs,
A2aMethod::DeleteTaskPushNotificationConfig,
A2aMethod::GetExtendedAgentCard,
] {
assert_eq!(A2aMethod::from_str(method.wire_name()).unwrap(), method);
}
}
#[test]
fn unknown_method_returns_method_not_found() {
let err = A2aMethod::from_str("Frobnicate").unwrap_err();
assert!(matches!(err, crate::error::A2aError::MethodNotFound(_)));
}
#[test]
fn headers_decode_with_all_four_fields() {
let mut h = klieo_core::Headers::new();
h.insert("Authorization".into(), "Bearer abc".into());
h.insert("Content-Type".into(), "application/json".into());
h.insert("A2A-Version".into(), "1.0".into());
h.insert("A2A-Extensions".into(), "ext1,ext2".into());
let decoded = A2aHeaders::decode_from(&h);
assert_eq!(decoded.authorization.as_deref(), Some("Bearer abc"));
assert_eq!(decoded.a2a_version, "1.0");
assert_eq!(
decoded.a2a_extensions,
vec!["ext1".to_string(), "ext2".to_string()]
);
}
#[test]
fn missing_version_defaults_to_one_zero() {
let h = klieo_core::Headers::new();
let decoded = A2aHeaders::decode_from(&h);
assert_eq!(decoded.a2a_version, "1.0");
}
}