Why a Task Failed

A failed task is TaskState::Failed plus an error message. The message is prose, written by whoever wrote the executor, so a caller deciding what to do next is matching English — and the decisions are genuinely different:

ClassWhat a caller should do
InvalidRequestNever retry. Fix the request.
TransientRetry with backoff.
PolicyRefusalStop and escalate to a person.
BudgetExhaustedRetry with more budget, or not at all.
InternalThe agent broke. Retry once, then escalate.

Without a class, an orchestrator either retries what can never succeed or abandons what would have worked on the second attempt. Both are visible as bad agent behaviour to whoever is watching.

This is not part of A2A v1.0. It ships as the declared extension https://a2a-rust.com/extensions/failure/v1, so servers stay conformant and the TCK is unaffected.

Reading it

#![allow(unused)]
fn main() {
use a2a_protocol_types::failure::{FailureClass, set_class};
use a2a_protocol_types::message::Message;
use a2a_protocol_types::task::{ContextId, Task, TaskState, TaskStatus};
fn decide(task: &Task) -> &'static str {
match task.failure_class() {
    Some(c) if c.is_retryable() => "back off and try the same request again",
    Some(c) if c.needs_human()  => "escalate; retrying is the wrong response",
    Some(FailureClass::BudgetExhausted) => "retry only with a larger budget",
    Some(_) => "give up; read task.status.message for the detail",
    None => "the agent classified nothing — older peer, or no extension",
}
}
let mut note = Message::agent_text("m1", "rate limited upstream");
set_class(&mut note, FailureClass::Transient);
let mut status = TaskStatus::new(TaskState::Failed);
status.message = Some(note);
let task = Task {
    id: "t1".into(), context_id: ContextId::new("c1"), status,
    history: None, artifacts: None, metadata: None,
};
assert_eq!(decide(&task), "back off and try the same request again");
}

None means the agent said nothing, not that the task succeeded — the state says that. An unrecognised class from a newer peer reads as Internal rather than as an error, so a peer classifying more finely than your build cannot make its failures unreadable.

BudgetExhausted is deliberately not is_retryable(). The identical request hits the identical bound; it is retryable with a larger budget, which is a different request.

Writing it

Most of the time you do not. When an executor returns Err, the server classifies from the error code — which can only ever yield InvalidRequest or Internal, because no ErrorCode carries the meaning "transient" or "refused on policy". An executor deadline is classified BudgetExhausted, since a deadline is a bound that was hit rather than an agent that broke.

The two classes no error code can express need the agent to say so:

#![allow(unused)]
fn main() {
use a2a_protocol_server::{EventEmitter, agent_executor};
use a2a_protocol_types::failure::FailureClass;
use a2a_protocol_types::task::TaskState;
struct RateLimitedAgent;
fn upstream_returned_429() -> bool { true }
agent_executor!(RateLimitedAgent, |ctx, queue| async {
    let emit = EventEmitter::new(ctx, queue);
    emit.status(TaskState::Working).await?;

    if upstream_returned_429() {
        // Retry me: the model was rate limited, not wrong.
        return emit
            .fail(FailureClass::Transient, "upstream model returned 429")
            .await;
    }

    emit.status(TaskState::Completed).await
});
}

The prose stays where it was, on the status message, for the human reading the incident. The class is for the caller deciding what to do about it.

Advertising it

Every server built with RequestHandlerBuilder advertises the extension on its card, because every server classifies — the classification happens in the server's own failure path rather than in a store that may or may not support it. It is never marked required: a client that does not know the extension reads the prose exactly as before. An operator who declares the extension themselves keeps their own entry, required flag included.

Next Steps

  • Idempotent Sends — the other declared extension
  • Error Handling — transport and protocol errors, which are a different thing from a task that ran and failed