bamboo_engine/runtime/execution/
runner_lifecycle.rs1use std::collections::HashMap;
9use std::sync::Arc;
10
11use chrono::Utc;
12use tokio::sync::{broadcast, RwLock};
13use tokio_util::sync::CancellationToken;
14
15use bamboo_agent_core::{AgentError, AgentEvent};
16
17use super::runner_state::{AgentRunner, AgentStatus};
18
19#[derive(Debug, Clone)]
21pub struct RunnerReservation {
22 pub cancel_token: CancellationToken,
23 pub run_id: String,
24}
25
26#[derive(Debug, Clone)]
28pub enum ReserveOutcome {
29 Reserved(RunnerReservation),
31 AlreadyRunning(String),
34}
35
36pub async fn reserve_runner_core(
59 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
60 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
61 session_id: &str,
62 event_sender: &broadcast::Sender<AgentEvent>,
63) -> ReserveOutcome {
64 let mut guard = runners.write().await;
65 if let Some(runner) = guard.get(session_id) {
66 if matches!(runner.status, AgentStatus::Running) {
67 return ReserveOutcome::AlreadyRunning(runner.run_id.clone());
68 }
69 }
70
71 guard.remove(session_id);
72
73 let mut runner = AgentRunner::new();
74 runner.status = AgentStatus::Running;
75 runner.event_sender = event_sender.clone();
76 let reservation = RunnerReservation {
77 cancel_token: runner.cancel_token.clone(),
78 run_id: runner.run_id.clone(),
79 };
80 guard.insert(session_id.to_string(), runner);
81
82 senders
87 .write()
88 .await
89 .entry(session_id.to_string())
90 .or_insert_with(|| event_sender.clone());
91
92 ReserveOutcome::Reserved(reservation)
93}
94
95pub async fn try_reserve_runner(
108 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
109 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
110 session_id: &str,
111 event_sender: &broadcast::Sender<AgentEvent>,
112) -> Option<RunnerReservation> {
113 match reserve_runner_core(runners, senders, session_id, event_sender).await {
114 ReserveOutcome::Reserved(reservation) => Some(reservation),
115 ReserveOutcome::AlreadyRunning(_) => {
116 tracing::debug!("[{}] Runner already running, skipping", session_id);
117 None
118 }
119 }
120}
121
122pub fn status_from_execution_result(result: &Result<(), AgentError>) -> AgentStatus {
124 match result {
125 Ok(_) => AgentStatus::Completed,
126 Err(error) if error.is_cancelled() => AgentStatus::Cancelled,
127 Err(error) => AgentStatus::Error(error.to_string()),
128 }
129}
130
131pub async fn finalize_runner(
133 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
134 session_id: &str,
135 result: &Result<(), AgentError>,
136) {
137 let mut guard = runners.write().await;
138 if let Some(runner) = guard.get_mut(session_id) {
139 runner.status = status_from_execution_result(result);
140 runner.completed_at = Some(Utc::now());
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn new_runners() -> Arc<RwLock<HashMap<String, AgentRunner>>> {
149 Arc::new(RwLock::new(HashMap::new()))
150 }
151
152 fn new_senders() -> Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> {
153 Arc::new(RwLock::new(HashMap::new()))
154 }
155
156 fn new_broadcaster() -> broadcast::Sender<AgentEvent> {
157 broadcast::channel(100).0
158 }
159
160 #[tokio::test]
161 async fn try_reserve_runner_creates_runner_with_running_status() {
162 let runners = new_runners();
163 let senders = new_senders();
164 let tx = new_broadcaster();
165 let token = try_reserve_runner(&runners, &senders, "s1", &tx).await;
166 assert!(token.is_some());
167
168 let guard = runners.read().await;
169 let runner = guard.get("s1").unwrap();
170 assert!(matches!(runner.status, AgentStatus::Running));
171 }
172
173 #[tokio::test]
174 async fn try_reserve_runner_returns_none_when_already_running() {
175 let runners = new_runners();
176 let senders = new_senders();
177 let tx = new_broadcaster();
178 let _ = try_reserve_runner(&runners, &senders, "s1", &tx).await;
179 let second = try_reserve_runner(&runners, &senders, "s1", &tx).await;
180 assert!(second.is_none());
181 }
182
183 #[tokio::test]
184 async fn try_reserve_runner_replaces_completed_runner() {
185 let runners = new_runners();
186 let senders = new_senders();
187 let tx = new_broadcaster();
188 let _ = try_reserve_runner(&runners, &senders, "s1", &tx).await;
189
190 {
191 let mut guard = runners.write().await;
192 let runner = guard.get_mut("s1").unwrap();
193 runner.status = AgentStatus::Completed;
194 }
195
196 let second = try_reserve_runner(&runners, &senders, "s1", &tx).await;
197 assert!(second.is_some());
198 }
199
200 #[tokio::test]
201 async fn try_reserve_runner_reasserts_evicted_sender_so_late_subscriber_receives() {
202 use super::super::session_events::get_or_create_event_sender;
208 use bamboo_agent_core::AgentEvent;
209
210 let runners = new_runners();
211 let senders = new_senders();
212
213 let session_tx = get_or_create_event_sender(&senders, "s1").await;
215 senders.write().await.remove("s1");
217 assert!(senders.read().await.get("s1").is_none());
218
219 let reservation = try_reserve_runner(&runners, &senders, "s1", &session_tx).await;
221 assert!(reservation.is_some(), "reservation must succeed");
222
223 assert!(
226 senders.read().await.get("s1").is_some(),
227 "reservation must re-assert the evicted session sender into the map"
228 );
229
230 let subscriber_tx = get_or_create_event_sender(&senders, "s1").await;
233 let mut rx = subscriber_tx.subscribe();
234
235 let _ = session_tx.send(AgentEvent::SessionDeleted {
237 session_id: "s1".to_string(),
238 });
239
240 let received = tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv()).await;
241 assert!(
242 matches!(received, Ok(Ok(_))),
243 "late subscriber must receive events from the resumed run; without the \
244 re-assert, get_or_create mints a fresh channel and the event is lost"
245 );
246 }
247
248 #[test]
249 fn status_from_execution_result_maps_correctly() {
250 let ok_result: Result<(), AgentError> = Ok(());
251 assert!(matches!(
252 status_from_execution_result(&ok_result),
253 AgentStatus::Completed
254 ));
255
256 let cancelled: Result<(), AgentError> = Err(AgentError::Cancelled);
261 assert!(matches!(
262 status_from_execution_result(&cancelled),
263 AgentStatus::Cancelled
264 ));
265
266 let failed: Result<(), AgentError> = Err(AgentError::LLM("network error".to_string()));
267 match status_from_execution_result(&failed) {
268 AgentStatus::Error(message) => assert!(message.contains("network error")),
269 other => panic!("unexpected status: {other:?}"),
270 }
271 }
272}