use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use a2a_protocol_types::error::A2aResult;
use a2a_protocol_types::idempotency::set_key;
use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
use a2a_protocol_types::params::MessageSendParams;
use a2a_protocol_types::task::{Task, TaskId};
use crate::builder::RequestHandlerBuilder;
use crate::executor::AgentExecutor;
use crate::handler::RequestHandler;
use crate::request_context::RequestContext;
use crate::streaming::EventQueueWriter;
use super::super::SendMessageResult;
use a2a_protocol_types::responses::SendMessageResponse;
pub(super) struct CountingExecutor(pub(super) Arc<AtomicUsize>);
impl AgentExecutor for CountingExecutor {
fn execute<'a>(
&'a self,
_ctx: &'a RequestContext,
_queue: &'a dyn EventQueueWriter,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.0.fetch_add(1, Ordering::SeqCst);
Box::pin(async { Ok(()) })
}
}
pub(super) fn counting_handler() -> (RequestHandler, Arc<AtomicUsize>) {
let counter = Arc::new(AtomicUsize::new(0));
let handler = RequestHandlerBuilder::new(CountingExecutor(Arc::clone(&counter)))
.build()
.expect("default build should succeed");
(handler, counter)
}
pub(super) fn keyed_params(msg_id: &str, key: Option<&str>) -> MessageSendParams {
let mut message = Message {
id: MessageId::new(msg_id),
role: MessageRole::User,
parts: vec![Part::text("hello")],
context_id: None,
task_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
};
if let Some(key) = key {
set_key(&mut message, key).expect("test key must be valid");
}
MessageSendParams {
message,
configuration: None,
metadata: None,
tenant: None,
}
}
pub(super) fn task_of(result: &SendMessageResult) -> &Task {
match result {
SendMessageResult::Response(SendMessageResponse::Task(task)) => task,
other => panic!("expected a Task response, got {other:?}"),
}
}
pub(super) fn card() -> a2a_protocol_types::AgentCard {
use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
AgentCard {
url: None,
name: "test".into(),
version: "1.0".into(),
description: "Test agent".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:8080".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
}
}
pub(super) fn advertised(
handler: &RequestHandler,
) -> Option<&a2a_protocol_types::extensions::AgentExtension> {
handler
.agent_card
.as_ref()?
.capabilities
.extensions
.as_ref()?
.iter()
.find(|e| e.uri == a2a_protocol_types::idempotency::IDEMPOTENCY_EXTENSION_URI)
}
#[derive(Default)]
pub(super) struct NoIdempotencyStore {
inner: crate::store::InMemoryTaskStore,
}
#[allow(clippy::manual_async_fn)]
impl crate::store::TaskStore for NoIdempotencyStore {
fn save<'a>(&'a self, t: &'a Task) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.save(t)
}
fn get<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>> {
self.inner.get(id)
}
fn list<'a>(
&'a self,
p: &'a a2a_protocol_types::params::ListTasksParams,
) -> Pin<
Box<
dyn Future<Output = A2aResult<a2a_protocol_types::responses::TaskListResponse>>
+ Send
+ 'a,
>,
> {
self.inner.list(p)
}
fn insert_if_absent<'a>(
&'a self,
t: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>> {
self.inner.insert_if_absent(t)
}
fn delete<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.delete(id)
}
}
pub(super) struct FailFirstSaveStore {
inner: crate::store::InMemoryTaskStore,
failed_once: std::sync::atomic::AtomicBool,
}
impl Default for FailFirstSaveStore {
fn default() -> Self {
Self {
inner: crate::store::InMemoryTaskStore::new(),
failed_once: std::sync::atomic::AtomicBool::new(false),
}
}
}
#[allow(clippy::manual_async_fn)]
impl crate::store::TaskStore for FailFirstSaveStore {
fn save<'a>(&'a self, t: &'a Task) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async move {
if !self.failed_once.swap(true, Ordering::SeqCst) {
return Err(a2a_protocol_types::error::A2aError::internal(
"injected store failure",
));
}
self.inner.save(t).await
})
}
fn get<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>> {
self.inner.get(id)
}
fn list<'a>(
&'a self,
p: &'a a2a_protocol_types::params::ListTasksParams,
) -> Pin<
Box<
dyn Future<Output = A2aResult<a2a_protocol_types::responses::TaskListResponse>>
+ Send
+ 'a,
>,
> {
self.inner.list(p)
}
fn insert_if_absent<'a>(
&'a self,
t: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>> {
self.inner.insert_if_absent(t)
}
fn delete<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.delete(id)
}
fn supports_idempotency(&self) -> bool {
self.inner.supports_idempotency()
}
fn claim_idempotency_key<'a>(
&'a self,
key: &'a str,
message_id: &'a MessageId,
task_id: &'a TaskId,
) -> Pin<
Box<dyn Future<Output = A2aResult<crate::store::task_store::IdempotencyClaim>> + Send + 'a>,
> {
self.inner.claim_idempotency_key(key, message_id, task_id)
}
fn release_idempotency_key<'a>(
&'a self,
key: &'a str,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.release_idempotency_key(key)
}
}
pub(super) struct WithholdingStore {
inner: crate::store::InMemoryTaskStore,
withheld: std::sync::Mutex<Option<(TaskId, usize)>>,
}
impl Default for WithholdingStore {
fn default() -> Self {
Self {
inner: crate::store::InMemoryTaskStore::new(),
withheld: std::sync::Mutex::new(None),
}
}
}
impl WithholdingStore {
pub(super) fn withhold(&self, id: &TaskId, n: usize) {
*self.withheld.lock().expect("test mutex") = Some((id.clone(), n));
}
fn take_withheld(&self, id: &TaskId) -> bool {
let mut armed = self.withheld.lock().expect("test mutex");
match armed.as_mut() {
Some((target, remaining)) if target == id && *remaining > 0 => {
*remaining -= 1;
true
}
_ => false,
}
}
}
#[allow(clippy::manual_async_fn)]
impl crate::store::TaskStore for WithholdingStore {
fn save<'a>(&'a self, t: &'a Task) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.save(t)
}
fn get<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>> {
Box::pin(async move {
if self.take_withheld(id) {
return Ok(None);
}
self.inner.get(id).await
})
}
fn list<'a>(
&'a self,
p: &'a a2a_protocol_types::params::ListTasksParams,
) -> Pin<
Box<
dyn Future<Output = A2aResult<a2a_protocol_types::responses::TaskListResponse>>
+ Send
+ 'a,
>,
> {
self.inner.list(p)
}
fn insert_if_absent<'a>(
&'a self,
t: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>> {
self.inner.insert_if_absent(t)
}
fn delete<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.delete(id)
}
fn supports_idempotency(&self) -> bool {
self.inner.supports_idempotency()
}
fn claim_idempotency_key<'a>(
&'a self,
key: &'a str,
message_id: &'a MessageId,
task_id: &'a TaskId,
) -> Pin<
Box<dyn Future<Output = A2aResult<crate::store::task_store::IdempotencyClaim>> + Send + 'a>,
> {
self.inner.claim_idempotency_key(key, message_id, task_id)
}
fn release_idempotency_key<'a>(
&'a self,
key: &'a str,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.release_idempotency_key(key)
}
}
pub(super) fn withholding_handler(
store: &Arc<WithholdingStore>,
) -> (RequestHandler, Arc<AtomicUsize>) {
let counter = Arc::new(AtomicUsize::new(0));
let handler = RequestHandlerBuilder::new(CountingExecutor(Arc::clone(&counter)))
.with_task_store_arc(Arc::clone(store) as Arc<dyn crate::store::TaskStore>)
.build()
.expect("withholding-store build should succeed");
(handler, counter)
}