bamboo_engine/runtime/execution/
event_forwarder.rs1use std::collections::HashMap;
8use std::sync::Arc;
9
10use chrono::Utc;
11use tokio::sync::{broadcast, mpsc, RwLock};
12
13use bamboo_agent_core::AgentEvent;
14
15use super::runner_state::AgentRunner;
16
17pub type AccountFeedInbox = mpsc::Sender<(Option<String>, AgentEvent)>;
22
23fn mirror_to_account_feed(inbox: &Option<AccountFeedInbox>, session_id: &str, event: &AgentEvent) {
29 if let Some(inbox) = inbox {
30 if event.is_durable_change() {
31 let route_session_id = event.session_id().unwrap_or(session_id);
32 let _ = inbox.try_send((Some(route_session_id.to_string()), event.clone()));
33 }
34 }
35}
36
37#[cfg(test)]
38#[allow(clippy::items_after_test_module)]
39mod tests {
40 use super::*;
41
42 #[tokio::test]
43 async fn child_approval_change_routes_to_parent_account_envelope() {
44 let (tx, mut rx) = mpsc::channel(4);
45 let event = AgentEvent::ChildApprovalChanged {
46 parent_session_id: "parent-1".into(),
47 child_session_id: "child-1".into(),
48 child_attempt: 1,
49 request_id: "req-1".into(),
50 version: 2,
51 status: "approved".into(),
52 reason: None,
53 tool_name: "Bash".into(),
54 permission: "execute".into(),
55 resource: "/tmp/x".into(),
56 created_at: "2026-01-01T00:00:00Z".into(),
57 resolved_at: Some("2026-01-01T00:00:01Z".into()),
58 };
59
60 mirror_to_account_feed(&Some(tx), "child-1", &event);
61 let (session_id, mirrored) = rx.recv().await.unwrap();
62 assert_eq!(session_id.as_deref(), Some("parent-1"));
63 assert!(matches!(mirrored, AgentEvent::ChildApprovalChanged { .. }));
64 }
65}
66
67pub fn create_event_forwarder(
76 session_id: String,
77 broadcast_tx: broadcast::Sender<AgentEvent>,
78 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
79 account_feed_inbox: Option<AccountFeedInbox>,
80) -> (mpsc::Sender<AgentEvent>, tokio::task::JoinHandle<()>) {
81 let (mpsc_tx, mut mpsc_rx) = mpsc::channel::<AgentEvent>(100);
82
83 let forwarder = tokio::spawn(async move {
84 {
87 let runners = runners.read().await;
88 if let Some(runner) = runners.get(&session_id) {
89 let started_event = AgentEvent::ExecutionStarted {
90 run_id: runner.run_id.clone(),
91 session_id: session_id.clone(),
92 started_at: Utc::now().to_rfc3339(),
93 };
94 mirror_to_account_feed(&account_feed_inbox, &session_id, &started_event);
95 let _ = broadcast_tx.send(started_event);
96 }
97 }
98
99 while let Some(event) = mpsc_rx.recv().await {
100 {
101 let mut runners = runners.write().await;
102 if let Some(runner) = runners.get_mut(&session_id) {
103 runner.last_event_at = Some(Utc::now());
104
105 match &event {
106 AgentEvent::TokenBudgetUpdated { .. } => {
107 runner.last_budget_event = Some(event.clone());
108 }
109 AgentEvent::ToolStart { tool_name, .. } => {
110 runner.last_tool_name = Some(tool_name.clone());
111 runner.last_tool_phase = Some("begin".to_string());
112 }
113 AgentEvent::ToolLifecycle {
114 tool_name, phase, ..
115 } => {
116 runner.last_tool_name = Some(tool_name.clone());
117 runner.last_tool_phase = Some(phase.clone());
118 }
119 AgentEvent::RunnerProgress { round_count, .. } => {
120 runner.round_count = *round_count;
121 }
122 _ => {}
123 }
124 }
125 }
126 mirror_to_account_feed(&account_feed_inbox, &session_id, &event);
127 let _ = broadcast_tx.send(event);
128 }
129 });
130
131 (mpsc_tx, forwarder)
132}