bamboo_engine/external_agents/
live.rs1use std::collections::{HashMap, HashSet};
12use std::sync::{Mutex, OnceLock};
13
14use bamboo_domain::poison::PoisonRecover;
15use bamboo_subagent::proto::ParentFrame;
16use tokio::sync::mpsc;
17
18fn map() -> &'static Mutex<HashMap<String, mpsc::UnboundedSender<ParentFrame>>> {
19 static MAP: OnceLock<Mutex<HashMap<String, mpsc::UnboundedSender<ParentFrame>>>> =
20 OnceLock::new();
21 MAP.get_or_init(|| Mutex::new(HashMap::new()))
22}
23
24fn pending() -> &'static Mutex<HashMap<String, HashSet<String>>> {
31 static PENDING: OnceLock<Mutex<HashMap<String, HashSet<String>>>> = OnceLock::new();
32 PENDING.get_or_init(|| Mutex::new(HashMap::new()))
33}
34
35pub fn register_pending_approval(child_id: &str, request_id: &str) {
39 pending()
40 .lock()
41 .recover_poison()
42 .entry(child_id.to_string())
43 .or_default()
44 .insert(request_id.to_string());
45}
46
47pub fn take_pending_approval(child_id: &str, request_id: &str) -> bool {
51 let mut guard = pending().lock().recover_poison();
52 let Some(set) = guard.get_mut(child_id) else {
53 return false;
54 };
55 let took = set.remove(request_id);
56 if set.is_empty() {
57 guard.remove(child_id);
58 }
59 took
60}
61
62pub fn clear_pending_approvals_for(child_id: &str) {
64 pending().lock().recover_poison().remove(child_id);
65}
66
67pub fn deliver_approval_checked(child_id: &str, request_id: &str, approved: bool) -> bool {
75 if take_pending_approval(child_id, request_id) {
76 deliver_approval(child_id, request_id, approved)
77 } else {
78 false
79 }
80}
81
82pub struct LiveActorGuard {
85 child_id: String,
86}
87
88impl Drop for LiveActorGuard {
89 fn drop(&mut self) {
90 map().lock().recover_poison().remove(&self.child_id);
91 clear_pending_approvals_for(&self.child_id);
94 }
95}
96
97pub fn register(child_id: &str, tx: mpsc::UnboundedSender<ParentFrame>) -> LiveActorGuard {
99 map()
100 .lock()
101 .recover_poison()
102 .insert(child_id.to_string(), tx);
103 LiveActorGuard {
104 child_id: child_id.to_string(),
105 }
106}
107
108pub fn deliver_message(child_id: &str, text: &str) -> bool {
111 let guard = map().lock().recover_poison();
112 match guard.get(child_id) {
113 Some(tx) => tx
114 .send(ParentFrame::Message {
115 text: text.to_string(),
116 })
117 .is_ok(),
118 None => false,
119 }
120}
121
122pub fn deliver_approval(child_id: &str, request_id: &str, approved: bool) -> bool {
133 let guard = map().lock().recover_poison();
134 match guard.get(child_id) {
135 Some(tx) => tx
136 .send(ParentFrame::ApprovalReply {
137 id: request_id.to_string(),
138 approved,
139 })
140 .is_ok(),
141 None => false,
142 }
143}
144
145pub fn is_live(child_id: &str) -> bool {
147 map().lock().recover_poison().contains_key(child_id)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn register_deliver_unregister() {
156 let (tx, mut rx) = mpsc::unbounded_channel();
157 let guard = register("c-live", tx);
158 assert!(is_live("c-live"));
159 assert!(deliver_message("c-live", "hi"));
160 match rx.try_recv() {
161 Ok(ParentFrame::Message { text }) => assert_eq!(text, "hi"),
162 other => panic!("expected message frame, got {other:?}"),
163 }
164
165 drop(guard);
166 assert!(!is_live("c-live"));
167 assert!(!deliver_message("c-live", "gone"));
168 }
169
170 #[test]
171 fn deliver_fails_when_receiver_dropped() {
172 let (tx, rx) = mpsc::unbounded_channel();
173 let _guard = register("c-dead", tx);
174 drop(rx);
175 assert!(!deliver_message("c-dead", "hi"));
176 }
177
178 #[test]
179 fn deliver_approval_routes_reply_frame() {
180 let (tx, mut rx) = mpsc::unbounded_channel();
181 let guard = register("c-appr", tx);
182 assert!(deliver_approval("c-appr", "req-7", true));
183 match rx.try_recv() {
184 Ok(ParentFrame::ApprovalReply { id, approved }) => {
185 assert_eq!(id, "req-7");
186 assert!(approved);
187 }
188 other => panic!("expected approval reply, got {other:?}"),
189 }
190 drop(guard);
191 assert!(!deliver_approval("c-appr", "req-8", false));
193 }
194
195 #[test]
196 fn pending_approval_is_one_shot() {
197 register_pending_approval("c-pend", "req-1");
198 assert!(take_pending_approval("c-pend", "req-1"));
200 assert!(!take_pending_approval("c-pend", "req-1"));
201 }
202
203 #[test]
204 fn take_of_unregistered_pair_is_false() {
205 assert!(!take_pending_approval("c-unknown", "req-x"));
207 register_pending_approval("c-known", "req-real");
209 assert!(!take_pending_approval("c-known", "req-bogus"));
210 assert!(take_pending_approval("c-known", "req-real"));
212 }
213
214 #[test]
215 fn deliver_approval_checked_only_delivers_for_registered_pair() {
216 let (tx, mut rx) = mpsc::unbounded_channel();
217 let _guard = register("c-checked", tx);
218
219 assert!(!deliver_approval_checked("c-checked", "req-stray", true));
221 assert!(rx.try_recv().is_err());
222
223 register_pending_approval("c-checked", "req-ok");
225 assert!(deliver_approval_checked("c-checked", "req-ok", true));
226 match rx.try_recv() {
227 Ok(ParentFrame::ApprovalReply { id, approved }) => {
228 assert_eq!(id, "req-ok");
229 assert!(approved);
230 }
231 other => panic!("expected approval reply, got {other:?}"),
232 }
233 assert!(!deliver_approval_checked("c-checked", "req-ok", true));
235 assert!(rx.try_recv().is_err());
236 }
237
238 #[test]
239 fn clear_pending_approvals_for_drops_them() {
240 register_pending_approval("c-clear", "req-a");
241 register_pending_approval("c-clear", "req-b");
242 clear_pending_approvals_for("c-clear");
243 assert!(!take_pending_approval("c-clear", "req-a"));
244 assert!(!take_pending_approval("c-clear", "req-b"));
245 }
246}