use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::Mutex;
use tracing::debug;
use a2a_rs::port::Transport;
use super::{AgentId, AgentRegistry};
use crate::handlers::tools::{PeerResolver, PeerUnavailable};
enum Lookup {
Endpoint(String),
Id(Arc<dyn AgentRegistry>, AgentId),
Skill(Arc<dyn AgentRegistry>, String),
}
struct Connected {
endpoint: String,
transport: Arc<dyn Transport>,
}
pub struct DiscoveredPeer {
lookup: Lookup,
connected: Mutex<Option<Connected>>,
}
impl DiscoveredPeer {
pub fn at(endpoint: impl Into<String>) -> Self {
Self::with(Lookup::Endpoint(endpoint.into()))
}
pub fn by_id(registry: Arc<dyn AgentRegistry>, id: AgentId) -> Self {
Self::with(Lookup::Id(registry, id))
}
pub fn by_skill(registry: Arc<dyn AgentRegistry>, skill: impl Into<String>) -> Self {
Self::with(Lookup::Skill(registry, skill.into()))
}
fn with(lookup: Lookup) -> Self {
Self {
lookup,
connected: Mutex::new(None),
}
}
async fn endpoint(&self) -> Result<String, PeerUnavailable> {
match &self.lookup {
Lookup::Endpoint(url) => Ok(url.clone()),
Lookup::Id(registry, id) => registry
.get(id)
.await
.map_err(|e| PeerUnavailable::new(format!("registry lookup failed: {e}")))?
.map(|found| found.endpoint)
.ok_or_else(|| {
PeerUnavailable::new(format!("no agent is registered with id '{id}'"))
}),
Lookup::Skill(registry, skill) => {
let mut matches = registry
.find_by_skill(skill)
.await
.map_err(|e| PeerUnavailable::new(format!("registry lookup failed: {e}")))?;
if matches.is_empty() {
return Err(PeerUnavailable::new(format!(
"no agent advertises the '{skill}' skill"
)));
}
if matches.len() > 1 {
debug!(
skill,
candidates = matches.len(),
"several agents advertise this skill; delegating to the first"
);
}
Ok(matches.remove(0).endpoint)
}
}
}
}
#[async_trait]
impl PeerResolver for DiscoveredPeer {
async fn resolve(&self) -> Result<Arc<dyn Transport>, PeerUnavailable> {
let endpoint = self.endpoint().await?;
let mut connected = self.connected.lock().await;
if let Some(existing) = connected.as_ref()
&& existing.endpoint == endpoint
{
return Ok(existing.transport.clone());
}
let transport: Arc<dyn Transport> = a2a_rs::auto_connect(&endpoint)
.await
.map_err(|e| PeerUnavailable::new(format!("could not connect to {endpoint}: {e}")))?
.into();
*connected = Some(Connected {
endpoint,
transport: transport.clone(),
});
Ok(transport)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::InMemoryAgentRegistry;
use a2a_rs::domain::{AgentCard, AgentSkill};
fn card(name: &str, skill: &str) -> AgentCard {
let mut card = AgentCard {
name: name.to_string(),
..Default::default()
};
card.skills = vec![AgentSkill::new(
skill.to_string(),
"S".to_string(),
"d".to_string(),
vec![],
)];
card
}
#[tokio::test]
async fn an_unregistered_peer_is_unavailable_not_an_error() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
let peer = DiscoveredPeer::by_id(registry, AgentId::from("billing"));
let Err(err) = peer.resolve().await else {
panic!("nothing is registered, so nothing can resolve");
};
assert!(err.to_string().contains("billing"), "{err}");
}
#[tokio::test]
async fn an_unmatched_skill_names_the_skill() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
let peer = DiscoveredPeer::by_skill(registry, "forecasting");
let Err(err) = peer.resolve().await else {
panic!("nothing advertises the skill, so nothing can resolve");
};
assert!(err.to_string().contains("forecasting"), "{err}");
}
#[tokio::test]
async fn a_late_joiner_is_found_by_a_lookup_that_missed_before() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
let peer = DiscoveredPeer::by_skill(registry.clone(), "forecasting");
assert!(peer.endpoint().await.is_err(), "nothing registered yet");
registry
.register(
card("Weather", "forecasting"),
"http://late:8080".to_string(),
)
.await
.unwrap();
assert_eq!(peer.endpoint().await.unwrap(), "http://late:8080");
}
#[tokio::test]
async fn a_moved_peer_resolves_to_its_new_endpoint() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
registry
.register(
card("Weather", "forecasting"),
"http://old:8080".to_string(),
)
.await
.unwrap();
let peer = DiscoveredPeer::by_id(registry.clone(), AgentId::from("weather"));
assert_eq!(peer.endpoint().await.unwrap(), "http://old:8080");
registry
.register(
card("Weather", "forecasting"),
"http://new:8080".to_string(),
)
.await
.unwrap();
assert_eq!(peer.endpoint().await.unwrap(), "http://new:8080");
}
}