codetether_agent/bus/global.rs
1//! Process-wide shared reference to the active [`AgentBus`].
2//!
3//! Spawned sub-agent sessions (created via the `spawn` tool) need to publish
4//! on the same bus as their parent, but the tool trait's [`execute`] method
5//! takes only a [`serde_json::Value`] — there is no way to thread the parent
6//! bus through the call chain without changing every tool.
7//!
8//! Entrypoints (CLI `run`, `serve`, `tui`, `ralph`) call [`set_global`] once
9//! after constructing their bus; factories that build child sessions call
10//! [`global`] to attach the same bus.
11//!
12//! The global is set **at most once** per process. Subsequent calls to
13//! [`set_global`] are no-ops.
14
15use std::sync::{Arc, OnceLock};
16
17use super::AgentBus;
18
19static GLOBAL_BUS: OnceLock<Arc<AgentBus>> = OnceLock::new();
20
21/// Install the process-wide bus. Idempotent — first writer wins.
22pub fn set_global(bus: Arc<AgentBus>) {
23 let _ = GLOBAL_BUS.set(bus);
24}
25
26/// Return a clone of the process-wide bus, if one has been installed.
27pub fn global() -> Option<Arc<AgentBus>> {
28 GLOBAL_BUS.get().cloned()
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[test]
36 fn global_is_none_by_default_or_already_set() {
37 // Cannot reliably test set_global in a unit test because OnceLock
38 // persists across tests in the same binary. We only assert that
39 // `global()` either returns None or returns Some consistently.
40 let first = global().map(|b| Arc::as_ptr(&b) as usize);
41 let second = global().map(|b| Arc::as_ptr(&b) as usize);
42 assert_eq!(first, second);
43 }
44}