1use std::collections::HashMap;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use chrono::Utc;
11use tokio::sync::{broadcast, RwLock};
12use tokio::time::{sleep, Duration, Instant};
13
14use crate::app_state::session_events::get_or_create_event_sender;
15use crate::app_state::{AgentRunner, AgentStatus};
16use bamboo_agent_core::storage::Storage;
17use bamboo_agent_core::{AgentEvent, Session, SessionKind};
18use bamboo_domain::session::runtime_state::{
19 AgentRuntimeState, ChildWaitPolicy, WaitingForChildrenState,
20};
21use bamboo_engine::execution::spawn::{SpawnJob, SpawnScheduler};
22use bamboo_engine::session_app::child_session::{
23 ChildRunnerInfo, ChildSessionEntry, ChildSessionError, ChildSessionPort, DeleteChildResult,
24 SubagentResolutionPort,
25};
26use bamboo_llm::Config;
27use bamboo_storage::{LockedSessionStore, SessionIndexEntry, SessionStoreV2};
28
29pub struct ChildSessionAdapter {
34 pub(crate) session_store: Arc<SessionStoreV2>,
35 pub(crate) storage: Arc<dyn Storage>,
36 pub(crate) persistence: Arc<LockedSessionStore>,
37 pub(crate) scheduler: Arc<SpawnScheduler>,
38 pub(crate) sessions_cache: bamboo_engine::SessionCache,
39 pub(crate) agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
40 pub(crate) session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
41 pub(crate) subagent_model_resolver: crate::tools::OptionalSubagentModelResolver,
43 pub(crate) config: Arc<RwLock<Config>>,
45 pub(crate) parent_wait_slots: Arc<dashmap::DashMap<String, Arc<ParentWaitSlot>>>,
50}
51
52#[derive(Default)]
61pub(crate) struct ParentWaitSlot {
62 flush_lock: tokio::sync::Mutex<()>,
63 pending: parking_lot::Mutex<Vec<(String, Option<String>)>>,
64}
65
66const AGENT_RUNTIME_STATE_METADATA_KEY: &str = "agent.runtime.state";
67
68fn is_terminal_child_status(status: &str) -> bool {
71 matches!(
72 status,
73 "completed" | "error" | "timeout" | "cancelled" | "skipped"
74 )
75}
76
77fn read_runtime_state(session: &Session) -> AgentRuntimeState {
78 session
79 .agent_runtime_state
80 .clone()
81 .or_else(|| {
82 session
83 .metadata
84 .get(AGENT_RUNTIME_STATE_METADATA_KEY)
85 .and_then(|raw| serde_json::from_str::<AgentRuntimeState>(raw).ok())
86 })
87 .unwrap_or_else(|| AgentRuntimeState::new(format!("{}-wait", session.id)))
88}
89
90fn write_runtime_state(session: &mut Session, runtime_state: &AgentRuntimeState) {
91 session.agent_runtime_state = Some(runtime_state.clone());
92 if let Ok(serialized) = serde_json::to_string(runtime_state) {
93 session
94 .metadata
95 .insert(AGENT_RUNTIME_STATE_METADATA_KEY.to_string(), serialized);
96 }
97}
98
99impl ChildSessionAdapter {
100 #[allow(clippy::too_many_arguments)]
105 pub fn new(
106 session_store: Arc<SessionStoreV2>,
107 storage: Arc<dyn Storage>,
108 persistence: Arc<LockedSessionStore>,
109 scheduler: Arc<SpawnScheduler>,
110 sessions_cache: bamboo_engine::SessionCache,
111 agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
112 session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
113 subagent_model_resolver: crate::tools::OptionalSubagentModelResolver,
114 config: Arc<RwLock<Config>>,
115 ) -> Self {
116 Self {
117 session_store,
118 storage,
119 persistence,
120 scheduler,
121 sessions_cache,
122 agent_runners,
123 session_event_senders,
124 subagent_model_resolver,
125 config,
126 parent_wait_slots: Arc::new(dashmap::DashMap::new()),
129 }
130 }
131
132 pub async fn resolve_subagent_model(
134 &self,
135 subagent_type: &str,
136 ) -> Option<bamboo_domain::ProviderModelRef> {
137 match &self.subagent_model_resolver {
138 Some(resolver) => resolver(subagent_type.to_string()).await,
139 None => None,
140 }
141 }
142
143 pub async fn resolve_runtime_metadata(&self, subagent_type: &str) -> HashMap<String, String> {
145 let config = self.config.read().await;
146 bamboo_engine::external_agents::config::resolve_runtime_metadata(&config, subagent_type)
147 }
148
149 pub async fn register_parent_wait_for_child(
163 &self,
164 parent_session_id: &str,
165 child_session_id: &str,
166 tool_call_id: Option<&str>,
167 ) -> Result<(), ChildSessionError> {
168 let slot = self
169 .parent_wait_slots
170 .entry(parent_session_id.to_string())
171 .or_default()
172 .clone();
173
174 slot.pending.lock().push((
176 child_session_id.to_string(),
177 tool_call_id.map(str::to_string),
178 ));
179
180 let _flush_guard = slot.flush_lock.lock().await;
182
183 let batch: Vec<(String, Option<String>)> = {
186 let mut pending = slot.pending.lock();
187 pending.drain(..).collect()
188 };
189 if batch.is_empty() {
190 return Ok(());
193 }
194
195 if let Err(error) = self
197 .flush_parent_waits(parent_session_id, &batch, ChildWaitPolicy::All)
198 .await
199 {
200 let mut pending = slot.pending.lock();
202 for item in batch {
203 pending.push(item);
204 }
205 return Err(error);
206 }
207 Ok(())
208 }
209
210 pub async fn register_parent_wait_for_children(
217 &self,
218 parent_session_id: &str,
219 child_session_ids: &[String],
220 policy: ChildWaitPolicy,
221 ) -> Result<usize, ChildSessionError> {
222 if child_session_ids.is_empty() {
223 return Ok(0);
224 }
225 let batch: Vec<(String, Option<String>)> = child_session_ids
226 .iter()
227 .map(|id| (id.clone(), None))
228 .collect();
229 self.flush_parent_waits(parent_session_id, &batch, policy)
230 .await?;
231 Ok(batch.len())
232 }
233
234 pub async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
237 self.storage
238 .list_child_run_statuses(parent_session_id)
239 .await
240 .unwrap_or_default()
241 .into_iter()
242 .filter(|(_, status)| !status.as_deref().is_some_and(is_terminal_child_status))
243 .map(|(id, _)| id)
244 .collect()
245 }
246
247 async fn flush_parent_waits(
249 &self,
250 parent_session_id: &str,
251 batch: &[(String, Option<String>)],
252 policy: ChildWaitPolicy,
253 ) -> Result<(), ChildSessionError> {
254 let Some(mut parent) =
255 self.storage
256 .load_session(parent_session_id)
257 .await
258 .map_err(|error| {
259 ChildSessionError::Execution(format!(
260 "failed to load parent session {parent_session_id}: {error}"
261 ))
262 })?
263 else {
264 return Err(ChildSessionError::NotFound(parent_session_id.to_string()));
265 };
266
267 let mut runtime_state = read_runtime_state(&parent);
271
272 let now = Utc::now();
273 let mut wait = runtime_state
274 .waiting_for_children
275 .take()
276 .unwrap_or_else(|| WaitingForChildrenState::for_children(Vec::new(), policy, now));
277 wait.wait_for = policy;
279 for (child_session_id, tool_call_id) in batch {
280 if !wait
281 .child_session_ids
282 .iter()
283 .any(|id| id == child_session_id)
284 {
285 wait.child_session_ids.push(child_session_id.clone());
286 }
287 if wait.registered_by_tool_call_id.is_none() {
288 wait.registered_by_tool_call_id = tool_call_id.clone();
289 }
290 }
291 wait.child_session_ids.sort();
292 wait.child_session_ids.dedup();
293 runtime_state.waiting_for_children = Some(wait);
294
295 write_runtime_state(&mut parent, &runtime_state);
296 parent.metadata.insert(
297 "runtime.suspend_reason".to_string(),
298 "waiting_for_children".to_string(),
299 );
300 parent.updated_at = Utc::now();
301
302 self.persistence
307 .save_runtime_only(&mut parent)
308 .await
309 .map_err(|error| {
310 ChildSessionError::Execution(format!("failed to save parent wait state: {error}"))
311 })?;
312 self.sessions_cache.insert(
313 parent.id.clone(),
314 Arc::new(parking_lot::RwLock::new(parent)),
315 );
316
317 Ok(())
318 }
319}
320
321fn map_index_entry_to_child_entry(entry: &SessionIndexEntry) -> ChildSessionEntry {
322 ChildSessionEntry {
323 child_session_id: entry.id.clone(),
324 title: entry.title.clone(),
325 pinned: entry.pinned,
326 message_count: entry.message_count,
327 updated_at: entry.updated_at.to_rfc3339(),
328 last_run_status: entry.last_run_status.clone(),
329 last_run_error: entry.last_run_error.clone(),
330 }
331}
332
333#[async_trait]
334impl SubagentResolutionPort for ChildSessionAdapter {
335 async fn resolve_subagent_model(
336 &self,
337 subagent_type: &str,
338 ) -> Option<bamboo_domain::ProviderModelRef> {
339 ChildSessionAdapter::resolve_subagent_model(self, subagent_type).await
340 }
341
342 async fn resolve_runtime_metadata(
343 &self,
344 subagent_type: &str,
345 ) -> std::collections::HashMap<String, String> {
346 ChildSessionAdapter::resolve_runtime_metadata(self, subagent_type).await
347 }
348}
349
350#[async_trait]
358impl bamboo_engine::GuardianSpawner for ChildSessionAdapter {
359 async fn spawn_guardian_review(
360 &self,
361 parent_session: &Session,
362 review_prompt: String,
363 model: String,
364 disabled_tools: Option<std::collections::BTreeSet<String>>,
365 ) -> Result<String, String> {
366 let input = bamboo_engine::session_app::child_session::CreateChildInput {
367 parent_session: parent_session.clone(),
368 child_id: format!("guardian-{}", uuid::Uuid::new_v4()),
369 title: "Guardian review".to_string(),
370 responsibility: "Adversarially verify the parent agent's completed work.".to_string(),
371 assignment_prompt: review_prompt,
372 subagent_type: "guardian".to_string(),
375 workspace: parent_session.workspace_path_meta().unwrap_or_default(),
376 model_override: Some(model),
377 model_ref_override: None,
378 runtime_metadata: HashMap::new(),
379 auto_run: true,
380 reasoning_effort: None,
381 lifecycle: None,
382 resident_name: None,
383 resident_context: None,
384 disabled_tools,
385 context_fork: None,
386 };
387 bamboo_engine::session_app::child_session::create_child_action(self, input)
388 .await
389 .map(|result| result.child_session_id)
390 .map_err(|error| error.to_string())
391 }
392}
393
394#[async_trait]
395impl ChildSessionPort for ChildSessionAdapter {
396 async fn load_root_session(&self, root_session_id: &str) -> Result<Session, ChildSessionError> {
397 let Some(session) = self
398 .storage
399 .load_session(root_session_id)
400 .await
401 .map_err(|error| {
402 ChildSessionError::Execution(format!(
403 "failed to load session {root_session_id}: {error}"
404 ))
405 })?
406 else {
407 return Err(ChildSessionError::NotFound(root_session_id.to_string()));
408 };
409
410 if session.kind != SessionKind::Root {
411 return Err(ChildSessionError::NotRootSession(
412 root_session_id.to_string(),
413 ));
414 }
415
416 Ok(session)
417 }
418
419 async fn load_child_for_parent(
420 &self,
421 parent_session_id: &str,
422 child_session_id: &str,
423 ) -> Result<Session, ChildSessionError> {
424 let Some(child) = self
425 .storage
426 .load_session(child_session_id)
427 .await
428 .map_err(|error| {
429 ChildSessionError::Execution(format!(
430 "failed to load child session {child_session_id}: {error}"
431 ))
432 })?
433 else {
434 return Err(ChildSessionError::NotFound(child_session_id.to_string()));
435 };
436
437 if child.kind != SessionKind::Child {
438 return Err(ChildSessionError::NotChildSession(
439 child_session_id.to_string(),
440 ));
441 }
442
443 if child.parent_session_id.as_deref() != Some(parent_session_id) {
444 return Err(ChildSessionError::NotChildOfParent {
445 child_id: child_session_id.to_string(),
446 parent_id: parent_session_id.to_string(),
447 });
448 }
449
450 Ok(child)
451 }
452
453 async fn save_child_session(&self, child: &mut Session) -> Result<(), ChildSessionError> {
454 self.persistence
455 .merge_save_runtime(child)
456 .await
457 .map_err(|error| {
458 ChildSessionError::Execution(format!("failed to save child session: {error}"))
459 })?;
460
461 self.sessions_cache.insert(
462 child.id.clone(),
463 Arc::new(parking_lot::RwLock::new(child.clone())),
464 );
465
466 Ok(())
467 }
468
469 async fn is_child_running(&self, child_session_id: &str) -> bool {
470 let runners = self.agent_runners.read().await;
471 runners
472 .get(child_session_id)
473 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
474 }
475
476 async fn list_children(&self, parent_session_id: &str) -> Vec<ChildSessionEntry> {
477 self.session_store
478 .list_index_entries()
479 .await
480 .into_iter()
481 .filter(|entry| {
482 entry.kind == SessionKind::Child
483 && entry.parent_session_id.as_deref() == Some(parent_session_id)
484 })
485 .map(|entry| map_index_entry_to_child_entry(&entry))
486 .collect()
487 }
488
489 async fn find_resident_child(
490 &self,
491 root_session_id: &str,
492 resident_name: &str,
493 ) -> Option<String> {
494 let name = resident_name.trim();
495 if name.is_empty() {
496 return None;
497 }
498 let mut best: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
502 for entry in self.session_store.list_index_entries().await {
503 if entry.kind == SessionKind::Child
504 && entry.root_session_id == root_session_id
505 && entry.resident_name.as_deref() == Some(name)
506 {
507 match &best {
508 Some((_, ts)) if *ts >= entry.updated_at => {}
509 _ => best = Some((entry.id.clone(), entry.updated_at)),
510 }
511 }
512 }
513 best.map(|(id, _)| id)
514 }
515
516 async fn enqueue_child_run(
517 &self,
518 parent: &Session,
519 child: &Session,
520 ) -> Result<(), ChildSessionError> {
521 let model = if child.model.trim().is_empty() {
522 parent.model.clone()
523 } else {
524 child.model.clone()
525 };
526 if model.trim().is_empty() {
527 return Err(ChildSessionError::Execution(
528 "child model is empty and parent model is unavailable".to_string(),
529 ));
530 }
531
532 let disabled_tools = child
538 .metadata
539 .get("disabled_tools")
540 .and_then(|raw| serde_json::from_str::<std::collections::BTreeSet<String>>(raw).ok())
541 .filter(|set| !set.is_empty())
542 .map(|set| set.into_iter().collect::<Vec<String>>());
543
544 self.scheduler
550 .enqueue(SpawnJob {
551 parent_session_id: parent.id.clone(),
552 child_session_id: child.id.clone(),
553 model,
554 disabled_tools,
555 })
556 .await
557 .map_err(ChildSessionError::Execution)?;
558
559 let parent_tx = get_or_create_event_sender(&self.session_event_senders, &parent.id).await;
560 let _ = parent_tx.send(AgentEvent::SubAgentStarted {
561 parent_session_id: parent.id.clone(),
562 child_session_id: child.id.clone(),
563 title: Some(child.title.clone()),
564 });
565
566 Ok(())
567 }
568
569 async fn cancel_child_run_and_wait(
570 &self,
571 child_session_id: &str,
572 ) -> Result<(), ChildSessionError> {
573 let cancelled = {
574 let mut runners = self.agent_runners.write().await;
575 if let Some(runner) = runners.get_mut(child_session_id) {
576 if matches!(runner.status, AgentStatus::Running) {
577 runner.cancel_token.cancel();
578 true
579 } else {
580 false
581 }
582 } else {
583 false
584 }
585 };
586
587 if !cancelled {
588 return Ok(());
589 }
590
591 let deadline = Instant::now() + Duration::from_secs(10);
592 loop {
593 let still_running = {
594 let runners = self.agent_runners.read().await;
595 runners
596 .get(child_session_id)
597 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
598 };
599 if !still_running {
600 return Ok(());
601 }
602 if Instant::now() >= deadline {
603 return Err(ChildSessionError::Execution(format!(
604 "timed out waiting for child session {child_session_id} to stop after cancellation"
605 )));
606 }
607 sleep(Duration::from_millis(50)).await;
608 }
609 }
610
611 async fn delete_child_session(
612 &self,
613 parent_session_id: &str,
614 child_id: &str,
615 ) -> Result<DeleteChildResult, ChildSessionError> {
616 let cancelled_running_child = {
617 let mut runners = self.agent_runners.write().await;
618 if let Some(runner) = runners.remove(child_id) {
619 runner.cancel_token.cancel();
620 true
621 } else {
622 false
623 }
624 };
625
626 let deleted = self
627 .storage
628 .delete_session(child_id)
629 .await
630 .map_err(|error| {
631 ChildSessionError::Execution(format!("failed to delete child session: {error}"))
632 })?;
633
634 self.sessions_cache.remove(child_id);
635 {
636 let mut senders = self.session_event_senders.write().await;
637 senders.remove(child_id);
638 if cancelled_running_child {
639 if let Some(parent_tx) = senders.get(parent_session_id) {
640 let _ = parent_tx.send(AgentEvent::SubAgentCompleted {
641 parent_session_id: parent_session_id.to_string(),
642 child_session_id: child_id.to_string(),
643 status: "cancelled".to_string(),
644 error: Some("Child session deleted while running".to_string()),
645 });
646 }
647 }
648 }
649
650 Ok(DeleteChildResult {
651 deleted,
652 cancelled_running_child,
653 })
654 }
655
656 async fn get_child_runner_info(&self, child_id: &str) -> Option<ChildRunnerInfo> {
657 let runners = self.agent_runners.read().await;
658 runners.get(child_id).map(|runner| ChildRunnerInfo {
659 started_at: Some(runner.started_at),
660 completed_at: runner.completed_at,
661 last_tool_name: runner.last_tool_name.clone(),
662 last_tool_phase: runner.last_tool_phase.clone(),
663 last_event_at: runner.last_event_at,
664 round_count: runner.round_count,
665 })
666 }
667
668 async fn register_parent_wait_for_child(
669 &self,
670 parent_session_id: &str,
671 child_session_id: &str,
672 tool_call_id: Option<&str>,
673 ) -> Result<(), ChildSessionError> {
674 ChildSessionAdapter::register_parent_wait_for_child(
675 self,
676 parent_session_id,
677 child_session_id,
678 tool_call_id,
679 )
680 .await
681 }
682
683 async fn register_parent_wait_for_children(
684 &self,
685 parent_session_id: &str,
686 child_session_ids: &[String],
687 policy: ChildWaitPolicy,
688 ) -> Result<usize, ChildSessionError> {
689 ChildSessionAdapter::register_parent_wait_for_children(
690 self,
691 parent_session_id,
692 child_session_ids,
693 policy,
694 )
695 .await
696 }
697
698 async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
699 ChildSessionAdapter::active_child_ids(self, parent_session_id).await
700 }
701
702 async fn ensure_child_indexed(&self, child_session_id: &str) {
703 let _ = self.session_store.get_index_entry(child_session_id).await;
704 }
705}