1use std::time::Duration;
11
12use okbot_core::{Budget, DelegationContext, Origin, PeerError, Secret};
13use serde_json::Value;
14
15use crate::a2a;
16use crate::manifest::{Link, Manifest, Member};
17
18pub const TEAM: &str = "external";
20
21#[derive(Debug, Clone)]
23pub struct ExternalAgent {
24 pub name: String,
26 pub url: String,
28 pub token: Option<Secret<String>>,
30 pub version: a2a::Version,
32 pub description: String,
34 pub skills: Vec<String>,
36}
37
38#[derive(Debug, Clone)]
40pub struct ExternalClient {
41 pub name: String,
43 pub token: Secret<String>,
45 pub person: Option<String>,
47}
48
49#[derive(Debug, Clone, Default)]
51pub struct ExternalConfig {
52 pub agents: Vec<ExternalAgent>,
54 pub clients: Vec<ExternalClient>,
56 pub max_concurrent: u32,
58 pub task_timeout: Duration,
60}
61
62impl ExternalConfig {
63 pub(crate) fn manifest(&self, me: &str) -> Manifest {
66 let mut m = Manifest::new(TEAM);
67 m.team.version = 1;
68 m.members.insert(me.to_string(), Member::default());
69 for agent in &self.agents {
70 m.members.insert(
71 agent.name.clone(),
72 Member {
73 url: Some(agent.url.clone()),
74 skills: agent.skills.clone(),
75 description: agent.description.clone(),
76 ..Member::default()
77 },
78 );
79 }
80 for client in &self.clients {
81 m.members.insert(
82 client.name.clone(),
83 Member {
84 roles: vec![client.name.clone()],
85 ..Member::default()
86 },
87 );
88 }
89 if !self.clients.is_empty() {
90 m.links.push(Link {
91 from: self.clients.iter().map(|c| c.name.clone()).collect(),
92 to: vec![me.to_string()],
93 });
94 }
95 if !self.agents.is_empty() {
96 m.links.push(Link {
97 from: vec![me.to_string()],
98 to: self.agents.iter().map(|a| a.name.clone()).collect(),
99 });
100 }
101 m
102 }
103
104 pub(crate) fn client(&self, bearer: &str) -> Option<&ExternalClient> {
106 self.clients.iter().find(|c| same(c.token.expose(), bearer))
107 }
108
109 pub(crate) fn agent(&self, name: &str) -> Option<&ExternalAgent> {
110 self.agents.iter().find(|a| a.name == name)
111 }
112}
113
114fn same(a: &str, b: &str) -> bool {
116 a.len() == b.len()
117 && a.bytes()
118 .zip(b.bytes())
119 .fold(0u8, |acc, (x, y)| acc | (x ^ y))
120 == 0
121}
122
123pub(crate) fn context(
126 client: &ExternalClient,
127 now: jiff::Timestamp,
128 timeout: Duration,
129) -> DelegationContext {
130 DelegationContext {
131 origin: Origin {
132 bot: client.name.clone(),
133 conversation: String::new(),
134 person: client.person.clone(),
135 name: client.name.clone(),
136 },
137 chain: vec![client.name.clone()],
138 deadline: now
139 + jiff::SignedDuration::try_from(timeout)
140 .unwrap_or(jiff::SignedDuration::from_secs(600)),
141 budget: Budget {
142 turns: 1,
143 tokens: None,
144 },
145 trace: hex::encode(rand::random::<[u8; 8]>()),
146 }
147}
148
149pub(crate) async fn call(
151 http: &reqwest::Client,
152 agent: &ExternalAgent,
153 request: &a2a::Request,
154) -> Result<Value, PeerError> {
155 let mut post = http
156 .post(&agent.url)
157 .header("content-type", "application/json")
158 .header(a2a::VERSION_HEADER, agent.version.header())
159 .json(request);
160 if let Some(token) = &agent.token {
161 post = post.bearer_auth(token.expose());
162 }
163 let reply = post.send().await.map_err(|e| {
164 PeerError::Unreachable(if e.is_timeout() {
165 "timed out".into()
166 } else {
167 e.without_url().to_string()
168 })
169 })?;
170 let status = reply.status().as_u16();
171 if !(200..300).contains(&status) {
172 return Err(PeerError::Refused(format!(
173 "`{}` answered HTTP {status}",
174 agent.name
175 )));
176 }
177 reply
178 .json()
179 .await
180 .map_err(|_| PeerError::Refused(format!("`{}` sent invalid JSON", agent.name)))
181}