fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn server_types_are_send_sync() {
assert_send_sync::<a2a_protocol_server::RequestHandler>();
assert_send_sync::<a2a_protocol_server::RequestHandlerBuilder>();
assert_send_sync::<a2a_protocol_server::JsonRpcDispatcher>();
assert_send_sync::<a2a_protocol_server::RestDispatcher>();
assert_send_sync::<a2a_protocol_server::StaticAgentCardHandler>();
assert_send_sync::<a2a_protocol_server::InMemoryTaskStore>();
assert_send_sync::<a2a_protocol_server::InMemoryPushConfigStore>();
assert_send_sync::<a2a_protocol_server::ServerError>();
assert_send_sync::<a2a_protocol_server::CorsConfig>();
assert_send_sync::<a2a_protocol_server::TaskStoreConfig>();
assert_send_sync::<a2a_protocol_server::EventQueueManager>();
assert_send_sync::<a2a_protocol_server::InMemoryQueueReader>();
assert_send_sync::<a2a_protocol_server::InMemoryQueueWriter>();
}
#[test]
fn types_types_are_send_sync() {
assert_send_sync::<a2a_protocol_types::task::Task>();
assert_send_sync::<a2a_protocol_types::task::TaskStatus>();
assert_send_sync::<a2a_protocol_types::task::TaskState>();
assert_send_sync::<a2a_protocol_types::task::TaskId>();
assert_send_sync::<a2a_protocol_types::task::ContextId>();
assert_send_sync::<a2a_protocol_types::message::Message>();
assert_send_sync::<a2a_protocol_types::message::Part>();
assert_send_sync::<a2a_protocol_types::message::MessageRole>();
assert_send_sync::<a2a_protocol_types::agent_card::AgentCard>();
assert_send_sync::<a2a_protocol_types::agent_card::AgentCapabilities>();
assert_send_sync::<a2a_protocol_types::agent_card::AgentInterface>();
assert_send_sync::<a2a_protocol_types::agent_card::AgentSkill>();
assert_send_sync::<a2a_protocol_types::events::StreamResponse>();
assert_send_sync::<a2a_protocol_types::events::TaskStatusUpdateEvent>();
assert_send_sync::<a2a_protocol_types::events::TaskArtifactUpdateEvent>();
assert_send_sync::<a2a_protocol_types::jsonrpc::JsonRpcRequest>();
assert_send_sync::<a2a_protocol_types::jsonrpc::JsonRpcError>();
assert_send_sync::<a2a_protocol_types::jsonrpc::JsonRpcVersion>();
assert_send_sync::<a2a_protocol_types::error::A2aError>();
assert_send_sync::<a2a_protocol_types::error::ErrorCode>();
assert_send_sync::<a2a_protocol_types::push::TaskPushNotificationConfig>();
assert_send_sync::<a2a_protocol_types::push::AuthenticationInfo>();
assert_send_sync::<a2a_protocol_types::params::MessageSendParams>();
assert_send_sync::<a2a_protocol_types::params::TaskQueryParams>();
assert_send_sync::<a2a_protocol_types::params::ListTasksParams>();
assert_send_sync::<a2a_protocol_types::params::CancelTaskParams>();
}
mod direct_message_response {
use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
use a2a_protocol_types::params::MessageSendParams;
use a2a_protocol_types::responses::SendMessageResponse;
use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
use a2a_protocol_server::builder::RequestHandlerBuilder;
use a2a_protocol_server::handler::SendMessageResult;
use a2a_protocol_server::{agent_executor, RequestHandler};
struct MessageOnlyExecutor;
agent_executor!(MessageOnlyExecutor, |_ctx, queue| async {
queue
.write(StreamResponse::Message(Message {
id: MessageId::new("agent-reply"),
role: MessageRole::Agent,
parts: vec![Part::text("Direct message response")],
task_id: None,
context_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
}))
.await
});
struct MessageThenCompleteExecutor;
agent_executor!(MessageThenCompleteExecutor, |ctx, queue| async {
queue
.write(StreamResponse::Message(Message {
id: MessageId::new("agent-progress"),
role: MessageRole::Agent,
parts: vec![Part::text("working on it")],
task_id: None,
context_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
}))
.await?;
queue
.write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: ctx.task_id.clone(),
context_id: ContextId::new(ctx.context_id.clone()),
status: TaskStatus::with_timestamp(TaskState::Completed),
metadata: None,
}))
.await
});
fn params(ctx: &str) -> MessageSendParams {
MessageSendParams {
tenant: None,
message: Message {
id: MessageId::new(format!("msg-{ctx}")),
role: MessageRole::User,
parts: vec![Part::text("hello")],
context_id: Some(ContextId::new(ctx)),
task_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
},
configuration: None,
metadata: None,
}
}
async fn send(handler: &RequestHandler, ctx: &str) -> SendMessageResult {
handler
.on_send_message(params(ctx), false, None)
.await
.expect("SendMessage must succeed")
}
#[tokio::test]
async fn message_only_executor_returns_a_message() {
let handler = RequestHandlerBuilder::new(MessageOnlyExecutor)
.build()
.unwrap();
match send(&handler, "ctx-msg-only").await {
SendMessageResult::Response(SendMessageResponse::Message(m)) => {
assert_eq!(m.role, MessageRole::Agent);
assert_eq!(
m.parts.first().and_then(Part::text_content),
Some("Direct message response"),
"the agent's message must be returned verbatim"
);
}
other => panic!("expected a Message response, got: {other:?}"),
}
}
#[tokio::test]
async fn message_then_work_still_returns_the_task() {
let handler = RequestHandlerBuilder::new(MessageThenCompleteExecutor)
.build()
.unwrap();
let task_id = match send(&handler, "ctx-msg-then-work").await {
SendMessageResult::Response(SendMessageResponse::Task(t)) => {
assert_eq!(t.status.state, TaskState::Completed);
t.id
}
other => panic!("expected a Task response, got: {other:?}"),
};
let fetched = handler
.on_get_task(
a2a_protocol_types::params::TaskQueryParams {
tenant: None,
id: task_id.0.clone(),
history_length: Some(10),
},
None,
)
.await
.expect("GetTask must succeed");
let history = fetched.history.unwrap_or_default();
assert!(
history.iter().any(|m| m.role == MessageRole::Agent),
"the agent's message must still be recorded in history: {history:?}"
);
}
#[tokio::test]
async fn no_message_returns_the_task() {
struct Silent;
agent_executor!(Silent, |ctx, queue| async {
queue
.write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: ctx.task_id.clone(),
context_id: ContextId::new(ctx.context_id.clone()),
status: TaskStatus::with_timestamp(TaskState::Completed),
metadata: None,
}))
.await
});
let handler = RequestHandlerBuilder::new(Silent).build().unwrap();
assert!(
matches!(
send(&handler, "ctx-silent").await,
SendMessageResult::Response(SendMessageResponse::Task(_))
),
"an executor that emits no message must still answer with a Task"
);
}
#[tokio::test]
async fn message_only_interaction_still_records_a_task() {
let handler = RequestHandlerBuilder::new(MessageOnlyExecutor)
.build()
.unwrap();
let _ = send(&handler, "ctx-msg-record").await;
let tasks = handler
.on_list_tasks(a2a_protocol_types::params::ListTasksParams::default(), None)
.await
.expect("listing tasks must succeed");
assert_eq!(
tasks.tasks.len(),
1,
"a message-only interaction must still leave a fetchable task"
);
}
}