use crate::error::{ServerError, ServerResult};
use super::RequestHandler;
impl RequestHandler {
pub(crate) fn ensure_streaming_supported(&self) -> ServerResult<()> {
if let Some(card) = &self.agent_card {
if card.capabilities.streaming != Some(true) {
return Err(ServerError::UnsupportedOperation(
"agent does not support streaming (AgentCard.capabilities.streaming is not true)"
.into(),
));
}
}
Ok(())
}
pub(crate) fn ensure_push_supported(&self) -> ServerResult<()> {
if let Some(card) = &self.agent_card {
if card.capabilities.push_notifications != Some(true) {
return Err(ServerError::PushNotSupported);
}
}
Ok(())
}
pub(crate) fn ensure_required_extensions(
&self,
ctx: &crate::call_context::CallContext,
) -> ServerResult<()> {
if self.required_extensions.is_empty() {
return Ok(());
}
let declared = ctx.extensions();
let missing: Vec<&str> = self
.required_extensions
.iter()
.filter(|uri| !declared.iter().any(|d| d == *uri))
.map(String::as_str)
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(ServerError::Protocol(
a2a_protocol_types::error::A2aError::extension_support_required(format!(
"this agent requires extension support the client did not declare \
(send them in the A2A-Extensions header): {}",
missing.join(", ")
)),
))
}
}
#[must_use]
pub fn activated_extensions(
&self,
headers: &std::collections::HashMap<String, String>,
) -> Vec<String> {
if self.declared_extensions.is_empty() {
return Vec::new();
}
super::helpers::parse_extensions_header(headers)
.into_iter()
.filter(|uri| self.declared_extensions.iter().any(|d| d == uri))
.collect()
}
pub(crate) fn activated_extensions_header_value(&self, raw: Option<&str>) -> Option<String> {
let raw = raw?;
if self.declared_extensions.is_empty() {
return None;
}
let activated: Vec<&str> = raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.filter(|uri| self.declared_extensions.iter().any(|d| d == uri))
.collect();
if activated.is_empty() {
None
} else {
Some(activated.join(", "))
}
}
}
#[cfg(test)]
mod tests {
use a2a_protocol_types::agent_card::{AgentCapabilities, AgentCard, AgentInterface};
use crate::agent_executor;
use crate::builder::RequestHandlerBuilder;
use crate::error::ServerError;
struct DummyExecutor;
agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
fn card_with(caps: AgentCapabilities) -> AgentCard {
AgentCard {
url: None,
name: "Test Agent".into(),
description: "A test agent".into(),
version: "1.0.0".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:8080".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
capabilities: caps,
provider: None,
icon_url: None,
documentation_url: None,
security_schemes: None,
security_requirements: None,
signatures: None,
}
}
#[test]
fn no_card_allows_streaming_and_push() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
assert!(handler.ensure_streaming_supported().is_ok());
assert!(handler.ensure_push_supported().is_ok());
}
#[test]
fn card_without_streaming_rejects_streaming() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with(AgentCapabilities::none()))
.build()
.unwrap();
assert!(matches!(
handler.ensure_streaming_supported(),
Err(ServerError::UnsupportedOperation(_))
));
}
#[test]
fn card_with_streaming_allows_streaming() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with(AgentCapabilities::none().with_streaming(true)))
.build()
.unwrap();
assert!(handler.ensure_streaming_supported().is_ok());
}
#[test]
fn card_without_push_rejects_push() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with(AgentCapabilities::none()))
.build()
.unwrap();
assert!(matches!(
handler.ensure_push_supported(),
Err(ServerError::PushNotSupported)
));
}
#[test]
fn card_with_push_allows_push() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with(
AgentCapabilities::none().with_push_notifications(true),
))
.build()
.unwrap();
assert!(handler.ensure_push_supported().is_ok());
}
fn card_with_extensions() -> AgentCard {
use a2a_protocol_types::extensions::AgentExtension;
let mut caps = AgentCapabilities::none();
caps.extensions = Some(vec![
AgentExtension {
uri: "https://example.com/ext/required/v1".into(),
description: None,
required: Some(true),
params: None,
},
AgentExtension {
uri: "https://example.com/ext/optional/v1".into(),
description: None,
required: Some(false),
params: None,
},
]);
card_with(caps)
}
fn ctx_with_extensions(exts: &[&str]) -> crate::call_context::CallContext {
let mut headers = std::collections::HashMap::new();
if !exts.is_empty() {
headers.insert("a2a-extensions".to_owned(), exts.join(","));
}
crate::handler::helpers::build_call_context("Test", Some(&headers))
}
#[tokio::test]
async fn missing_required_extension_is_rejected() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with_extensions())
.build()
.unwrap();
let ctx = ctx_with_extensions(&[]);
let err = handler
.ensure_required_extensions(&ctx)
.expect_err("client without the required extension must be rejected");
assert!(
matches!(err, ServerError::Protocol(ref e)
if e.code == a2a_protocol_types::error::ErrorCode::ExtensionSupportRequired
&& e.message.contains("https://example.com/ext/required/v1")),
"expected ExtensionSupportRequired naming the URI, got: {err:?}"
);
}
#[tokio::test]
async fn declared_required_extension_is_accepted() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with_extensions())
.build()
.unwrap();
let ctx = ctx_with_extensions(&["https://example.com/ext/required/v1"]);
assert!(handler.ensure_required_extensions(&ctx).is_ok());
}
#[tokio::test]
async fn optional_extension_absence_is_fine() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with_extensions())
.build()
.unwrap();
let ctx = ctx_with_extensions(&[
"https://example.com/ext/required/v1",
"https://other.example/uninvolved",
]);
assert!(handler.ensure_required_extensions(&ctx).is_ok());
}
#[tokio::test]
async fn no_card_no_required_extensions() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let ctx = ctx_with_extensions(&[]);
assert!(handler.ensure_required_extensions(&ctx).is_ok());
}
#[test]
fn activated_extensions_header_intersects_with_declared() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with_extensions())
.build()
.unwrap();
let echoed = handler.activated_extensions_header_value(Some(
"https://example.com/ext/optional/v1, https://unknown.example/ext/v9",
));
assert_eq!(
echoed.as_deref(),
Some("https://example.com/ext/optional/v1")
);
assert_eq!(handler.activated_extensions_header_value(None), None);
assert_eq!(
handler.activated_extensions_header_value(Some("https://unknown.example/ext/v9")),
None
);
}
#[test]
fn activated_extensions_returns_the_declared_intersection() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with_extensions())
.build()
.unwrap();
let mut headers = std::collections::HashMap::new();
headers.insert(
"a2a-extensions".to_owned(),
"https://example.com/ext/optional/v1,https://unknown.example/ext/v9".to_owned(),
);
assert_eq!(
handler.activated_extensions(&headers),
vec!["https://example.com/ext/optional/v1".to_owned()]
);
assert!(handler
.activated_extensions(&std::collections::HashMap::new())
.is_empty());
}
#[tokio::test]
async fn get_task_enforces_required_extension() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card_with_extensions())
.build()
.unwrap();
let params = a2a_protocol_types::params::TaskQueryParams {
tenant: None,
id: "missing-task".into(),
history_length: None,
};
let err = handler
.on_get_task(params.clone(), None)
.await
.expect_err("must reject undeclared client");
assert!(
matches!(err, ServerError::Protocol(ref e)
if e.code == a2a_protocol_types::error::ErrorCode::ExtensionSupportRequired),
"expected ExtensionSupportRequired, got: {err:?}"
);
let mut headers = std::collections::HashMap::new();
headers.insert(
"a2a-extensions".to_owned(),
"https://example.com/ext/required/v1".to_owned(),
);
let err = handler
.on_get_task(params, Some(&headers))
.await
.expect_err("task does not exist");
assert!(
matches!(err, ServerError::TaskNotFound(_)),
"expected TaskNotFound once the extension is declared, got: {err:?}"
);
}
}