use std::time::Duration;
use okbot_core::{Budget, DelegationContext, Origin, PeerError, Secret};
use serde_json::Value;
use crate::a2a;
use crate::manifest::{Link, Manifest, Member};
pub const TEAM: &str = "external";
#[derive(Debug, Clone)]
pub struct ExternalAgent {
pub name: String,
pub url: String,
pub token: Option<Secret<String>>,
pub version: a2a::Version,
pub description: String,
pub skills: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ExternalClient {
pub name: String,
pub token: Secret<String>,
pub person: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ExternalConfig {
pub agents: Vec<ExternalAgent>,
pub clients: Vec<ExternalClient>,
pub max_concurrent: u32,
pub task_timeout: Duration,
}
impl ExternalConfig {
pub(crate) fn manifest(&self, me: &str) -> Manifest {
let mut m = Manifest::new(TEAM);
m.team.version = 1;
m.members.insert(me.to_string(), Member::default());
for agent in &self.agents {
m.members.insert(
agent.name.clone(),
Member {
url: Some(agent.url.clone()),
skills: agent.skills.clone(),
description: agent.description.clone(),
..Member::default()
},
);
}
for client in &self.clients {
m.members.insert(
client.name.clone(),
Member {
roles: vec![client.name.clone()],
..Member::default()
},
);
}
if !self.clients.is_empty() {
m.links.push(Link {
from: self.clients.iter().map(|c| c.name.clone()).collect(),
to: vec![me.to_string()],
});
}
if !self.agents.is_empty() {
m.links.push(Link {
from: vec![me.to_string()],
to: self.agents.iter().map(|a| a.name.clone()).collect(),
});
}
m
}
pub(crate) fn client(&self, bearer: &str) -> Option<&ExternalClient> {
self.clients.iter().find(|c| same(c.token.expose(), bearer))
}
pub(crate) fn agent(&self, name: &str) -> Option<&ExternalAgent> {
self.agents.iter().find(|a| a.name == name)
}
}
fn same(a: &str, b: &str) -> bool {
a.len() == b.len()
&& a.bytes()
.zip(b.bytes())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}
pub(crate) fn context(
client: &ExternalClient,
now: jiff::Timestamp,
timeout: Duration,
) -> DelegationContext {
DelegationContext {
origin: Origin {
bot: client.name.clone(),
conversation: String::new(),
person: client.person.clone(),
name: client.name.clone(),
},
chain: vec![client.name.clone()],
deadline: now
+ jiff::SignedDuration::try_from(timeout)
.unwrap_or(jiff::SignedDuration::from_secs(600)),
budget: Budget {
turns: 1,
tokens: None,
},
trace: hex::encode(rand::random::<[u8; 8]>()),
}
}
pub(crate) async fn call(
http: &reqwest::Client,
agent: &ExternalAgent,
request: &a2a::Request,
) -> Result<Value, PeerError> {
let mut post = http
.post(&agent.url)
.header("content-type", "application/json")
.header(a2a::VERSION_HEADER, agent.version.header())
.json(request);
if let Some(token) = &agent.token {
post = post.bearer_auth(token.expose());
}
let reply = post.send().await.map_err(|e| {
PeerError::Unreachable(if e.is_timeout() {
"timed out".into()
} else {
e.without_url().to_string()
})
})?;
let status = reply.status().as_u16();
if !(200..300).contains(&status) {
return Err(PeerError::Refused(format!(
"`{}` answered HTTP {status}",
agent.name
)));
}
reply
.json()
.await
.map_err(|_| PeerError::Refused(format!("`{}` sent invalid JSON", agent.name)))
}