use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::message::A2AMessage;
use super::task::{A2ATask, TaskStatus};
pub(crate) fn default_protocol_version() -> String {
"0.3.0".to_string()
}
pub(crate) fn default_input_modes() -> Vec<String> {
vec!["text".to_string()]
}
pub(crate) fn default_output_modes() -> Vec<String> {
vec!["text".to_string()]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2ATaskResult {
pub output: String,
}
impl A2ATaskResult {
pub fn new(output: impl Into<String>) -> Self {
Self {
output: output.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2ATaskDetails {
pub task: A2ATask,
pub result: Option<A2ATaskResult>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AWorkflow {
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub steps: Vec<WorkflowStep>,
}
impl A2AWorkflow {
pub fn new(steps: Vec<WorkflowStep>) -> Self {
Self {
workflow_id: None,
name: None,
steps,
}
}
pub fn with_workflow_id(mut self, id: impl Into<String>) -> Self {
self.workflow_id = Some(id.into());
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowStep {
pub id: String,
pub message: A2AMessage,
#[serde(skip_serializing_if = "Option::is_none")]
pub skill_id: Option<String>,
}
impl WorkflowStep {
pub fn new(id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
id: id.into(),
message: A2AMessage::user(content),
skill_id: None,
}
}
pub fn with_skill(
id: impl Into<String>,
content: impl Into<String>,
skill_id: impl Into<String>,
) -> Self {
Self {
id: id.into(),
message: A2AMessage::user(content),
skill_id: Some(skill_id.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2ARequest {
pub jsonrpc: String,
pub id: u64,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
}
pub mod metadata_keys {
pub const TRACE_ID: &str = "trace_id";
pub const OWNER: &str = "owner";
pub const MESSAGE_ID: &str = "message_id";
}
impl A2ARequest {
pub fn new(id: u64, method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
method: method.into(),
params,
metadata: None,
}
}
pub fn send_task(id: u64, message: &A2AMessage) -> Self {
let params = serde_json::to_value(message)
.ok()
.map(|v| serde_json::json!({ "message": v }));
Self::new(id, "tasks/send", params)
}
pub fn send_task_with_message_id(id: u64, message: &A2AMessage, message_id: &str) -> Self {
Self::send_task(id, message).with_message_id(message_id)
}
pub fn send_envelope(id: u64, envelope: &MessageEnvelope) -> Self {
let mut req = Self::send_task(id, &envelope.message);
if let Some(owner) = &envelope.owner {
req = req.with_owner(owner);
}
if let Some(trace) = &envelope.trace {
req = req.with_trace_id(trace.trace_id.as_str());
}
req
}
pub fn continue_task(id: u64, task_id: &str, message: &A2AMessage) -> Self {
let params = serde_json::to_value(message)
.ok()
.map(|v| serde_json::json!({ "taskId": task_id, "message": v }));
Self::new(id, "tasks/send", params)
}
pub fn get_task(id: u64, task_id: &str) -> Self {
Self::new(
id,
"tasks/get",
Some(serde_json::json!({ "taskId": task_id })),
)
}
pub fn cancel_task(id: u64, task_id: &str) -> Self {
Self::new(
id,
"tasks/cancel",
Some(serde_json::json!({ "taskId": task_id })),
)
}
pub fn run_workflow(id: u64, workflow: &A2AWorkflow) -> Self {
let params = serde_json::to_value(workflow)
.ok()
.map(|v| serde_json::json!({ "workflow": v }));
Self::new(id, "tasks/runWorkflow", params)
}
pub fn with_metadata(mut self, metadata: Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
let meta = self.metadata.get_or_insert_with(|| serde_json::json!({}));
meta[metadata_keys::TRACE_ID] = serde_json::Value::String(trace_id.into());
self
}
pub fn with_owner(mut self, owner: impl Into<String>) -> Self {
let meta = self.metadata.get_or_insert_with(|| serde_json::json!({}));
meta[metadata_keys::OWNER] = serde_json::Value::String(owner.into());
self
}
pub fn with_message_id(mut self, message_id: impl Into<String>) -> Self {
let meta = self.metadata.get_or_insert_with(|| serde_json::json!({}));
meta[metadata_keys::MESSAGE_ID] = serde_json::Value::String(message_id.into());
self
}
pub fn trace_id(&self) -> Option<&str> {
self.metadata
.as_ref()
.and_then(|m| m.get(metadata_keys::TRACE_ID))
.and_then(serde_json::Value::as_str)
}
pub fn owner(&self) -> Option<&str> {
self.metadata
.as_ref()
.and_then(|m| m.get(metadata_keys::OWNER))
.and_then(serde_json::Value::as_str)
}
pub fn message_id(&self) -> Option<&str> {
if let Some(id) = self
.metadata
.as_ref()
.and_then(|m| m.get(metadata_keys::MESSAGE_ID))
.and_then(serde_json::Value::as_str)
{
return Some(id);
}
self.params
.as_ref()
.and_then(|p| p.get("messageId"))
.and_then(serde_json::Value::as_str)
}
pub fn task_id(&self) -> Option<&str> {
self.params
.as_ref()
.and_then(|p| p.get("taskId"))
.and_then(serde_json::Value::as_str)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AResponse {
pub jsonrpc: String,
pub id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<A2AErrorData>,
}
impl A2AResponse {
pub fn ok(id: u64, result: Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: Some(result),
error: None,
}
}
pub fn error(id: u64, code: i32, message: impl Into<String>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(A2AErrorData {
code,
message: message.into(),
}),
}
}
pub fn from_error_data(id: u64, error: A2AErrorData) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(error),
}
}
pub fn is_error(&self) -> bool {
self.error.is_some()
}
pub fn into_result(self) -> Result<Value, A2AErrorData> {
if let Some(err) = self.error {
return Err(err);
}
Ok(self.result.unwrap_or(Value::Null))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AErrorData {
pub code: i32,
pub message: String,
}
impl A2AErrorData {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn method_not_found() -> Self {
Self::new(-32601, "Method not found")
}
pub fn invalid_params(msg: impl Into<String>) -> Self {
Self::new(-32602, msg)
}
pub fn internal_error(msg: impl Into<String>) -> Self {
Self::new(-32603, msg)
}
}
impl std::fmt::Display for A2AErrorData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "A2A Error [{}]: {}", self.code, self.message)
}
}
impl std::error::Error for A2AErrorData {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum TaskPushNotification {
#[serde(rename_all = "camelCase")]
StatusUpdate {
id: String,
status: TaskStatus,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
#[serde(rename_all = "camelCase")]
ArtifactUpdate {
id: String,
artifact: A2ATaskResult,
},
}
impl TaskPushNotification {
pub fn status(id: impl Into<String>, status: TaskStatus) -> Self {
TaskPushNotification::StatusUpdate {
id: id.into(),
status,
error: None,
}
}
pub fn status_with_error(
id: impl Into<String>,
status: TaskStatus,
error: impl Into<String>,
) -> Self {
TaskPushNotification::StatusUpdate {
id: id.into(),
status,
error: Some(error.into()),
}
}
pub fn artifact(id: impl Into<String>, artifact: A2ATaskResult) -> Self {
TaskPushNotification::ArtifactUpdate {
id: id.into(),
artifact,
}
}
pub fn id(&self) -> &str {
match self {
TaskPushNotification::StatusUpdate { id, .. }
| TaskPushNotification::ArtifactUpdate { id, .. } => id,
}
}
pub fn status_value(&self) -> Option<TaskStatus> {
match self {
TaskPushNotification::StatusUpdate { status, .. } => Some(*status),
TaskPushNotification::ArtifactUpdate { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TraceContext {
pub version: u8,
pub trace_id: String,
pub parent_id: String,
pub flags: u8,
}
impl TraceContext {
pub fn new(trace_id: impl Into<String>, parent_id: impl Into<String>) -> Self {
Self {
version: 0,
trace_id: trace_id.into(),
parent_id: parent_id.into(),
flags: 0,
}
}
pub fn sampled(mut self) -> Self {
self.flags |= 0b0000_0001;
self
}
pub fn is_sampled(&self) -> bool {
self.flags & 0b0000_0001 != 0
}
pub fn parse(s: &str) -> Option<Self> {
let mut parts = s.trim().split('-');
let version = parts.next()?;
let trace_id = parts.next()?;
let parent_id = parts.next()?;
let flags = parts.next()?;
if parts.next().is_some() {
return None;
}
let version = u8::from_str_radix(version, 16).ok()?;
if trace_id.len() != 32 || !trace_id.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
if parent_id.len() != 16 || !parent_id.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let flags = u8::from_str_radix(flags, 16).ok()?;
Some(Self {
version,
trace_id: trace_id.to_string(),
parent_id: parent_id.to_string(),
flags,
})
}
pub fn to_traceparent(&self) -> String {
format!(
"{:02x}-{}-{}-{:02x}",
self.version, self.trace_id, self.parent_id, self.flags
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageEnvelope {
pub protocol_version: String,
pub message: A2AMessage,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace: Option<TraceContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, String>,
}
impl MessageEnvelope {
pub fn new(message: A2AMessage) -> Self {
Self {
protocol_version: "0.3.0".to_string(),
message,
trace: None,
owner: None,
headers: HashMap::new(),
}
}
pub fn with_trace(mut self, trace: TraceContext) -> Self {
self.trace = Some(trace);
self
}
pub fn with_owner(mut self, owner: impl Into<String>) -> Self {
self.owner = Some(owner.into());
self
}
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn into_message(self) -> A2AMessage {
self.message
}
}
#[derive(Debug, Clone, Default)]
pub struct TaskFilter {
pub owner: Option<String>,
pub statuses: Option<Vec<TaskStatus>>,
}
impl TaskFilter {
pub fn new() -> Self {
Self::default()
}
pub fn with_owner(mut self, owner: impl Into<String>) -> Self {
self.owner = Some(owner.into());
self
}
pub fn with_statuses(mut self, statuses: Vec<TaskStatus>) -> Self {
self.statuses = Some(statuses);
self
}
pub fn matches(&self, task: &A2ATask) -> bool {
if let Some(owner) = &self.owner {
if task.owner.as_deref() != Some(owner.as_str()) {
return false;
}
}
if let Some(statuses) = &self.statuses {
if !statuses.contains(&task.status) {
return false;
}
}
true
}
}