use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub mod method {
pub const MESSAGE_SEND: &str = "message/send";
pub const MESSAGE_STREAM: &str = "message/stream";
pub const TASKS_GET: &str = "tasks/get";
pub const TASKS_CANCEL: &str = "tasks/cancel";
pub const TASKS_RESUBSCRIBE: &str = "tasks/resubscribe";
pub const TASKS_PUSH_NOTIFICATION_CONFIG_SET: &str = "tasks/pushNotificationConfig/set";
pub const TASKS_PUSH_NOTIFICATION_CONFIG_GET: &str = "tasks/pushNotificationConfig/get";
pub const TASKS_PUSH_NOTIFICATION_CONFIG_LIST: &str = "tasks/pushNotificationConfig/list";
pub const TASKS_PUSH_NOTIFICATION_CONFIG_DELETE: &str = "tasks/pushNotificationConfig/delete";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2aRequest {
pub jsonrpc: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Value>,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2aResponse {
pub jsonrpc: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<A2aError>,
}
impl A2aResponse {
pub fn ok(id: Option<Value>, result: Value) -> Self {
Self {
jsonrpc: "2.0".into(),
id,
result: Some(result),
error: None,
}
}
pub fn err(id: Option<Value>, error: A2aError) -> Self {
Self {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(error),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct A2aError {
pub code: i32,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl A2aError {
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 fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
}
impl std::fmt::Display for A2aError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "A2A error {}: {}", self.code, self.message)
}
}
impl std::error::Error for A2aError {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Message {
pub kind: MessageKind,
pub role: MessageRole,
pub parts: Vec<Part>,
#[serde(rename = "messageId")]
pub message_id: String,
#[serde(default, rename = "taskId", skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default, rename = "contextId", skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(
default,
rename = "referenceTaskIds",
skip_serializing_if = "Vec::is_empty"
)]
pub reference_task_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<IndexMap<String, Value>>,
}
impl Message {
pub fn user_text(text: impl Into<String>) -> Self {
Self {
kind: MessageKind::Message,
role: MessageRole::User,
parts: vec![Part::Text {
text: text.into(),
metadata: None,
}],
message_id: uuid::Uuid::new_v4().to_string(),
task_id: None,
context_id: None,
reference_task_ids: Vec::new(),
metadata: None,
}
}
pub fn agent_text(text: impl Into<String>) -> Self {
Self {
kind: MessageKind::Message,
role: MessageRole::Agent,
parts: vec![Part::Text {
text: text.into(),
metadata: None,
}],
message_id: uuid::Uuid::new_v4().to_string(),
task_id: None,
context_id: None,
reference_task_ids: Vec::new(),
metadata: None,
}
}
#[must_use]
pub fn text_concat(&self) -> String {
let mut out = String::new();
for p in &self.parts {
if let Part::Text { text, .. } = p {
out.push_str(text);
}
}
out
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum MessageKind {
#[default]
Message,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Part {
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<IndexMap<String, Value>>,
},
File {
file: FilePayload,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<IndexMap<String, Value>>,
},
Data {
data: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<IndexMap<String, Value>>,
},
}
impl Part {
pub fn text(t: impl Into<String>) -> Self {
Self::Text {
text: t.into(),
metadata: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum FilePayload {
Inline {
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, rename = "mimeType", skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
bytes: String,
},
Uri {
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, rename = "mimeType", skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
uri: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Task {
pub kind: TaskKind,
pub id: String,
#[serde(rename = "contextId")]
pub context_id: String,
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub history: Vec<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<IndexMap<String, Value>>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TaskKind {
#[default]
Task,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskStatus {
pub state: TaskState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum TaskState {
Submitted,
Working,
InputRequired,
Completed,
Canceled,
Failed,
Rejected,
AuthRequired,
Unknown,
}
impl TaskState {
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::Completed | Self::Canceled | Self::Failed | Self::Rejected
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Artifact {
#[serde(rename = "artifactId")]
pub artifact_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub index: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub append: Option<bool>,
#[serde(default, rename = "lastChunk", skip_serializing_if = "Option::is_none")]
pub last_chunk: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<IndexMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskStatusUpdateEvent {
pub kind: StatusUpdateKind,
#[serde(rename = "taskId")]
pub task_id: String,
#[serde(rename = "contextId")]
pub context_id: String,
pub status: TaskStatus,
#[serde(rename = "final")]
pub is_final: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<IndexMap<String, Value>>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum StatusUpdateKind {
#[default]
StatusUpdate,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskArtifactUpdateEvent {
pub kind: ArtifactUpdateKind,
#[serde(rename = "taskId")]
pub task_id: String,
#[serde(rename = "contextId")]
pub context_id: String,
pub artifact: Artifact,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<IndexMap<String, Value>>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ArtifactUpdateKind {
#[default]
ArtifactUpdate,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum StreamingMessageResult {
Task(Task),
Message(Message),
Status(TaskStatusUpdateEvent),
Artifact(TaskArtifactUpdateEvent),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageSendParams {
pub message: Message,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub configuration: Option<MessageSendConfiguration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<IndexMap<String, Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MessageSendConfiguration {
#[serde(
default,
rename = "acceptedOutputModes",
skip_serializing_if = "Vec::is_empty"
)]
pub accepted_output_modes: Vec<String>,
#[serde(
default,
rename = "historyLength",
skip_serializing_if = "Option::is_none"
)]
pub history_length: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocking: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskQueryParams {
pub id: String,
#[serde(
default,
rename = "historyLength",
skip_serializing_if = "Option::is_none"
)]
pub history_length: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskIdParams {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PushNotificationConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authentication: Option<PushNotificationAuthenticationInfo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct PushNotificationAuthenticationInfo {
pub schemes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credentials: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskPushNotificationConfig {
#[serde(rename = "taskId")]
pub task_id: String,
#[serde(rename = "pushNotificationConfig")]
pub push_notification_config: PushNotificationConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetTaskPushNotificationConfigParams {
pub id: String,
#[serde(
default,
rename = "pushNotificationConfigId",
skip_serializing_if = "Option::is_none"
)]
pub push_notification_config_id: Option<String>,
}
pub type ListTaskPushNotificationConfigResult = Vec<TaskPushNotificationConfig>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AgentCard {
pub name: String,
pub description: String,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
pub version: String,
#[serde(
default,
rename = "documentationUrl",
skip_serializing_if = "Option::is_none"
)]
pub documentation_url: Option<String>,
pub capabilities: AgentCapabilities,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authentication: Option<AgentAuthentication>,
#[serde(default, rename = "defaultInputModes")]
pub default_input_modes: Vec<String>,
#[serde(default, rename = "defaultOutputModes")]
pub default_output_modes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<AgentSkill>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct AgentCapabilities {
#[serde(default)]
pub streaming: bool,
#[serde(default, rename = "pushNotifications")]
pub push_notifications: bool,
#[serde(default, rename = "stateTransitionHistory")]
pub state_transition_history: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AgentProvider {
pub organization: String,
pub url: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct AgentAuthentication {
pub schemes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credentials: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AgentSkill {
pub id: String,
pub name: String,
pub description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<String>,
#[serde(default, rename = "inputModes", skip_serializing_if = "Vec::is_empty")]
pub input_modes: Vec<String>,
#[serde(default, rename = "outputModes", skip_serializing_if = "Vec::is_empty")]
pub output_modes: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn part_text_serializes_with_kind_tag() {
let p = Part::text("hello");
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v, json!({"kind": "text", "text": "hello"}));
let back: Part = serde_json::from_value(v).unwrap();
assert_eq!(p, back);
}
#[test]
fn file_part_round_trips_inline_and_uri() {
let inline = Part::File {
file: FilePayload::Inline {
name: Some("a.txt".into()),
mime_type: Some("text/plain".into()),
bytes: "aGk=".into(),
},
metadata: None,
};
let v = serde_json::to_value(&inline).unwrap();
assert_eq!(v["kind"], "file");
assert_eq!(v["file"]["bytes"], "aGk=");
let back: Part = serde_json::from_value(v).unwrap();
assert_eq!(inline, back);
let uri = Part::File {
file: FilePayload::Uri {
name: None,
mime_type: Some("image/png".into()),
uri: "https://example.com/x.png".into(),
},
metadata: None,
};
let v = serde_json::to_value(&uri).unwrap();
assert_eq!(v["file"]["uri"], "https://example.com/x.png");
let back: Part = serde_json::from_value(v).unwrap();
assert_eq!(uri, back);
}
#[test]
fn message_serializes_message_id_and_role() {
let m = Message::user_text("hi");
let v = serde_json::to_value(&m).unwrap();
assert_eq!(v["role"], "user");
assert_eq!(v["kind"], "message");
assert!(v["messageId"].is_string());
assert_eq!(v["parts"][0]["kind"], "text");
let back: Message = serde_json::from_value(v).unwrap();
assert_eq!(m, back);
}
#[test]
fn task_kind_field_round_trips() {
let t = Task {
kind: TaskKind::Task,
id: "t-1".into(),
context_id: "c-1".into(),
status: TaskStatus {
state: TaskState::Working,
message: None,
timestamp: None,
},
artifacts: vec![],
history: vec![],
metadata: None,
};
let v = serde_json::to_value(&t).unwrap();
assert_eq!(v["kind"], "task");
assert_eq!(v["status"]["state"], "working");
let back: Task = serde_json::from_value(v).unwrap();
assert_eq!(t, back);
}
#[test]
fn task_state_terminality() {
assert!(TaskState::Completed.is_terminal());
assert!(TaskState::Canceled.is_terminal());
assert!(TaskState::Failed.is_terminal());
assert!(TaskState::Rejected.is_terminal());
assert!(!TaskState::Working.is_terminal());
assert!(!TaskState::InputRequired.is_terminal());
assert!(!TaskState::Submitted.is_terminal());
}
#[test]
fn input_required_state_serializes_as_kebab() {
let v = serde_json::to_value(TaskState::InputRequired).unwrap();
assert_eq!(v, json!("input-required"));
}
#[test]
fn streaming_event_kinds_are_kebab() {
let st = TaskStatusUpdateEvent {
kind: StatusUpdateKind::StatusUpdate,
task_id: "t".into(),
context_id: "c".into(),
status: TaskStatus {
state: TaskState::Working,
message: None,
timestamp: None,
},
is_final: false,
metadata: None,
};
let v = serde_json::to_value(&st).unwrap();
assert_eq!(v["kind"], "status-update");
assert_eq!(v["final"], false);
let back: TaskStatusUpdateEvent = serde_json::from_value(v).unwrap();
assert_eq!(st, back);
}
#[test]
fn streaming_result_dispatches_to_correct_variant() {
let v = json!({
"kind": "task",
"id": "t-1",
"contextId": "c-1",
"status": {"state": "working"}
});
match serde_json::from_value::<StreamingMessageResult>(v).unwrap() {
StreamingMessageResult::Task(t) => assert_eq!(t.id, "t-1"),
other => panic!("expected Task, got {other:?}"),
}
let v = json!({
"kind": "status-update",
"taskId": "t-1",
"contextId": "c-1",
"status": {"state": "completed"},
"final": true
});
match serde_json::from_value::<StreamingMessageResult>(v).unwrap() {
StreamingMessageResult::Status(s) => assert!(s.is_final),
other => panic!("expected Status, got {other:?}"),
}
}
#[test]
fn push_config_round_trips() {
let cfg = PushNotificationConfig {
id: Some("pn-1".into()),
url: "https://hooks.example.com/cb".into(),
token: Some("secret".into()),
authentication: None,
};
let v = serde_json::to_value(&cfg).unwrap();
assert_eq!(v["url"], "https://hooks.example.com/cb");
assert_eq!(v["token"], "secret");
let back: PushNotificationConfig = serde_json::from_value(v).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn task_push_config_bundle_uses_spec_field_names() {
let bundle = TaskPushNotificationConfig {
task_id: "t-1".into(),
push_notification_config: PushNotificationConfig {
id: None,
url: "https://hooks.example.com/x".into(),
token: None,
authentication: None,
},
};
let v = serde_json::to_value(&bundle).unwrap();
assert_eq!(v["taskId"], "t-1");
assert!(v["pushNotificationConfig"].is_object());
}
#[test]
fn agent_card_round_trips_minimal() {
let card = AgentCard {
name: "Greeter".into(),
description: "Says hi".into(),
url: "https://example.com/a2a".into(),
provider: None,
version: "0.1.0".into(),
documentation_url: None,
capabilities: AgentCapabilities {
streaming: true,
push_notifications: false,
state_transition_history: false,
},
authentication: None,
default_input_modes: vec!["text/plain".into()],
default_output_modes: vec!["text/plain".into()],
skills: vec![],
};
let v = serde_json::to_value(&card).unwrap();
assert_eq!(v["capabilities"]["streaming"], true);
assert_eq!(v["defaultInputModes"][0], "text/plain");
let back: AgentCard = serde_json::from_value(v).unwrap();
assert_eq!(card, back);
}
}