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 event_publication: Arc<super::event_publication::EventPublication>,
61
62 pub visible_messages: Arc<super::visible_messages::VisibleMessageStream>,
64 pub event_sender: broadcast::Sender<AgentEvent>,
69
70 pub cancel_token: CancellationToken,
75
76 pub status: AgentStatus,
78
79 pub started_at: DateTime<Utc>,
81
82 pub completed_at: Option<DateTime<Utc>>,
84
85 pub last_budget_event: Option<AgentEvent>,
90
91 pub last_critical_events: Vec<AgentEvent>,
96
97 pub last_tool_name: Option<String>,
100
101 pub last_tool_phase: Option<String>,
104
105 pub last_event_at: Option<DateTime<Utc>>,
108
109 pub round_count: u32,
112
113 pub run_id: String,
118}
119
120impl Default for AgentRunner {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl AgentRunner {
127 pub fn last_activity_at(&self) -> Option<DateTime<Utc>> {
129 self.last_event_at
130 .max(self.event_publication.last_event_at())
131 }
132 pub const EVENT_CHANNEL_CAPACITY: usize = 1000;
134
135 pub const CRITICAL_EVENTS_CAPACITY: usize = 32;
137
138 pub fn new() -> Self {
143 let (event_sender, _) = broadcast::channel(Self::EVENT_CHANNEL_CAPACITY);
144 Self {
145 event_sender,
146 event_publication: Arc::default(),
147 visible_messages: Arc::default(),
148 cancel_token: CancellationToken::new(),
149 status: AgentStatus::Pending,
150 started_at: Utc::now(),
151 completed_at: None,
152 last_budget_event: None,
153 last_critical_events: Vec::new(),
154 last_tool_name: None,
155 last_tool_phase: None,
156 last_event_at: None,
157 round_count: 0,
158 run_id: Uuid::new_v4().to_string(),
159 }
160 }
161
162 pub fn push_critical_event(&mut self, event: AgentEvent) {
172 let lifecycle_id = match &event {
173 AgentEvent::WorkflowActivated { event_id, .. }
174 | AgentEvent::WorkflowDeactivated { event_id, .. } => Some(event_id),
175 _ => None,
176 };
177 if lifecycle_id.is_some_and(|event_id| {
178 self.last_critical_events
179 .iter()
180 .any(|existing| match existing {
181 AgentEvent::WorkflowActivated {
182 event_id: existing_id,
183 ..
184 }
185 | AgentEvent::WorkflowDeactivated {
186 event_id: existing_id,
187 ..
188 } => existing_id == event_id,
189 _ => false,
190 })
191 }) {
192 return;
193 }
194 if let Some(key) = subagent_lifecycle_key(&event) {
195 self.last_critical_events
196 .retain(|existing| subagent_lifecycle_key(existing) != Some(key));
197 }
198 if self.last_critical_events.len() >= Self::CRITICAL_EVENTS_CAPACITY {
199 self.last_critical_events.remove(0);
200 }
201 self.last_critical_events.push(event);
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn started(child_session_id: &str, title: &str) -> AgentEvent {
210 AgentEvent::SubAgentStarted {
211 parent_session_id: "parent".to_string(),
212 child_session_id: child_session_id.to_string(),
213 title: Some(title.to_string()),
214 }
215 }
216
217 fn completed(child_session_id: &str, status: &str) -> AgentEvent {
218 AgentEvent::SubAgentCompleted {
219 parent_session_id: "parent".to_string(),
220 child_session_id: child_session_id.to_string(),
221 status: status.to_string(),
222 error: None,
223 }
224 }
225
226 fn wrapped(inner: AgentEvent) -> AgentEvent {
227 AgentEvent::SubAgentEvent {
228 parent_session_id: "root".into(),
229 child_session_id: "parent".into(),
230 event: Box::new(inner),
231 }
232 }
233
234 #[test]
235 fn critical_replay_keeps_only_latest_generation_state_per_child() {
236 let mut runner = AgentRunner::new();
237
238 for event in [
242 started("resident", "generation-1"),
243 completed("resident", "completed"),
244 started("resident", "generation-2"),
245 completed("resident", "completed"),
246 started("resident", "generation-3"),
247 ] {
248 runner.push_critical_event(event);
249 }
250
251 assert_eq!(runner.last_critical_events.len(), 1);
252 assert!(matches!(
253 &runner.last_critical_events[0],
254 AgentEvent::SubAgentStarted {
255 child_session_id,
256 title: Some(title),
257 ..
258 } if child_session_id == "resident" && title == "generation-3"
259 ));
260
261 runner.push_critical_event(completed("resident", "completed"));
262 assert_eq!(runner.last_critical_events.len(), 1);
263 assert!(matches!(
264 &runner.last_critical_events[0],
265 AgentEvent::SubAgentCompleted {
266 child_session_id,
267 status,
268 ..
269 } if child_session_id == "resident" && status == "completed"
270 ));
271 }
272
273 #[test]
274 fn subagent_coalescing_preserves_other_children_and_recency() {
275 let mut runner = AgentRunner::new();
276 runner.push_critical_event(started("child-a", "a1"));
277 runner.push_critical_event(started("child-b", "b1"));
278 runner.push_critical_event(completed("child-a", "completed"));
279
280 assert_eq!(runner.last_critical_events.len(), 2);
281 assert!(matches!(
282 &runner.last_critical_events[0],
283 AgentEvent::SubAgentStarted {
284 child_session_id, ..
285 } if child_session_id == "child-b"
286 ));
287 assert!(matches!(
288 &runner.last_critical_events[1],
289 AgentEvent::SubAgentCompleted {
290 child_session_id, ..
291 } if child_session_id == "child-a"
292 ));
293 }
294
295 #[test]
296 fn wrapped_legacy_lifecycle_replays_once_and_cannot_double_count_direct_lifecycle() {
297 let mut runner = AgentRunner::new();
298 let legacy_start = wrapped(started("child-a", "initial"));
299 assert!(legacy_start.is_replayable_session_state());
300 runner.push_critical_event(legacy_start);
301 runner.push_critical_event(started("child-a", "direct"));
302 assert_eq!(runner.last_critical_events.len(), 1);
303 assert!(matches!(
304 &runner.last_critical_events[0],
305 AgentEvent::SubAgentStarted { title: Some(title), .. } if title == "direct"
306 ));
307 runner.push_critical_event(wrapped(completed("child-a", "completed")));
308 assert_eq!(runner.last_critical_events.len(), 1);
309 assert!(matches!(
310 &runner.last_critical_events[0],
311 AgentEvent::SubAgentEvent { .. }
312 ));
313 assert!(runner.last_critical_events.len() <= AgentRunner::CRITICAL_EVENTS_CAPACITY);
314
315 let invalid = AgentEvent::SubAgentEvent {
316 parent_session_id: "root".into(),
317 child_session_id: "unrelated".into(),
318 event: Box::new(started("child-b", "forged")),
319 };
320 assert!(!invalid.is_replayable_session_state());
321 assert!(invalid.legacy_child_lifecycle_observation().is_none());
322 }
323}