bamboo_engine/runtime/execution/
runner_state.rs1use chrono::{DateTime, Utc};
8use std::sync::Arc;
9use tokio::sync::broadcast;
10use tokio_util::sync::CancellationToken;
11use uuid::Uuid;
12
13use bamboo_agent_core::AgentEvent;
14
15fn subagent_lifecycle_key(event: &AgentEvent) -> Option<(&str, &str)> {
16 event
17 .legacy_child_lifecycle_observation()
18 .map(|observation| (observation.parent_session_id, observation.child_session_id))
19}
20
21#[derive(Debug, Clone)]
26pub enum AgentStatus {
27 Pending,
29
30 Running,
32
33 Completed,
35
36 Cancelled,
38
39 Error(String),
41}
42
43#[derive(Debug, Clone)]
58pub struct AgentRunner {
59 pub(crate) root_execution: Arc<super::root_execution_lifetime::RootExecutionLifetime>,
61 root_slot: super::root_execution_lifetime::RootRunnerSlot,
62 pub event_publication: Arc<super::event_publication::EventPublication>,
64
65 pub visible_messages: Arc<super::visible_messages::VisibleMessageStream>,
67 pub event_sender: broadcast::Sender<AgentEvent>,
72
73 pub cancel_token: CancellationToken,
78
79 pub status: AgentStatus,
81
82 pub started_at: DateTime<Utc>,
84
85 pub completed_at: Option<DateTime<Utc>>,
87
88 pub(super) execution_finalized: CancellationToken,
92 supervisor_run_target: Option<bamboo_domain::SupervisorCurrentUserTarget>,
95 supervisor_run_workspace: Option<Option<String>>,
96
97 pub last_budget_event: Option<AgentEvent>,
102
103 pub last_critical_events: Vec<AgentEvent>,
108
109 pub last_tool_name: Option<String>,
112
113 pub last_tool_phase: Option<String>,
116
117 pub last_event_at: Option<DateTime<Utc>>,
120
121 pub round_count: u32,
124
125 pub run_id: String,
130}
131
132impl Default for AgentRunner {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138impl AgentRunner {
139 pub fn root_reset_admission_reserved(&self) -> bool {
141 self.root_execution.reset_admission_reserved()
142 }
143
144 pub(crate) fn reserve_root_reset_admission(
147 &self,
148 session: &bamboo_domain::Session,
149 epoch: u64,
150 operation_id: Uuid,
151 ) -> std::io::Result<super::root_execution_lifetime::RootResetAdmission> {
152 self.root_slot
153 .reserve_reset_admission(session, epoch, operation_id)
154 }
155
156 pub fn last_activity_at(&self) -> Option<DateTime<Utc>> {
158 self.last_event_at
159 .max(self.event_publication.last_event_at())
160 }
161
162 pub fn bind_supervisor_run_target(
164 &mut self,
165 target: &bamboo_domain::SupervisorCurrentUserTarget,
166 workspace_path: Option<&str>,
167 ) {
168 self.supervisor_run_target = Some(target.clone());
169 self.supervisor_run_workspace = Some(workspace_path.map(str::to_owned));
170 }
171
172 pub fn exact_execution_finalized(&self) -> bool {
174 self.execution_finalized.is_cancelled()
175 }
176
177 pub fn wait_for_exact_finalization(
179 &self,
180 ) -> impl std::future::Future<Output = ()> + Send + 'static {
181 self.execution_finalized.clone().cancelled_owned()
182 }
183
184 pub fn supervisor_run_observation_matches(
188 &self,
189 target: &bamboo_domain::SupervisorCurrentUserTarget,
190 workspace_path: Option<&str>,
191 ) -> Option<bool> {
192 match (&self.supervisor_run_target, &self.supervisor_run_workspace) {
193 (None, None) => None,
194 (Some(observed), Some(workspace)) => {
195 let mut observed = observed.clone();
196 observed.metadata_version = target.metadata_version;
197 Some(&observed == target && workspace.as_deref() == workspace_path)
198 }
199 _ => Some(false),
200 }
201 }
202
203 pub fn supervisor_run_is_finalized_for(
205 &self,
206 target: &bamboo_domain::SupervisorCurrentUserTarget,
207 workspace_path: Option<&str>,
208 ) -> bool {
209 self.execution_finalized.is_cancelled()
210 && self.supervisor_run_observation_matches(target, workspace_path) == Some(true)
211 }
212 pub const EVENT_CHANNEL_CAPACITY: usize = 1000;
214
215 pub const CRITICAL_EVENTS_CAPACITY: usize = 32;
217
218 pub fn new() -> Self {
223 let (event_sender, _) = broadcast::channel(Self::EVENT_CHANNEL_CAPACITY);
224 let root_execution = Arc::default();
225 Self {
226 root_slot: super::root_execution_lifetime::RootRunnerSlot::new(Arc::clone(
227 &root_execution,
228 )),
229 root_execution,
230 event_sender,
231 event_publication: Arc::default(),
232 visible_messages: Arc::default(),
233 cancel_token: CancellationToken::new(),
234 status: AgentStatus::Pending,
235 started_at: Utc::now(),
236 completed_at: None,
237 execution_finalized: CancellationToken::new(),
238 supervisor_run_target: None,
239 supervisor_run_workspace: None,
240 last_budget_event: None,
241 last_critical_events: Vec::new(),
242 last_tool_name: None,
243 last_tool_phase: None,
244 last_event_at: None,
245 round_count: 0,
246 run_id: Uuid::new_v4().to_string(),
247 }
248 }
249
250 pub(super) fn transfer_root_slot(&mut self, previous: Option<&mut Self>) {
253 if let Some(previous) = previous {
254 previous.root_slot.disarm();
255 }
256 self.root_slot.disarm();
257 self.root_slot =
258 super::root_execution_lifetime::RootRunnerSlot::new(self.root_execution.clone());
259 }
260
261 pub fn push_critical_event(&mut self, event: AgentEvent) {
271 let lifecycle_id = match &event {
272 AgentEvent::WorkflowActivated { event_id, .. }
273 | AgentEvent::WorkflowDeactivated { event_id, .. } => Some(event_id),
274 _ => None,
275 };
276 if lifecycle_id.is_some_and(|event_id| {
277 self.last_critical_events
278 .iter()
279 .any(|existing| match existing {
280 AgentEvent::WorkflowActivated {
281 event_id: existing_id,
282 ..
283 }
284 | AgentEvent::WorkflowDeactivated {
285 event_id: existing_id,
286 ..
287 } => existing_id == event_id,
288 _ => false,
289 })
290 }) {
291 return;
292 }
293 if let Some(key) = subagent_lifecycle_key(&event) {
294 self.last_critical_events
295 .retain(|existing| subagent_lifecycle_key(existing) != Some(key));
296 }
297 if self.last_critical_events.len() >= Self::CRITICAL_EVENTS_CAPACITY {
298 self.last_critical_events.remove(0);
299 }
300 self.last_critical_events.push(event);
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn started(child_session_id: &str, title: &str) -> AgentEvent {
309 AgentEvent::SubAgentStarted {
310 parent_session_id: "parent".to_string(),
311 child_session_id: child_session_id.to_string(),
312 title: Some(title.to_string()),
313 }
314 }
315
316 fn completed(child_session_id: &str, status: &str) -> AgentEvent {
317 AgentEvent::SubAgentCompleted {
318 parent_session_id: "parent".to_string(),
319 child_session_id: child_session_id.to_string(),
320 status: status.to_string(),
321 error: None,
322 }
323 }
324
325 fn wrapped(inner: AgentEvent) -> AgentEvent {
326 AgentEvent::SubAgentEvent {
327 parent_session_id: "root".into(),
328 child_session_id: "parent".into(),
329 event: Box::new(inner),
330 }
331 }
332
333 #[test]
334 fn critical_replay_keeps_only_latest_generation_state_per_child() {
335 let mut runner = AgentRunner::new();
336
337 for event in [
341 started("resident", "generation-1"),
342 completed("resident", "completed"),
343 started("resident", "generation-2"),
344 completed("resident", "completed"),
345 started("resident", "generation-3"),
346 ] {
347 runner.push_critical_event(event);
348 }
349
350 assert_eq!(runner.last_critical_events.len(), 1);
351 assert!(matches!(
352 &runner.last_critical_events[0],
353 AgentEvent::SubAgentStarted {
354 child_session_id,
355 title: Some(title),
356 ..
357 } if child_session_id == "resident" && title == "generation-3"
358 ));
359
360 runner.push_critical_event(completed("resident", "completed"));
361 assert_eq!(runner.last_critical_events.len(), 1);
362 assert!(matches!(
363 &runner.last_critical_events[0],
364 AgentEvent::SubAgentCompleted {
365 child_session_id,
366 status,
367 ..
368 } if child_session_id == "resident" && status == "completed"
369 ));
370 }
371
372 #[test]
373 fn subagent_coalescing_preserves_other_children_and_recency() {
374 let mut runner = AgentRunner::new();
375 runner.push_critical_event(started("child-a", "a1"));
376 runner.push_critical_event(started("child-b", "b1"));
377 runner.push_critical_event(completed("child-a", "completed"));
378
379 assert_eq!(runner.last_critical_events.len(), 2);
380 assert!(matches!(
381 &runner.last_critical_events[0],
382 AgentEvent::SubAgentStarted {
383 child_session_id, ..
384 } if child_session_id == "child-b"
385 ));
386 assert!(matches!(
387 &runner.last_critical_events[1],
388 AgentEvent::SubAgentCompleted {
389 child_session_id, ..
390 } if child_session_id == "child-a"
391 ));
392 }
393
394 #[test]
395 fn wrapped_legacy_lifecycle_replays_once_and_cannot_double_count_direct_lifecycle() {
396 let mut runner = AgentRunner::new();
397 let legacy_start = wrapped(started("child-a", "initial"));
398 assert!(legacy_start.is_replayable_session_state());
399 runner.push_critical_event(legacy_start);
400 runner.push_critical_event(started("child-a", "direct"));
401 assert_eq!(runner.last_critical_events.len(), 1);
402 assert!(matches!(
403 &runner.last_critical_events[0],
404 AgentEvent::SubAgentStarted { title: Some(title), .. } if title == "direct"
405 ));
406 runner.push_critical_event(wrapped(completed("child-a", "completed")));
407 assert_eq!(runner.last_critical_events.len(), 1);
408 assert!(matches!(
409 &runner.last_critical_events[0],
410 AgentEvent::SubAgentEvent { .. }
411 ));
412 assert!(runner.last_critical_events.len() <= AgentRunner::CRITICAL_EVENTS_CAPACITY);
413
414 let invalid = AgentEvent::SubAgentEvent {
415 parent_session_id: "root".into(),
416 child_session_id: "unrelated".into(),
417 event: Box::new(started("child-b", "forged")),
418 };
419 assert!(!invalid.is_replayable_session_state());
420 assert!(invalid.legacy_child_lifecycle_observation().is_none());
421 }
422}