use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use a2a_agents_common::llm::{ToolCall, ToolDefinition};
use a2a_rs::domain::{
A2AError, Message, Part, Role, SendCompletion, Task, TaskState, TaskStateExt,
};
use a2a_rs::port::Transport;
use a2a_rs::{StreamEvent, StreamItem};
use async_trait::async_trait;
use buffa::EnumValue;
use futures::{Stream, StreamExt};
use tokio::time::Instant;
#[async_trait]
pub trait ToolSource: Send + Sync {
fn tool_defs(&self) -> Vec<ToolDefinition>;
fn has_tool(&self, name: &str) -> bool;
async fn invoke(&self, task_id: &str, call: &ToolCall) -> Result<String, A2AError>;
}
pub fn resolve<'a>(sources: &'a [Arc<dyn ToolSource>], name: &str) -> Option<&'a dyn ToolSource> {
sources
.iter()
.find(|s| s.has_tool(name))
.map(|s| s.as_ref())
}
pub fn collect_tool_defs(sources: &[Arc<dyn ToolSource>]) -> Vec<ToolDefinition> {
sources.iter().flat_map(|s| s.tool_defs()).collect()
}
type EventStream = Pin<Box<dyn Stream<Item = Result<StreamEvent, A2AError>> + Send>>;
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct PeerUnavailable(String);
impl PeerUnavailable {
pub fn new(reason: impl Into<String>) -> Self {
Self(reason.into())
}
}
#[async_trait]
pub trait PeerResolver: Send + Sync {
async fn resolve(&self) -> Result<Arc<dyn Transport>, PeerUnavailable>;
}
pub struct ConnectedPeer(Arc<dyn Transport>);
impl ConnectedPeer {
pub fn new(transport: Arc<dyn Transport>) -> Self {
Self(transport)
}
}
#[async_trait]
impl PeerResolver for ConnectedPeer {
async fn resolve(&self) -> Result<Arc<dyn Transport>, PeerUnavailable> {
Ok(self.0.clone())
}
}
pub struct A2aAgentToolSource {
agent: String,
tool_name: String,
description: String,
peer: Arc<dyn PeerResolver>,
poll_interval: Duration,
deadline: Duration,
}
impl A2aAgentToolSource {
pub fn new(name: &str, description: String, transport: Arc<dyn Transport>) -> Self {
Self::resolving(name, description, Arc::new(ConnectedPeer::new(transport)))
}
pub fn resolving(name: &str, description: String, peer: Arc<dyn PeerResolver>) -> Self {
Self {
agent: name.to_string(),
tool_name: tool_name_for(name),
description,
peer,
poll_interval: Duration::from_millis(250),
deadline: Duration::from_secs(60),
}
}
pub fn with_deadline(mut self, deadline: Duration) -> Self {
self.deadline = deadline;
self
}
pub fn tool_name(&self) -> &str {
&self.tool_name
}
async fn wait_until_settled(
&self,
transport: &dyn Transport,
remote_task_id: &str,
deadline: Instant,
) -> Result<Task, A2AError> {
if let Ok(stream) = transport
.subscribe_to_task(remote_task_id, Some(1), None)
.await
{
watch_until_settled(stream, deadline).await;
let task = transport.get_task(remote_task_id, Some(1)).await?;
if task_settled(&task) || Instant::now() >= deadline {
return Ok(task);
}
}
self.poll_until_settled(transport, remote_task_id, deadline)
.await
}
async fn poll_until_settled(
&self,
transport: &dyn Transport,
remote_task_id: &str,
deadline: Instant,
) -> Result<Task, A2AError> {
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
tokio::time::sleep(self.poll_interval.min(remaining)).await;
let task = transport.get_task(remote_task_id, Some(1)).await?;
if task_settled(&task) || Instant::now() >= deadline {
return Ok(task);
}
}
}
}
#[async_trait]
impl ToolSource for A2aAgentToolSource {
fn tool_defs(&self) -> Vec<ToolDefinition> {
vec![ToolDefinition {
name: self.tool_name.clone(),
description: self.description.clone(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The natural-language request to send to the agent."
}
},
"required": ["message"]
}),
}]
}
fn has_tool(&self, name: &str) -> bool {
name == self.tool_name
}
async fn invoke(&self, _task_id: &str, call: &ToolCall) -> Result<String, A2AError> {
let args: serde_json::Value = serde_json::from_str(&call.arguments)
.map_err(|e| A2AError::InvalidParams(format!("tool arguments must be JSON: {e}")))?;
let text = args
.get("message")
.and_then(|v| v.as_str())
.ok_or_else(|| A2AError::InvalidParams("missing `message` string argument".into()))?;
let transport = match self.peer.resolve().await {
Ok(transport) => transport,
Err(unavailable) => {
return Ok(format!(
"the '{}' agent is not reachable right now: {unavailable}",
self.agent
));
}
};
let remote_task_id = uuid::Uuid::new_v4().to_string();
let msg = Message::builder()
.role(Role::User)
.parts(vec![Part::text(text.to_string())])
.message_id(uuid::Uuid::new_v4().to_string())
.build();
let deadline = Instant::now() + self.deadline;
let mut task = transport
.send_task_message(
&remote_task_id,
&msg,
None,
Some(1),
SendCompletion::WhenCreated,
)
.await?;
if !task_settled(&task) {
task = self
.wait_until_settled(transport.as_ref(), &remote_task_id, deadline)
.await?;
}
if !task_settled(&task) {
return Err(A2AError::Internal(format!(
"remote agent tool '{}' did not finish within {:?}",
self.tool_name, self.deadline
)));
}
Ok(delegation_result(&self.agent, &task))
}
}
async fn watch_until_settled(mut stream: EventStream, deadline: Instant) {
let drain = async {
while let Some(Ok(event)) = stream.next().await {
if event_settles(&event) {
break;
}
}
};
let _ = tokio::time::timeout_at(deadline, drain).await;
}
fn event_settles(event: &StreamEvent) -> bool {
match &event.item {
StreamItem::Task(task) => task.status.state.is_settled(),
StreamItem::StatusUpdate(update) => update.status.state.is_settled(),
StreamItem::ArtifactUpdate(_) => false,
}
}
pub fn tool_name_for(agent: &str) -> String {
format!("ask_{}", crate::utils::slugify(agent, '_'))
}
fn task_settled(task: &Task) -> bool {
task.status
.as_option()
.map(|s| s.state.is_settled())
.unwrap_or(false)
}
fn delegation_result(agent: &str, task: &Task) -> String {
let reply = task_reply(task);
match outcome_note(&task.status.state) {
Some(note) => format!("the '{agent}' agent {note}: {reply}"),
None => reply,
}
}
fn outcome_note(state: &EnumValue<TaskState>) -> Option<&'static str> {
match state {
EnumValue::Known(TaskState::InputRequired) => {
Some("needs more information before it can answer")
}
EnumValue::Known(TaskState::AuthRequired) => {
Some("needs authentication before it can answer")
}
EnumValue::Known(TaskState::Failed) => Some("failed the request"),
EnumValue::Known(TaskState::Rejected) => Some("rejected the request"),
EnumValue::Known(TaskState::Canceled) => Some("had its task canceled"),
_ => None,
}
}
fn task_reply(task: &Task) -> String {
task.status
.as_option()
.and_then(|s| s.message.as_option())
.map(|m| {
m.parts
.iter()
.filter_map(|p| p.get_text())
.collect::<Vec<_>>()
.join(" ")
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "(the agent returned no text)".to_string())
}
#[cfg(feature = "mcp-server")]
pub use mcp::{McpToolSource, UnusedInner};
#[cfg(feature = "mcp-server")]
mod mcp {
use super::*;
use a2a_mcp::McpToA2ABridge;
use a2a_rs::domain::Task;
use a2a_rs::port::{AsyncMessageHandler, RequestContext};
#[derive(Clone)]
pub struct UnusedInner;
#[async_trait]
impl AsyncMessageHandler for UnusedInner {
async fn process_message(
&self,
_task_id: &str,
_message: &Message,
_ctx: &RequestContext,
) -> Result<Task, A2AError> {
Err(A2AError::UnsupportedOperation(
"the generic LLM handler does not delegate to the MCP bridge".to_string(),
))
}
}
pub struct McpToolSource {
bridge: Arc<McpToA2ABridge<UnusedInner>>,
}
impl McpToolSource {
pub fn new(bridge: Arc<McpToA2ABridge<UnusedInner>>) -> Self {
Self { bridge }
}
}
#[async_trait]
impl ToolSource for McpToolSource {
fn tool_defs(&self) -> Vec<ToolDefinition> {
self.bridge.get_llm_tools()
}
fn has_tool(&self, name: &str) -> bool {
self.bridge.tools().iter().any(|t| t.name.as_ref() == name)
}
async fn invoke(&self, task_id: &str, call: &ToolCall) -> Result<String, A2AError> {
self.bridge
.execute_llm_tool_call(task_id, call)
.await
.map_err(|e| e.to_a2a_error())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_name_is_slugified_and_prefixed() {
assert_eq!(tool_name_for("Weather Agent"), "ask_weather_agent");
assert_eq!(tool_name_for("billing-v2"), "ask_billing_v2");
assert_eq!(tool_name_for(" Spaces "), "ask_spaces");
}
#[derive(Clone)]
struct FakeSource {
name: String,
result: String,
}
#[async_trait]
impl ToolSource for FakeSource {
fn tool_defs(&self) -> Vec<ToolDefinition> {
vec![ToolDefinition {
name: self.name.clone(),
description: "fake".into(),
parameters: serde_json::json!({"type": "object"}),
}]
}
fn has_tool(&self, name: &str) -> bool {
name == self.name
}
async fn invoke(&self, _task_id: &str, _call: &ToolCall) -> Result<String, A2AError> {
Ok(self.result.clone())
}
}
#[test]
fn resolve_picks_owning_source_and_collects_defs() {
let sources: Vec<Arc<dyn ToolSource>> = vec![
Arc::new(FakeSource {
name: "alpha".into(),
result: "a".into(),
}),
Arc::new(FakeSource {
name: "beta".into(),
result: "b".into(),
}),
];
assert!(resolve(&sources, "beta").is_some());
assert!(resolve(&sources, "missing").is_none());
assert_eq!(collect_tool_defs(&sources).len(), 2);
}
#[tokio::test]
async fn resolved_source_executes() {
let sources: Vec<Arc<dyn ToolSource>> = vec![Arc::new(FakeSource {
name: "alpha".into(),
result: "hello".into(),
})];
let src = resolve(&sources, "alpha").unwrap();
let call = ToolCall {
id: "1".into(),
name: "alpha".into(),
arguments: "{}".into(),
};
assert_eq!(src.invoke("t1", &call).await.unwrap(), "hello");
}
}