mod conversation;
mod reload;
mod retry;
use std::{
collections::BTreeSet,
path::PathBuf,
sync::{Arc, Mutex, RwLock},
time::{Duration, Instant},
};
use mentra::a2a::{A2aClient, A2aError as ClientError};
use super::A2aAgent;
pub(crate) use conversation::{Conversation, Outstanding, Waiting, forget};
pub(crate) use retry::{Reached, Route, Unreached};
pub const A2A_RETRY_BACKOFF: Duration = Duration::from_secs(30);
pub(crate) type OnChange = Box<dyn Fn() + Send + Sync>;
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)
}
pub(crate) fn names(&self) -> Vec<String> {
self.agents
.iter()
.map(|agent| agent.name().to_string())
.collect()
}
}
pub(crate) struct RemoteAgents {
roster: RwLock<Arc<Roster>>,
conversations: conversation::Book,
backoff: Duration,
on_change: Option<OnChange>,
hop: u32,
}
impl RemoteAgents {
pub(crate) async fn connect(agents: Vec<A2aAgent>, remembered_in: Option<PathBuf>) -> 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(),
},
remembered_in,
)
}
fn with_roster(roster: Roster, remembered_in: Option<PathBuf>) -> Self {
Self {
roster: RwLock::new(Arc::new(roster)),
conversations: conversation::Book::new(remembered_in),
backoff: A2A_RETRY_BACKOFF,
on_change: None,
hop: 0,
}
}
pub(crate) fn with_hop(self, hop: u32) -> Self {
Self { hop, ..self }
}
pub(crate) fn hop(&self) -> u32 {
self.hop
}
pub(crate) fn with_on_change(self, on_change: OnChange) -> Self {
Self {
on_change: Some(on_change),
..self
}
}
fn changed(&self) {
if let Some(on_change) = &self.on_change {
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()),
)
}
pub(crate) fn names(&self) -> Vec<String> {
self.snapshot().names()
}
pub(crate) fn conversation(
&self,
caller: &str,
remote: &str,
remembered: bool,
) -> Conversation {
self.conversations.get(caller, remote, remembered)
}
pub(crate) fn record(
&self,
caller: &str,
remote: &str,
conversation: Conversation,
remembered: bool,
) -> std::io::Result<()> {
self.conversations
.record(caller, remote, conversation, remembered)
}
}
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 RemoteAgents {
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 RemoteAgents {
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(),
},
None,
)
}
pub(crate) fn unreachable_in(names: &[&str], dir: &std::path::Path) -> Self {
let mut agents = Self::unreachable(names);
agents.conversations = conversation::Book::new(Some(dir.to_path_buf()));
agents
}
}
#[cfg(test)]
pub(crate) mod testing;
#[cfg(test)]
mod tests;