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: bool,
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
175 }
176
177 pub fn supervisor_run_observation_matches(
181 &self,
182 target: &bamboo_domain::SupervisorCurrentUserTarget,
183 workspace_path: Option<&str>,
184 ) -> Option<bool> {
185 match (&self.supervisor_run_target, &self.supervisor_run_workspace) {
186 (None, None) => None,
187 (Some(observed), Some(workspace)) => {
188 let mut observed = observed.clone();
189 observed.metadata_version = target.metadata_version;
190 Some(&observed == target && workspace.as_deref() == workspace_path)
191 }
192 _ => Some(false),
193 }
194 }
195
196 pub fn supervisor_run_is_finalized_for(
198 &self,
199 target: &bamboo_domain::SupervisorCurrentUserTarget,
200 workspace_path: Option<&str>,
201 ) -> bool {
202 self.execution_finalized
203 && self.supervisor_run_observation_matches(target, workspace_path) == Some(true)
204 }
205 pub const EVENT_CHANNEL_CAPACITY: usize = 1000;
207
208 pub const CRITICAL_EVENTS_CAPACITY: usize = 32;
210
211 pub fn new() -> Self {
216 let (event_sender, _) = broadcast::channel(Self::EVENT_CHANNEL_CAPACITY);
217 let root_execution = Arc::default();
218 Self {
219 root_slot: super::root_execution_lifetime::RootRunnerSlot::new(Arc::clone(
220 &root_execution,
221 )),
222 root_execution,
223 event_sender,
224 event_publication: Arc::default(),
225 visible_messages: Arc::default(),
226 cancel_token: CancellationToken::new(),
227 status: AgentStatus::Pending,
228 started_at: Utc::now(),
229 completed_at: None,
230 execution_finalized: false,
231 supervisor_run_target: None,
232 supervisor_run_workspace: None,
233 last_budget_event: None,
234 last_critical_events: Vec::new(),
235 last_tool_name: None,
236 last_tool_phase: None,
237 last_event_at: None,
238 round_count: 0,
239 run_id: Uuid::new_v4().to_string(),
240 }
241 }
242
243 pub(super) fn transfer_root_slot(&mut self, previous: Option<&mut Self>) {
246 if let Some(previous) = previous {
247 previous.root_slot.disarm();
248 }
249 self.root_slot.disarm();
250 self.root_slot =
251 super::root_execution_lifetime::RootRunnerSlot::new(self.root_execution.clone());
252 }
253
254 pub fn push_critical_event(&mut self, event: AgentEvent) {
264 let lifecycle_id = match &event {
265 AgentEvent::WorkflowActivated { event_id, .. }
266 | AgentEvent::WorkflowDeactivated { event_id, .. } => Some(event_id),
267 _ => None,
268 };
269 if lifecycle_id.is_some_and(|event_id| {
270 self.last_critical_events
271 .iter()
272 .any(|existing| match existing {
273 AgentEvent::WorkflowActivated {
274 event_id: existing_id,
275 ..
276 }
277 | AgentEvent::WorkflowDeactivated {
278 event_id: existing_id,
279 ..
280 } => existing_id == event_id,
281 _ => false,
282 })
283 }) {
284 return;
285 }
286 if let Some(key) = subagent_lifecycle_key(&event) {
287 self.last_critical_events
288 .retain(|existing| subagent_lifecycle_key(existing) != Some(key));
289 }
290 if self.last_critical_events.len() >= Self::CRITICAL_EVENTS_CAPACITY {
291 self.last_critical_events.remove(0);
292 }
293 self.last_critical_events.push(event);
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn started(child_session_id: &str, title: &str) -> AgentEvent {
302 AgentEvent::SubAgentStarted {
303 parent_session_id: "parent".to_string(),
304 child_session_id: child_session_id.to_string(),
305 title: Some(title.to_string()),
306 }
307 }
308
309 fn completed(child_session_id: &str, status: &str) -> AgentEvent {
310 AgentEvent::SubAgentCompleted {
311 parent_session_id: "parent".to_string(),
312 child_session_id: child_session_id.to_string(),
313 status: status.to_string(),
314 error: None,
315 }
316 }
317
318 fn wrapped(inner: AgentEvent) -> AgentEvent {
319 AgentEvent::SubAgentEvent {
320 parent_session_id: "root".into(),
321 child_session_id: "parent".into(),
322 event: Box::new(inner),
323 }
324 }
325
326 #[test]
327 fn critical_replay_keeps_only_latest_generation_state_per_child() {
328 let mut runner = AgentRunner::new();
329
330 for event in [
334 started("resident", "generation-1"),
335 completed("resident", "completed"),
336 started("resident", "generation-2"),
337 completed("resident", "completed"),
338 started("resident", "generation-3"),
339 ] {
340 runner.push_critical_event(event);
341 }
342
343 assert_eq!(runner.last_critical_events.len(), 1);
344 assert!(matches!(
345 &runner.last_critical_events[0],
346 AgentEvent::SubAgentStarted {
347 child_session_id,
348 title: Some(title),
349 ..
350 } if child_session_id == "resident" && title == "generation-3"
351 ));
352
353 runner.push_critical_event(completed("resident", "completed"));
354 assert_eq!(runner.last_critical_events.len(), 1);
355 assert!(matches!(
356 &runner.last_critical_events[0],
357 AgentEvent::SubAgentCompleted {
358 child_session_id,
359 status,
360 ..
361 } if child_session_id == "resident" && status == "completed"
362 ));
363 }
364
365 #[test]
366 fn subagent_coalescing_preserves_other_children_and_recency() {
367 let mut runner = AgentRunner::new();
368 runner.push_critical_event(started("child-a", "a1"));
369 runner.push_critical_event(started("child-b", "b1"));
370 runner.push_critical_event(completed("child-a", "completed"));
371
372 assert_eq!(runner.last_critical_events.len(), 2);
373 assert!(matches!(
374 &runner.last_critical_events[0],
375 AgentEvent::SubAgentStarted {
376 child_session_id, ..
377 } if child_session_id == "child-b"
378 ));
379 assert!(matches!(
380 &runner.last_critical_events[1],
381 AgentEvent::SubAgentCompleted {
382 child_session_id, ..
383 } if child_session_id == "child-a"
384 ));
385 }
386
387 #[test]
388 fn wrapped_legacy_lifecycle_replays_once_and_cannot_double_count_direct_lifecycle() {
389 let mut runner = AgentRunner::new();
390 let legacy_start = wrapped(started("child-a", "initial"));
391 assert!(legacy_start.is_replayable_session_state());
392 runner.push_critical_event(legacy_start);
393 runner.push_critical_event(started("child-a", "direct"));
394 assert_eq!(runner.last_critical_events.len(), 1);
395 assert!(matches!(
396 &runner.last_critical_events[0],
397 AgentEvent::SubAgentStarted { title: Some(title), .. } if title == "direct"
398 ));
399 runner.push_critical_event(wrapped(completed("child-a", "completed")));
400 assert_eq!(runner.last_critical_events.len(), 1);
401 assert!(matches!(
402 &runner.last_critical_events[0],
403 AgentEvent::SubAgentEvent { .. }
404 ));
405 assert!(runner.last_critical_events.len() <= AgentRunner::CRITICAL_EVENTS_CAPACITY);
406
407 let invalid = AgentEvent::SubAgentEvent {
408 parent_session_id: "root".into(),
409 child_session_id: "unrelated".into(),
410 event: Box::new(started("child-b", "forged")),
411 };
412 assert!(!invalid.is_replayable_session_state());
413 assert!(invalid.legacy_child_lifecycle_observation().is_none());
414 }
415}