mod follow;
#[cfg(test)]
mod tests;
mod wait;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use a2a_lf::{Message, Part, Role, SendMessageConfiguration, SendMessageResponse, Task, TaskState};
use serde_json::Value;
use tokio::time::Instant;
use super::client::A2aClient;
use super::error::A2aError;
use super::output::remote_line;
use crate::runtime::RunOptions;
use follow::{Followed, follow};
use wait::{Wake, wait};
const MAX_PROGRESS_ID_BYTES: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum A2aRequest {
Send(A2aSend),
Resume { task_id: String },
}
impl A2aRequest {
pub fn send(text: impl Into<String>) -> Self {
Self::Send(A2aSend::new(text))
}
pub fn resume(task_id: impl Into<String>) -> Self {
Self::Resume {
task_id: task_id.into(),
}
}
}
impl From<A2aSend> for A2aRequest {
fn from(send: A2aSend) -> Self {
Self::Send(send)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct A2aSend {
pub text: String,
pub task_id: Option<String>,
pub context_id: Option<String>,
pub metadata: HashMap<String, Value>,
pub extensions: Vec<String>,
}
impl A2aSend {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
task_id: None,
context_id: None,
metadata: HashMap::new(),
extensions: Vec::new(),
}
}
pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
self.task_id = Some(task_id.into());
self
}
pub fn with_context_id(mut self, context_id: impl Into<String>) -> Self {
self.context_id = Some(context_id.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn with_extension(mut self, uri: impl Into<String>) -> Self {
let uri = uri.into();
if !self.extensions.contains(&uri) {
self.extensions.push(uri);
}
self
}
fn into_message(self) -> Message {
let mut message = Message::new(Role::User, vec![Part::text(self.text)]);
message.task_id = self.task_id;
message.context_id = self.context_id;
message.metadata = (!self.metadata.is_empty()).then_some(self.metadata);
message.extensions = (!self.extensions.is_empty()).then_some(self.extensions);
message
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum A2aExchange {
Reply(Message),
Settled(Task),
StillWorking { task: Task, reason: A2aStillWorking },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum A2aStillWorking {
WaitBound(Duration),
RunStopping,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum A2aExchangeError {
#[error("the request failed: {0}")]
Send(#[source] A2aError),
#[error("checking on task {task_id} failed: {error}")]
Poll {
task_id: String,
#[source]
error: A2aError,
},
#[error("the run was cancelled while task {task_id} was working{}", cancel_outcome(.canceled))]
Abandoned {
task_id: String,
canceled: Result<(), A2aError>,
},
}
fn cancel_outcome(canceled: &Result<(), A2aError>) -> String {
match canceled {
Ok(()) => "; the remote task was asked to cancel".to_string(),
Err(error) => format!("; asking the remote task to cancel failed: {error}"),
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum A2aSubmitted {
Reply(Message),
Task(Task),
}
pub async fn exchange(
client: &Arc<A2aClient>,
request: A2aRequest,
options: &RunOptions,
progress: &(dyn Fn(String) + Send + Sync),
) -> Result<A2aExchange, A2aExchangeError> {
let started = Instant::now();
match submit(client, request).await? {
A2aSubmitted::Reply(reply) => Ok(A2aExchange::Reply(reply)),
A2aSubmitted::Task(task) => settle_from(client, task, started, options, progress).await,
}
}
pub async fn submit(
client: &Arc<A2aClient>,
request: A2aRequest,
) -> Result<A2aSubmitted, A2aExchangeError> {
let task = match request {
A2aRequest::Send(send) => {
let message = send.into_message();
let configuration = SendMessageConfiguration {
accepted_output_modes: None,
task_push_notification_config: None,
history_length: Some(0),
return_immediately: Some(true),
};
match client
.send_message(message, configuration)
.await
.map_err(A2aExchangeError::Send)?
{
SendMessageResponse::Message(reply) => return Ok(A2aSubmitted::Reply(reply)),
SendMessageResponse::Task(task) => task,
}
}
A2aRequest::Resume { task_id } => client
.get_task(&task_id, Some(0))
.await
.map_err(A2aExchangeError::Send)?,
};
Ok(A2aSubmitted::Task(task))
}
pub async fn settle(
client: &Arc<A2aClient>,
task: Task,
options: &RunOptions,
progress: &(dyn Fn(String) + Send + Sync),
) -> Result<A2aExchange, A2aExchangeError> {
settle_from(client, task, Instant::now(), options, progress).await
}
async fn settle_from(
client: &Arc<A2aClient>,
mut task: Task,
started: Instant,
options: &RunOptions,
progress: &(dyn Fn(String) + Send + Sync),
) -> Result<A2aExchange, A2aExchangeError> {
let limits = client.limits();
let deadline = started + limits.max_wait;
let guard = CancelOnDrop::new(client, &task);
let streams = client.card().capabilities.streaming == Some(true);
if streams && !is_settled(&task.status.state) {
if let Followed::Woke(wake) = follow(client, &mut task, deadline, options, progress).await {
return woken(client, task, wake, guard).await;
}
}
loop {
if is_settled(&task.status.state) {
guard.disarm();
return Ok(A2aExchange::Settled(task));
}
if Instant::now() >= deadline {
return woken(client, task, Wake::Elapsed, guard).await;
}
let next_poll = (Instant::now() + limits.poll_interval).min(deadline);
match wait(next_poll, options).await {
Wake::Elapsed => {}
wake => return woken(client, task, wake, guard).await,
}
let previous = task.status.state.clone();
task = match client.get_task(&task.id, Some(0)).await {
Ok(next) => next,
Err(error) => {
guard.disarm();
return Err(A2aExchangeError::Poll {
task_id: task.id,
error,
});
}
};
if task.status.state != previous {
progress(status_line(&task, None));
}
}
}
fn status_line(task: &Task, said: Option<&str>) -> String {
let mut line = format!(
"remote task {} is {}",
progress_id(&task.id),
state_name(&task.status.state)
);
if let Some(said) = said {
line.push_str(": ");
line.push_str(said);
}
line
}
fn progress_id(task_id: &str) -> String {
remote_line(task_id, MAX_PROGRESS_ID_BYTES)
}
async fn woken(
client: &A2aClient,
task: Task,
wake: Wake,
guard: CancelOnDrop,
) -> Result<A2aExchange, A2aExchangeError> {
guard.disarm();
let reason = match wake {
Wake::Elapsed => A2aStillWorking::WaitBound(client.limits().max_wait),
Wake::Stopping => A2aStillWorking::RunStopping,
Wake::Cancelled => {
let canceled = client.cancel_task(&task.id).await.map(drop);
return Err(A2aExchangeError::Abandoned {
task_id: task.id,
canceled,
});
}
};
Ok(A2aExchange::StillWorking { task, reason })
}
pub fn is_settled(state: &TaskState) -> bool {
state.is_terminal() || matches!(state, TaskState::InputRequired | TaskState::AuthRequired)
}
pub fn state_name(state: &TaskState) -> &'static str {
match state {
TaskState::Unspecified => "unspecified",
TaskState::Submitted => "submitted",
TaskState::Working => "working",
TaskState::Completed => "completed",
TaskState::Failed => "failed",
TaskState::Canceled => "canceled",
TaskState::InputRequired => "input-required",
TaskState::Rejected => "rejected",
TaskState::AuthRequired => "auth-required",
}
}
struct CancelOnDrop {
pending: Option<(Arc<A2aClient>, String)>,
}
impl CancelOnDrop {
fn new(client: &Arc<A2aClient>, task: &Task) -> Self {
Self {
pending: Some((Arc::clone(client), task.id.clone())),
}
}
fn disarm(mut self) {
self.pending = None;
}
}
impl Drop for CancelOnDrop {
fn drop(&mut self) {
let Some((client, task_id)) = self.pending.take() else {
return;
};
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
return;
};
runtime.spawn(async move {
let _ = client.cancel_task(&task_id).await;
});
}
}