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 last_budget_event: Option<AgentEvent>,
93
94 pub last_critical_events: Vec<AgentEvent>,
99
100 pub last_tool_name: Option<String>,
103
104 pub last_tool_phase: Option<String>,
107
108 pub last_event_at: Option<DateTime<Utc>>,
111
112 pub round_count: u32,
115
116 pub run_id: String,
121}
122
123impl Default for AgentRunner {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl AgentRunner {
130 pub fn root_reset_admission_reserved(&self) -> bool {
132 self.root_execution.reset_admission_reserved()
133 }
134
135 pub(crate) fn reserve_root_reset_admission(
138 &self,
139 session: &bamboo_domain::Session,
140 epoch: u64,
141 operation_id: Uuid,
142 ) -> std::io::Result<super::root_execution_lifetime::RootResetAdmission> {
143 self.root_slot
144 .reserve_reset_admission(session, epoch, operation_id)
145 }
146
147 pub fn last_activity_at(&self) -> Option<DateTime<Utc>> {
149 self.last_event_at
150 .max(self.event_publication.last_event_at())
151 }
152 pub const EVENT_CHANNEL_CAPACITY: usize = 1000;
154
155 pub const CRITICAL_EVENTS_CAPACITY: usize = 32;
157
158 pub fn new() -> Self {
163 let (event_sender, _) = broadcast::channel(Self::EVENT_CHANNEL_CAPACITY);
164 let root_execution = Arc::default();
165 Self {
166 root_slot: super::root_execution_lifetime::RootRunnerSlot::new(Arc::clone(
167 &root_execution,
168 )),
169 root_execution,
170 event_sender,
171 event_publication: Arc::default(),
172 visible_messages: Arc::default(),
173 cancel_token: CancellationToken::new(),
174 status: AgentStatus::Pending,
175 started_at: Utc::now(),
176 completed_at: None,
177 last_budget_event: None,
178 last_critical_events: Vec::new(),
179 last_tool_name: None,
180 last_tool_phase: None,
181 last_event_at: None,
182 round_count: 0,
183 run_id: Uuid::new_v4().to_string(),
184 }
185 }
186
187 pub(super) fn transfer_root_slot(&mut self, previous: Option<&mut Self>) {
190 if let Some(previous) = previous {
191 previous.root_slot.disarm();
192 }
193 self.root_slot.disarm();
194 self.root_slot =
195 super::root_execution_lifetime::RootRunnerSlot::new(self.root_execution.clone());
196 }
197
198 pub fn push_critical_event(&mut self, event: AgentEvent) {
208 let lifecycle_id = match &event {
209 AgentEvent::WorkflowActivated { event_id, .. }
210 | AgentEvent::WorkflowDeactivated { event_id, .. } => Some(event_id),
211 _ => None,
212 };
213 if lifecycle_id.is_some_and(|event_id| {
214 self.last_critical_events
215 .iter()
216 .any(|existing| match existing {
217 AgentEvent::WorkflowActivated {
218 event_id: existing_id,
219 ..
220 }
221 | AgentEvent::WorkflowDeactivated {
222 event_id: existing_id,
223 ..
224 } => existing_id == event_id,
225 _ => false,
226 })
227 }) {
228 return;
229 }
230 if let Some(key) = subagent_lifecycle_key(&event) {
231 self.last_critical_events
232 .retain(|existing| subagent_lifecycle_key(existing) != Some(key));
233 }
234 if self.last_critical_events.len() >= Self::CRITICAL_EVENTS_CAPACITY {
235 self.last_critical_events.remove(0);
236 }
237 self.last_critical_events.push(event);
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 fn started(child_session_id: &str, title: &str) -> AgentEvent {
246 AgentEvent::SubAgentStarted {
247 parent_session_id: "parent".to_string(),
248 child_session_id: child_session_id.to_string(),
249 title: Some(title.to_string()),
250 }
251 }
252
253 fn completed(child_session_id: &str, status: &str) -> AgentEvent {
254 AgentEvent::SubAgentCompleted {
255 parent_session_id: "parent".to_string(),
256 child_session_id: child_session_id.to_string(),
257 status: status.to_string(),
258 error: None,
259 }
260 }
261
262 fn wrapped(inner: AgentEvent) -> AgentEvent {
263 AgentEvent::SubAgentEvent {
264 parent_session_id: "root".into(),
265 child_session_id: "parent".into(),
266 event: Box::new(inner),
267 }
268 }
269
270 #[test]
271 fn critical_replay_keeps_only_latest_generation_state_per_child() {
272 let mut runner = AgentRunner::new();
273
274 for event in [
278 started("resident", "generation-1"),
279 completed("resident", "completed"),
280 started("resident", "generation-2"),
281 completed("resident", "completed"),
282 started("resident", "generation-3"),
283 ] {
284 runner.push_critical_event(event);
285 }
286
287 assert_eq!(runner.last_critical_events.len(), 1);
288 assert!(matches!(
289 &runner.last_critical_events[0],
290 AgentEvent::SubAgentStarted {
291 child_session_id,
292 title: Some(title),
293 ..
294 } if child_session_id == "resident" && title == "generation-3"
295 ));
296
297 runner.push_critical_event(completed("resident", "completed"));
298 assert_eq!(runner.last_critical_events.len(), 1);
299 assert!(matches!(
300 &runner.last_critical_events[0],
301 AgentEvent::SubAgentCompleted {
302 child_session_id,
303 status,
304 ..
305 } if child_session_id == "resident" && status == "completed"
306 ));
307 }
308
309 #[test]
310 fn subagent_coalescing_preserves_other_children_and_recency() {
311 let mut runner = AgentRunner::new();
312 runner.push_critical_event(started("child-a", "a1"));
313 runner.push_critical_event(started("child-b", "b1"));
314 runner.push_critical_event(completed("child-a", "completed"));
315
316 assert_eq!(runner.last_critical_events.len(), 2);
317 assert!(matches!(
318 &runner.last_critical_events[0],
319 AgentEvent::SubAgentStarted {
320 child_session_id, ..
321 } if child_session_id == "child-b"
322 ));
323 assert!(matches!(
324 &runner.last_critical_events[1],
325 AgentEvent::SubAgentCompleted {
326 child_session_id, ..
327 } if child_session_id == "child-a"
328 ));
329 }
330
331 #[test]
332 fn wrapped_legacy_lifecycle_replays_once_and_cannot_double_count_direct_lifecycle() {
333 let mut runner = AgentRunner::new();
334 let legacy_start = wrapped(started("child-a", "initial"));
335 assert!(legacy_start.is_replayable_session_state());
336 runner.push_critical_event(legacy_start);
337 runner.push_critical_event(started("child-a", "direct"));
338 assert_eq!(runner.last_critical_events.len(), 1);
339 assert!(matches!(
340 &runner.last_critical_events[0],
341 AgentEvent::SubAgentStarted { title: Some(title), .. } if title == "direct"
342 ));
343 runner.push_critical_event(wrapped(completed("child-a", "completed")));
344 assert_eq!(runner.last_critical_events.len(), 1);
345 assert!(matches!(
346 &runner.last_critical_events[0],
347 AgentEvent::SubAgentEvent { .. }
348 ));
349 assert!(runner.last_critical_events.len() <= AgentRunner::CRITICAL_EVENTS_CAPACITY);
350
351 let invalid = AgentEvent::SubAgentEvent {
352 parent_session_id: "root".into(),
353 child_session_id: "unrelated".into(),
354 event: Box::new(started("child-b", "forged")),
355 };
356 assert!(!invalid.is_replayable_session_state());
357 assert!(invalid.legacy_child_lifecycle_observation().is_none());
358 }
359}