use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use a2a_protocol_server::builder::RequestHandlerBuilder;
use a2a_protocol_server::handler::SendMessageResult;
use a2a_protocol_server::{
AgentExecutor, EventEmitter, EventQueueWriter, RequestContext, RequestHandler,
};
use a2a_protocol_types::error::{A2aError, A2aResult};
use a2a_protocol_types::failure::FailureClass;
use a2a_protocol_types::message::Message;
use a2a_protocol_types::params::MessageSendParams;
use a2a_protocol_types::responses::SendMessageResponse;
use a2a_protocol_types::task::{Task, TaskState};
struct ErroringExecutor(fn() -> A2aError);
impl AgentExecutor for ErroringExecutor {
fn execute<'a>(
&'a self,
_ctx: &'a RequestContext,
_queue: &'a dyn EventQueueWriter,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async move { Err((self.0)()) })
}
}
struct HangingExecutor;
impl AgentExecutor for HangingExecutor {
fn execute<'a>(
&'a self,
_ctx: &'a RequestContext,
_queue: &'a dyn EventQueueWriter,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async move {
std::future::pending::<()>().await;
Ok(())
})
}
}
struct SelfClassifyingExecutor(FailureClass);
impl AgentExecutor for SelfClassifyingExecutor {
fn execute<'a>(
&'a self,
ctx: &'a RequestContext,
queue: &'a dyn EventQueueWriter,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async move {
let emit = EventEmitter::new(ctx, queue);
emit.status(TaskState::Working).await?;
emit.fail(self.0, "the upstream model returned 429").await
})
}
}
fn params() -> MessageSendParams {
MessageSendParams::new(Message::user_text("m1", "go"))
}
async fn failed_task(handler: &RequestHandler) -> Task {
match handler
.on_send_message(params(), false, None)
.await
.expect("a failed task is a task, not a transport error")
{
SendMessageResult::Response(SendMessageResponse::Task(task)) => task,
other => panic!("expected a task, got {other:?}"),
}
}
#[tokio::test]
async fn a_bad_request_is_classified_as_one_and_never_invites_a_retry() {
let handler = RequestHandlerBuilder::new(ErroringExecutor(|| {
A2aError::invalid_params("the postcode is not a postcode")
}))
.build()
.expect("handler");
let task = failed_task(&handler).await;
assert_eq!(task.status.state, TaskState::Failed);
assert_eq!(task.failure_class(), Some(FailureClass::InvalidRequest));
assert!(
!task.failure_class().expect("classified").is_retryable(),
"retrying a request that was wrong will produce the same answer"
);
}
#[tokio::test]
async fn an_internal_error_is_classified_as_internal() {
let handler = RequestHandlerBuilder::new(ErroringExecutor(|| A2aError::internal("boom")))
.build()
.expect("handler");
assert_eq!(
failed_task(&handler).await.failure_class(),
Some(FailureClass::Internal)
);
}
#[tokio::test]
async fn a_deadline_is_budget_exhausted_rather_than_internal() {
let handler = RequestHandlerBuilder::new(HangingExecutor)
.with_executor_timeout(Duration::from_millis(50))
.build()
.expect("handler");
let task = failed_task(&handler).await;
assert_eq!(task.status.state, TaskState::Failed);
assert_eq!(task.failure_class(), Some(FailureClass::BudgetExhausted));
assert!(!task.failure_class().expect("classified").is_retryable());
}
#[tokio::test]
async fn an_executor_can_state_a_class_no_error_code_could_express() {
let handler = RequestHandlerBuilder::new(SelfClassifyingExecutor(FailureClass::Transient))
.build()
.expect("handler");
let task = failed_task(&handler).await;
assert_eq!(task.status.state, TaskState::Failed);
assert_eq!(task.failure_class(), Some(FailureClass::Transient));
assert!(
task.failure_class().expect("classified").is_retryable(),
"this is the class that tells an orchestrator to back off and retry"
);
assert_eq!(
task.status
.message
.as_ref()
.and_then(Message::text)
.unwrap_or_default(),
"the upstream model returned 429",
"the prose stays for a human; the class is for the caller"
);
}
#[tokio::test]
async fn a_policy_refusal_asks_for_a_person_rather_than_a_retry() {
let handler = RequestHandlerBuilder::new(SelfClassifyingExecutor(FailureClass::PolicyRefusal))
.build()
.expect("handler");
let class = failed_task(&handler)
.await
.failure_class()
.expect("classified");
assert!(class.needs_human());
assert!(!class.is_retryable());
}
#[tokio::test]
async fn a_class_carried_on_the_error_wins_over_the_code() {
let handler = RequestHandlerBuilder::new(ErroringExecutor(|| {
A2aError::with_data(
a2a_protocol_types::error::ErrorCode::InternalError,
"downstream timed out",
serde_json::json!({ a2a_protocol_types::failure::FAILURE_METADATA_KEY: "transient" }),
)
}))
.build()
.expect("handler");
let task = failed_task(&handler).await;
assert_eq!(task.status.state, TaskState::Failed);
assert_eq!(task.failure_class(), Some(FailureClass::Transient));
}
#[tokio::test]
async fn unrelated_error_data_leaves_the_code_to_decide() {
let handler = RequestHandlerBuilder::new(ErroringExecutor(|| {
A2aError::with_data(
a2a_protocol_types::error::ErrorCode::InvalidParams,
"bad",
serde_json::json!([{ "reason": "whatever" }]),
)
}))
.build()
.expect("handler");
assert_eq!(
failed_task(&handler).await.failure_class(),
Some(FailureClass::InvalidRequest)
);
}