mod reload;
mod retry;
use std::{
collections::BTreeSet,
sync::{Arc, Mutex, OnceLock, RwLock},
time::{Duration, Instant},
};
use async_trait::async_trait;
use mentra::a2a::{A2aClient, A2aError as ClientError};
use super::A2aAgent;
use crate::agents::{AgentResolver, OnChange, Resolved, Route, Taught, TaughtAgent, Unresolved};
pub const A2A_RETRY_BACKOFF: Duration = Duration::from_secs(30);
pub(crate) struct RemoteAgent {
agent: A2aAgent,
client: Option<Arc<A2aClient>>,
attempted: Mutex<Instant>,
}
impl RemoteAgent {
fn new(agent: A2aAgent, client: Option<Arc<A2aClient>>) -> Self {
Self {
agent,
client,
attempted: Mutex::new(Instant::now()),
}
}
pub(crate) fn name(&self) -> &str {
self.agent.name()
}
pub(crate) fn client(&self) -> Option<&Arc<A2aClient>> {
self.client.as_ref()
}
}
#[derive(Default)]
pub(crate) struct Roster {
agents: Vec<Arc<RemoteAgent>>,
removed: BTreeSet<String>,
}
impl Roster {
pub(crate) fn agents(&self) -> &[Arc<RemoteAgent>] {
&self.agents
}
pub(crate) fn get(&self, name: &str) -> Option<&Arc<RemoteAgent>> {
self.agents.iter().find(|agent| agent.name() == name)
}
#[cfg(test)]
pub(crate) fn names(&self) -> Vec<String> {
self.agents
.iter()
.map(|agent| agent.name().to_string())
.collect()
}
}
pub(crate) struct ConfiguredAgents {
roster: RwLock<Arc<Roster>>,
backoff: Duration,
on_change: OnceLock<OnChange>,
}
impl ConfiguredAgents {
pub(crate) async fn connect(agents: Vec<A2aAgent>) -> Self {
let connected = connect_each(agents)
.await
.into_iter()
.map(|(agent, client)| {
let client = warned(agent.name(), client);
Arc::new(RemoteAgent::new(agent, client))
})
.collect();
Self::with_roster(Roster {
agents: connected,
removed: BTreeSet::new(),
})
}
fn with_roster(roster: Roster) -> Self {
Self {
roster: RwLock::new(Arc::new(roster)),
backoff: A2A_RETRY_BACKOFF,
on_change: OnceLock::new(),
}
}
fn changed(&self) {
if let Some(on_change) = self.on_change.get() {
on_change();
}
}
fn swap_with(&self, next: impl FnOnce(&Roster) -> Roster) {
let mut roster = self
.roster
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*roster = Arc::new(next(&roster));
}
pub(crate) fn snapshot(&self) -> Arc<Roster> {
Arc::clone(
&self
.roster
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner()),
)
}
#[cfg(test)]
pub(crate) fn names(&self) -> Vec<String> {
self.snapshot().names()
}
}
#[async_trait]
impl AgentResolver for ConfiguredAgents {
async fn resolve(&self, name: &str) -> Result<Resolved, Unresolved> {
self.reach(name).await
}
fn route(&self, name: &str) -> Route {
self.standing(name)
}
fn taught(&self) -> Taught {
self.snapshot()
.agents()
.iter()
.fold(Taught::default(), |taught, agent| {
let mut entry = TaughtAgent::new(agent.name());
if let Some(client) = agent.client() {
entry = entry.with_summary(mentra::a2a::card_summary(client.card()));
}
taught.with_agent(entry)
})
}
fn on_change(&self, hook: OnChange) {
let _ = self.on_change.set(hook);
}
}
async fn connect_each(
agents: Vec<A2aAgent>,
) -> Vec<(A2aAgent, Result<Arc<A2aClient>, ClientError>)> {
let pending: Vec<_> = agents
.into_iter()
.map(|agent| {
let config = agent.config().clone();
let task = tokio::spawn(async move { A2aClient::connect(&config).await });
(agent, task)
})
.collect();
let mut connected = Vec::with_capacity(pending.len());
for (agent, task) in pending {
let client = match task.await {
Ok(result) => result.map(Arc::new),
Err(_) => Err(ClientError::Connect),
};
connected.push((agent, client));
}
connected
}
fn warned(name: &str, client: Result<Arc<A2aClient>, ClientError>) -> Option<Arc<A2aClient>> {
client
.inspect_err(|error| eprintln!("Warning: A2A agent '{name}' failed to connect: {error}"))
.ok()
}
impl std::fmt::Debug for ConfiguredAgents {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let roster = self.snapshot();
f.debug_list()
.entries(
roster
.agents
.iter()
.map(|agent| (agent.name().to_string(), agent.client.is_some())),
)
.finish()
}
}
#[cfg(test)]
impl ConfiguredAgents {
pub(crate) fn unreachable(names: &[&str]) -> Self {
Self::with_roster(Roster {
agents: names
.iter()
.map(|name| {
Arc::new(RemoteAgent::new(
A2aAgent::new(mentra::a2a::A2aAgentConfig::new(
*name,
"http://127.0.0.1:1",
)),
None,
))
})
.collect(),
removed: BTreeSet::new(),
})
}
}
#[cfg(test)]
pub(crate) mod testing;
#[cfg(test)]
mod tests;