use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::errors::A2aErrorCode;
pub(crate) const METHOD_MESSAGE_SEND: &str = "message/send";
pub(crate) const METHOD_MESSAGE_STREAM: &str = "message/stream";
pub(crate) const METHOD_TASKS_GET: &str = "tasks/get";
pub(crate) const METHOD_TASKS_LIST: &str = "tasks/list";
pub(crate) const METHOD_TASKS_CANCEL: &str = "tasks/cancel";
pub(crate) const METHOD_TASKS_PUSH_CONFIG_SET: &str = "tasks/pushNotificationConfig/set";
pub(crate) const METHOD_TASKS_PUSH_CONFIG_GET: &str = "tasks/pushNotificationConfig/get";
pub const METHOD_TASKS_RESUBSCRIBE: &str = "tasks/resubscribe";
pub const METHOD_AGENT_GET_EXTENDED_CARD: &str = "agent/getAuthenticatedExtendedCard";
pub(crate) const JSONRPC_VERSION: &str = "2.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub(crate) jsonrpc: String,
pub(crate) method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) params: Option<Value>,
pub(crate) id: Value,
}
impl JsonRpcRequest {
fn new(method: impl Into<String>, params: Option<Value>, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params,
id,
}
}
pub(crate) fn with_string_id(method: impl Into<String>, params: Option<Value>, id: impl Into<String>) -> Self {
Self::new(method, params, Value::String(id.into()))
}
pub fn with_numeric_id(method: impl Into<String>, params: Option<Value>, id: i64) -> Self {
Self::new(method, params, Value::Number(id.into()))
}
pub fn message_send(params: MessageSendParams, id: Value) -> Self {
Self::new(METHOD_MESSAGE_SEND, Some(serde_json::to_value(params).unwrap_or_default()), id)
}
pub fn tasks_get(task_id: impl Into<String>, id: Value) -> Self {
Self::new(METHOD_TASKS_GET, Some(serde_json::json!({ "id": task_id.into() })), id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<JsonRpcError>,
id: Value,
}
impl JsonRpcResponse {
pub(crate) fn success(result: Value, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
result: Some(result),
error: None,
id,
}
}
pub(crate) fn error(error: JsonRpcError, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
result: None,
error: Some(error),
id,
}
}
fn is_success(&self) -> bool {
self.result.is_some() && self.error.is_none()
}
fn is_error(&self) -> bool {
self.error.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub(crate) code: i32,
pub(crate) message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
}
impl JsonRpcError {
pub(crate) fn new(code: i32, message: impl Into<String>) -> Self {
Self { code, message: message.into(), data: None }
}
pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
Self { code, message: message.into(), data: Some(data) }
}
fn from_code(code: A2aErrorCode, message: impl Into<String>) -> Self {
Self::new(code.into(), message)
}
pub fn parse_error(message: impl Into<String>) -> Self {
Self::from_code(A2aErrorCode::JsonParseError, message)
}
pub(crate) fn invalid_request(message: impl Into<String>) -> Self {
Self::from_code(A2aErrorCode::InvalidRequest, message)
}
pub(crate) fn method_not_found(method: impl Into<String>) -> Self {
Self::from_code(A2aErrorCode::MethodNotFound, format!("Method not found: {}", method.into()))
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::from_code(A2aErrorCode::InvalidParams, message)
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::from_code(A2aErrorCode::InternalError, message)
}
fn task_not_found(task_id: impl Into<String>) -> Self {
Self::from_code(A2aErrorCode::TaskNotFound, format!("Task not found: {}", task_id.into()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageSendParams {
pub(crate) message: super::types::Message,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) context_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
configuration: Option<MessageConfiguration>,
}
impl MessageSendParams {
pub fn new(message: super::types::Message) -> Self {
Self {
message,
task_id: None,
context_id: None,
configuration: None,
}
}
pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
self.task_id = Some(task_id.into());
self
}
pub fn with_context_id(mut self, context_id: impl Into<String>) -> Self {
self.context_id = Some(context_id.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
accepted_input_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
accepted_output_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
history_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
push_notification_config: Option<PushNotificationConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PushNotificationConfig {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
authentication: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskQueryParams {
pub(crate) id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) history_length: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ListTasksParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) status: Option<super::types::TaskState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) page_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) history_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) last_updated_after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) include_artifacts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) metadata: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTasksResult {
pub(crate) tasks: Vec<super::types::Task>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) total_size: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) page_size: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) next_page_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskIdParams {
pub(crate) id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskPushNotificationConfig {
pub(crate) task_id: String,
pub(crate) url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) authentication: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendStreamingMessageResponse {
#[serde(flatten)]
pub(crate) event: StreamingEvent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum StreamingEvent {
#[serde(rename = "message")]
Message {
message: super::types::Message,
#[serde(skip_serializing_if = "Option::is_none")]
context_id: Option<String>,
#[serde(default = "default_message_kind")]
kind: String,
#[serde(default)]
r#final: bool,
},
#[serde(rename = "task-status")]
TaskStatus {
task_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
context_id: Option<String>,
status: super::types::TaskStatus,
#[serde(default = "default_status_kind")]
kind: String,
#[serde(default)]
r#final: bool,
},
#[serde(rename = "task-artifact")]
TaskArtifact {
task_id: String,
artifact: super::types::Artifact,
#[serde(default)]
append: bool,
#[serde(default)]
last_chunk: bool,
#[serde(default)]
r#final: bool,
},
#[serde(other)]
Unknown,
}
fn default_message_kind() -> String {
"streaming-response".to_string()
}
fn default_status_kind() -> String {
"status-update".to_string()
}
impl StreamingEvent {
pub(crate) fn is_final(&self) -> bool {
match self {
StreamingEvent::Message { r#final, .. } => *r#final,
StreamingEvent::TaskStatus { r#final, .. } => *r#final,
StreamingEvent::TaskArtifact { r#final, .. } => *r#final,
StreamingEvent::Unknown => false,
}
}
pub(crate) fn task_id(&self) -> Option<&str> {
match self {
StreamingEvent::Message { .. } => None,
StreamingEvent::TaskStatus { task_id, .. } => Some(task_id),
StreamingEvent::TaskArtifact { task_id, .. } => Some(task_id),
StreamingEvent::Unknown => None,
}
}
fn context_id(&self) -> Option<&str> {
match self {
StreamingEvent::Message { context_id, .. } => context_id.as_deref(),
StreamingEvent::TaskStatus { context_id, .. } => context_id.as_deref(),
StreamingEvent::TaskArtifact { .. } | StreamingEvent::Unknown => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_rpc_request_creation() {
let request =
JsonRpcRequest::with_string_id(METHOD_MESSAGE_SEND, Some(serde_json::json!({"message": {}})), "req-1");
assert_eq!(request.jsonrpc, "2.0");
assert_eq!(request.method, "message/send");
assert_eq!(request.id, Value::String("req-1".to_string()));
}
#[test]
fn test_json_rpc_response_success() {
let response =
JsonRpcResponse::success(serde_json::json!({"status": "ok"}), Value::String("req-1".to_string()));
assert!(response.is_success());
assert!(!response.is_error());
}
#[test]
fn test_json_rpc_response_error() {
let error = JsonRpcError::task_not_found("task-123");
let response = JsonRpcResponse::error(error, Value::String("req-1".to_string()));
assert!(!response.is_success());
assert!(response.is_error());
}
#[test]
fn test_error_code_serialization() {
let error = JsonRpcError::from_code(A2aErrorCode::TaskNotFound, "Task not found");
assert_eq!(error.code, -32001);
}
#[test]
fn test_streaming_event_message() {
let event = StreamingEvent::Message {
message: super::super::types::Message::agent_text("Response"),
context_id: Some("ctx-1".to_string()),
kind: "streaming-response".to_string(),
r#final: false,
};
assert!(!event.is_final());
assert_eq!(event.context_id(), Some("ctx-1"));
}
#[test]
fn test_streaming_event_task_status() {
let event = StreamingEvent::TaskStatus {
task_id: "task-1".to_string(),
context_id: None,
status: super::super::types::TaskStatus::new(super::super::types::TaskState::Completed),
kind: "status-update".to_string(),
r#final: true,
};
assert!(event.is_final());
assert_eq!(event.task_id(), Some("task-1"));
}
#[test]
fn test_streaming_event_artifact() {
let artifact = super::super::types::Artifact::text("art-1", "Output");
let event = StreamingEvent::TaskArtifact {
task_id: "task-1".to_string(),
artifact,
append: false,
last_chunk: true,
r#final: false,
};
assert!(!event.is_final());
assert_eq!(event.task_id(), Some("task-1"));
}
#[test]
fn test_send_streaming_message_response_serialization() {
let msg = super::super::types::Message::agent_text("Hello");
let response = SendStreamingMessageResponse {
event: StreamingEvent::Message {
message: msg,
context_id: Some("ctx-1".to_string()),
kind: "streaming-response".to_string(),
r#final: false,
},
};
let json = serde_json::to_string(&response).expect("serialize");
assert!(json.contains("streaming-response"));
assert!(json.contains("message"));
let deserialized: SendStreamingMessageResponse = serde_json::from_str(&json).expect("deserialize");
match deserialized.event {
StreamingEvent::Message { ref kind, .. } => {
assert_eq!(kind, "streaming-response");
}
_ => panic!("Expected Message event"),
}
}
#[test]
fn test_task_push_notification_config() {
let config = TaskPushNotificationConfig {
task_id: "task-1".to_string(),
url: "https://example.com/webhook".to_string(),
authentication: Some("Bearer token123".to_string()),
};
let json = serde_json::to_string(&config).expect("serialize");
let deserialized: TaskPushNotificationConfig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(deserialized.task_id, "task-1");
assert_eq!(deserialized.url, "https://example.com/webhook");
assert!(deserialized.authentication.is_some());
}
}