use a2a_protocol_types::idempotency::{
IDEMPOTENCY_EXTENSION_URI, IDEMPOTENCY_METADATA_KEY, key_of,
};
use a2a_protocol_types::message::Message;
use a2a_protocol_types::task::{Task, TaskId};
use crate::error::{ServerError, ServerResult};
use crate::handler::RequestHandler;
use crate::store::task_store::IdempotencyClaim;
pub(super) enum SendKey {
Absent,
Claimed(String),
Replay(Box<Task>),
}
impl RequestHandler {
pub(super) async fn claim_send_key(
&self,
message: &Message,
task_id: &TaskId,
) -> ServerResult<SendKey> {
let key = match key_of(message) {
Ok(None) => return Ok(SendKey::Absent),
Ok(Some(key)) => key,
Err(err) => return Err(ServerError::InvalidParams(err.to_string())),
};
if !self.task_store.supports_idempotency() {
return Err(ServerError::UnsupportedOperation(format!(
"this server's task store cannot honour `{IDEMPOTENCY_METADATA_KEY}`, so the \
send was refused rather than executed without deduplication; the agent card \
does not advertise `{IDEMPOTENCY_EXTENSION_URI}`"
)));
}
match self
.task_store
.claim_idempotency_key(key, &message.id, task_id)
.await?
{
IdempotencyClaim::Claimed => Ok(SendKey::Claimed(key.to_owned())),
IdempotencyClaim::Replay(existing) => {
let task = self.await_claimed_task(&existing).await?;
Ok(SendKey::Replay(Box::new(task)))
}
IdempotencyClaim::Conflict { held_by } => Err(ServerError::InvalidParams(format!(
"idempotency key is already held by message `{held_by}`. A retry resends the \
identical message, so a different message id means the key was reused across \
two distinct sends. Returning the first task here would answer a message that \
was never sent."
))),
}
}
async fn await_claimed_task(&self, existing: &TaskId) -> ServerResult<Task> {
const ATTEMPTS: u32 = 10;
const INTERVAL: std::time::Duration = std::time::Duration::from_millis(25);
if let Some(task) = self.task_store.get(existing).await? {
return Ok(task);
}
for _ in 1..ATTEMPTS {
tokio::time::sleep(INTERVAL).await;
if let Some(task) = self.task_store.get(existing).await? {
return Ok(task);
}
}
Err(ServerError::TaskNotFound(existing.clone()))
}
pub(super) async fn release_send_key(&self, key: &str) {
if let Err(_err) = self.task_store.release_idempotency_key(key).await {
trace_warn!(
error = %_err,
"failed to release an idempotency key after a failed send; a retry of \
that key will report its task as missing rather than re-executing"
);
}
}
}