use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::errors::A2aErrorCode;
pub const METHOD_MESSAGE_SEND: &str = "message/send";
pub const METHOD_MESSAGE_STREAM: &str = "message/stream";
pub const METHOD_TASKS_GET: &str = "tasks/get";
pub const METHOD_TASKS_LIST: &str = "tasks/list";
pub const METHOD_TASKS_CANCEL: &str = "tasks/cancel";
pub const METHOD_TASKS_PUSH_CONFIG_SET: &str = "tasks/pushNotificationConfig/set";
pub 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 const JSONRPC_VERSION: &str = "2.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
pub id: Value,
}
impl JsonRpcRequest {
pub fn new(method: impl Into<String>, params: Option<Value>, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params,
id,
}
}
pub 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 {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
pub id: Value,
}
impl JsonRpcResponse {
pub fn success(result: Value, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
result: Some(result),
error: None,
id,
}
}
pub fn error(error: JsonRpcError, id: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
result: None,
error: Some(error),
id,
}
}
pub fn is_success(&self) -> bool {
self.result.is_some() && self.error.is_none()
}
pub fn is_error(&self) -> bool {
self.error.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl JsonRpcError {
pub 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),
}
}
pub 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 fn invalid_request(message: impl Into<String>) -> Self {
Self::from_code(A2aErrorCode::InvalidRequest, message)
}
pub 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)
}
pub 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 message: super::types::Message,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub 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")]
pub accepted_input_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accepted_output_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub history_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub push_notification_config: Option<PushNotificationConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PushNotificationConfig {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskQueryParams {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub 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 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 page_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub history_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_updated_after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_artifacts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTasksResult {
pub tasks: Vec<super::types::Task>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_size: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskIdParams {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskPushNotificationConfig {
pub task_id: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendStreamingMessageResponse {
#[serde(flatten)]
pub 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,
},
}
fn default_message_kind() -> String {
"streaming-response".to_string()
}
fn default_status_kind() -> String {
"status-update".to_string()
}
impl StreamingEvent {
pub 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,
}
}
pub 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),
}
}
pub 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 { .. } => 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());
}
}