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 pub(crate) notification_relay: Option<crate::app_state::session_events::NotificationRelayDeps>,
64}
65
66#[derive(Default)]
75pub(crate) struct ParentWaitSlot {
76 flush_lock: tokio::sync::Mutex<()>,
77 pending: parking_lot::Mutex<Vec<(String, Option<String>)>>,
78}
79
80const AGENT_RUNTIME_STATE_METADATA_KEY: &str = "agent.runtime.state";
81
82fn is_terminal_child_status(status: &str) -> bool {
85 matches!(
86 status,
87 "completed" | "error" | "timeout" | "cancelled" | "skipped"
88 )
89}
90
91fn read_runtime_state(session: &Session) -> AgentRuntimeState {
92 session
93 .agent_runtime_state
94 .clone()
95 .or_else(|| {
96 session
97 .metadata
98 .get(AGENT_RUNTIME_STATE_METADATA_KEY)
99 .and_then(|raw| serde_json::from_str::<AgentRuntimeState>(raw).ok())
100 })
101 .unwrap_or_else(|| AgentRuntimeState::new(format!("{}-wait", session.id)))
102}
103
104fn write_runtime_state(session: &mut Session, runtime_state: &AgentRuntimeState) {
105 session.agent_runtime_state = Some(runtime_state.clone());
106 if let Ok(serialized) = serde_json::to_string(runtime_state) {
107 session
108 .metadata
109 .insert(AGENT_RUNTIME_STATE_METADATA_KEY.to_string(), serialized);
110 }
111}
112
113impl ChildSessionAdapter {
114 fn finish_child_save(
119 &self,
120 child: &Session,
121 saved: std::io::Result<()>,
122 ) -> Result<(), ChildSessionError> {
123 saved.map_err(|error| {
124 ChildSessionError::Execution(format!("failed to save child session: {error}"))
125 })?;
126 self.sessions_cache.insert(
127 child.id.clone(),
128 Arc::new(parking_lot::RwLock::new(child.clone())),
129 );
130 Ok(())
131 }
132
133 #[allow(clippy::too_many_arguments)]
138 pub fn new(
139 session_store: Arc<SessionStoreV2>,
140 storage: Arc<dyn Storage>,
141 persistence: Arc<LockedSessionStore>,
142 scheduler: Arc<SpawnScheduler>,
143 sessions_cache: bamboo_engine::SessionCache,
144 agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
145 session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
146 subagent_model_resolver: crate::tools::OptionalSubagentModelResolver,
147 config: Arc<RwLock<Config>>,
148 ) -> Self {
149 Self {
150 session_store,
151 storage,
152 persistence,
153 scheduler,
154 sessions_cache,
155 agent_runners,
156 session_event_senders,
157 subagent_model_resolver,
158 config,
159 parent_wait_slots: Arc::new(dashmap::DashMap::new()),
162 notification_relay: None,
166 }
167 }
168
169 pub async fn resolve_subagent_model(
171 &self,
172 subagent_type: &str,
173 ) -> Option<bamboo_domain::ProviderModelRef> {
174 match &self.subagent_model_resolver {
175 Some(resolver) => resolver(subagent_type.to_string()).await,
176 None => None,
177 }
178 }
179
180 pub async fn resolve_runtime_metadata(&self, subagent_type: &str) -> HashMap<String, String> {
182 let config = self.config.read().await;
183 bamboo_engine::external_agents::config::resolve_runtime_metadata(&config, subagent_type)
184 }
185
186 pub async fn register_parent_wait_for_child(
200 &self,
201 parent_session_id: &str,
202 child_session_id: &str,
203 tool_call_id: Option<&str>,
204 ) -> Result<(), ChildSessionError> {
205 let slot = self
206 .parent_wait_slots
207 .entry(parent_session_id.to_string())
208 .or_default()
209 .clone();
210
211 slot.pending.lock().push((
213 child_session_id.to_string(),
214 tool_call_id.map(str::to_string),
215 ));
216
217 let _flush_guard = slot.flush_lock.lock().await;
219
220 let batch: Vec<(String, Option<String>)> = {
223 let mut pending = slot.pending.lock();
224 pending.drain(..).collect()
225 };
226 if batch.is_empty() {
227 return Ok(());
230 }
231
232 if let Err(error) = self
234 .flush_parent_waits(parent_session_id, &batch, ChildWaitPolicy::All)
235 .await
236 {
237 let mut pending = slot.pending.lock();
239 for item in batch {
240 pending.push(item);
241 }
242 return Err(error);
243 }
244
245 self.parent_wait_slots
259 .remove_if(parent_session_id, |_, slot| slot.pending.lock().is_empty());
260
261 Ok(())
262 }
263
264 pub async fn register_parent_wait_for_children(
271 &self,
272 parent_session_id: &str,
273 child_session_ids: &[String],
274 policy: ChildWaitPolicy,
275 ) -> Result<usize, ChildSessionError> {
276 if child_session_ids.is_empty() {
277 return Ok(0);
278 }
279 let batch: Vec<(String, Option<String>)> = child_session_ids
280 .iter()
281 .map(|id| (id.clone(), None))
282 .collect();
283 self.flush_parent_waits(parent_session_id, &batch, policy)
284 .await?;
285 Ok(batch.len())
286 }
287
288 pub async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
291 self.storage
292 .list_child_run_statuses(parent_session_id)
293 .await
294 .unwrap_or_default()
295 .into_iter()
296 .filter(|(_, status)| !status.as_deref().is_some_and(is_terminal_child_status))
297 .map(|(id, _)| id)
298 .collect()
299 }
300
301 pub async fn terminal_child_ids(
306 &self,
307 parent_session_id: &str,
308 candidates: &[String],
309 ) -> Vec<(String, String)> {
310 let statuses = self
311 .storage
312 .list_child_run_statuses(parent_session_id)
313 .await
314 .unwrap_or_default();
315 candidates
316 .iter()
317 .filter_map(|candidate| {
318 statuses.iter().find_map(|(id, status)| {
319 let status = status.as_deref()?;
320 (id == candidate && is_terminal_child_status(status))
321 .then(|| (candidate.clone(), status.to_string()))
322 })
323 })
324 .collect()
325 }
326
327 async fn flush_parent_waits(
329 &self,
330 parent_session_id: &str,
331 batch: &[(String, Option<String>)],
332 policy: ChildWaitPolicy,
333 ) -> Result<(), ChildSessionError> {
334 let Some(mut parent) =
335 self.storage
336 .load_session(parent_session_id)
337 .await
338 .map_err(|error| {
339 ChildSessionError::Execution(format!(
340 "failed to load parent session {parent_session_id}: {error}"
341 ))
342 })?
343 else {
344 return Err(ChildSessionError::NotFound(parent_session_id.to_string()));
345 };
346
347 let mut runtime_state = read_runtime_state(&parent);
351
352 let now = Utc::now();
353 let mut wait = runtime_state
354 .waiting_for_children
355 .take()
356 .unwrap_or_else(|| WaitingForChildrenState::for_children(Vec::new(), policy, now));
357 wait.wait_for = policy;
359 for (child_session_id, tool_call_id) in batch {
360 if !wait
361 .child_session_ids
362 .iter()
363 .any(|id| id == child_session_id)
364 {
365 wait.child_session_ids.push(child_session_id.clone());
366 }
367 if wait.registered_by_tool_call_id.is_none() {
368 wait.registered_by_tool_call_id = tool_call_id.clone();
369 }
370 }
371 wait.child_session_ids.sort();
372 wait.child_session_ids.dedup();
373 runtime_state.waiting_for_children = Some(wait);
374
375 write_runtime_state(&mut parent, &runtime_state);
376 parent.metadata.insert(
377 "runtime.suspend_reason".to_string(),
378 "waiting_for_children".to_string(),
379 );
380 parent.updated_at = Utc::now();
381
382 self.persistence
387 .save_runtime_only(&mut parent)
388 .await
389 .map_err(|error| {
390 ChildSessionError::Execution(format!("failed to save parent wait state: {error}"))
391 })?;
392 self.sessions_cache.insert(
393 parent.id.clone(),
394 Arc::new(parking_lot::RwLock::new(parent)),
395 );
396
397 Ok(())
398 }
399}
400
401fn map_index_entry_to_child_entry(entry: &SessionIndexEntry) -> ChildSessionEntry {
402 ChildSessionEntry {
403 child_session_id: entry.id.clone(),
404 title: entry.title.clone(),
405 pinned: entry.pinned,
406 message_count: entry.message_count,
407 updated_at: entry.updated_at.to_rfc3339(),
408 last_run_status: entry.last_run_status.clone(),
409 last_run_error: entry.last_run_error.clone(),
410 }
411}
412
413#[async_trait]
414impl SubagentResolutionPort for ChildSessionAdapter {
415 async fn resolve_subagent_model(
416 &self,
417 subagent_type: &str,
418 ) -> Option<bamboo_domain::ProviderModelRef> {
419 ChildSessionAdapter::resolve_subagent_model(self, subagent_type).await
420 }
421
422 async fn resolve_runtime_metadata(
423 &self,
424 subagent_type: &str,
425 ) -> std::collections::HashMap<String, String> {
426 ChildSessionAdapter::resolve_runtime_metadata(self, subagent_type).await
427 }
428}
429
430#[async_trait]
438impl bamboo_engine::GuardianSpawner for ChildSessionAdapter {
439 async fn spawn_guardian_review(
440 &self,
441 parent_session: &Session,
442 review_prompt: String,
443 model: String,
444 disabled_tools: Option<std::collections::BTreeSet<String>>,
445 ) -> Result<String, String> {
446 let input = bamboo_engine::session_app::child_session::CreateChildInput {
447 parent_session: parent_session.clone(),
448 child_id: format!("guardian-{}", uuid::Uuid::new_v4()),
449 title: "Guardian review".to_string(),
450 responsibility: "Adversarially verify the parent agent's completed work.".to_string(),
451 assignment_prompt: review_prompt,
452 subagent_type: "guardian".to_string(),
455 workspace: parent_session.workspace_path_meta().unwrap_or_default(),
456 model_override: Some(model),
457 model_ref_override: None,
458 runtime_metadata: HashMap::new(),
459 auto_run: true,
460 reasoning_effort: None,
461 lifecycle: None,
462 resident_name: None,
463 resident_context: None,
464 disabled_tools,
465 context_fork: None,
466 };
467 bamboo_engine::session_app::child_session::create_child_action(self, input)
468 .await
469 .map(|result| result.child_session_id)
470 .map_err(|error| error.to_string())
471 }
472}
473
474#[async_trait]
475impl ChildSessionPort for ChildSessionAdapter {
476 async fn load_root_session(&self, root_session_id: &str) -> Result<Session, ChildSessionError> {
477 let Some(session) = self
478 .storage
479 .load_session(root_session_id)
480 .await
481 .map_err(|error| {
482 ChildSessionError::Execution(format!(
483 "failed to load session {root_session_id}: {error}"
484 ))
485 })?
486 else {
487 return Err(ChildSessionError::NotFound(root_session_id.to_string()));
488 };
489
490 if session.kind != SessionKind::Root {
491 return Err(ChildSessionError::NotRootSession(
492 root_session_id.to_string(),
493 ));
494 }
495
496 Ok(session)
497 }
498
499 async fn load_child_for_parent(
500 &self,
501 parent_session_id: &str,
502 child_session_id: &str,
503 ) -> Result<Session, ChildSessionError> {
504 let Some(child) = self
505 .storage
506 .load_session(child_session_id)
507 .await
508 .map_err(|error| {
509 ChildSessionError::Execution(format!(
510 "failed to load child session {child_session_id}: {error}"
511 ))
512 })?
513 else {
514 return Err(ChildSessionError::NotFound(child_session_id.to_string()));
515 };
516
517 if child.kind != SessionKind::Child {
518 return Err(ChildSessionError::NotChildSession(
519 child_session_id.to_string(),
520 ));
521 }
522
523 if child.parent_session_id.as_deref() != Some(parent_session_id) {
524 return Err(ChildSessionError::NotChildOfParent {
525 child_id: child_session_id.to_string(),
526 parent_id: parent_session_id.to_string(),
527 });
528 }
529
530 Ok(child)
531 }
532
533 async fn save_child_session(&self, child: &mut Session) -> Result<(), ChildSessionError> {
534 let saved = self.persistence.merge_save_runtime(child).await;
538 self.finish_child_save(child, saved)
539 }
540
541 async fn save_child_session_authoritative_flags(
542 &self,
543 child: &mut Session,
544 ) -> Result<(), ChildSessionError> {
545 let saved = self
549 .persistence
550 .save_runtime_authoritative_flags(child)
551 .await;
552 self.finish_child_save(child, saved)
553 }
554
555 async fn is_child_running(&self, child_session_id: &str) -> bool {
556 let runners = self.agent_runners.read().await;
557 runners
558 .get(child_session_id)
559 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
560 }
561
562 async fn list_children(&self, parent_session_id: &str) -> Vec<ChildSessionEntry> {
563 self.session_store
564 .list_index_entries()
565 .await
566 .into_iter()
567 .filter(|entry| {
568 entry.kind == SessionKind::Child
569 && entry.parent_session_id.as_deref() == Some(parent_session_id)
570 })
571 .map(|entry| map_index_entry_to_child_entry(&entry))
572 .collect()
573 }
574
575 async fn find_resident_child(
576 &self,
577 root_session_id: &str,
578 resident_name: &str,
579 ) -> Option<String> {
580 let name = resident_name.trim();
581 if name.is_empty() {
582 return None;
583 }
584 let mut best: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
588 for entry in self.session_store.list_index_entries().await {
589 if entry.kind == SessionKind::Child
590 && entry.root_session_id == root_session_id
591 && entry.resident_name.as_deref() == Some(name)
592 {
593 match &best {
594 Some((_, ts)) if *ts >= entry.updated_at => {}
595 _ => best = Some((entry.id.clone(), entry.updated_at)),
596 }
597 }
598 }
599 best.map(|(id, _)| id)
600 }
601
602 async fn enqueue_child_run(
603 &self,
604 parent: &Session,
605 child: &Session,
606 ) -> Result<(), ChildSessionError> {
607 let model = if child.model.trim().is_empty() {
608 parent.model.clone()
609 } else {
610 child.model.clone()
611 };
612 if model.trim().is_empty() {
613 return Err(ChildSessionError::Execution(
614 "child model is empty and parent model is unavailable".to_string(),
615 ));
616 }
617
618 let disabled_tools = child
624 .metadata
625 .get("disabled_tools")
626 .and_then(|raw| serde_json::from_str::<std::collections::BTreeSet<String>>(raw).ok())
627 .filter(|set| !set.is_empty())
628 .map(|set| set.into_iter().collect::<Vec<String>>());
629
630 if let Some(relay) = &self.notification_relay {
638 let child_tx = get_or_create_event_sender(&self.session_event_senders, &child.id).await;
639 crate::app_state::session_events::ensure_notification_relay(relay, &child.id, child_tx);
640 }
641
642 self.scheduler
648 .enqueue(SpawnJob {
649 parent_session_id: parent.id.clone(),
650 child_session_id: child.id.clone(),
651 model,
652 disabled_tools,
653 })
654 .await
655 .map_err(ChildSessionError::Execution)?;
656
657 let parent_tx = get_or_create_event_sender(&self.session_event_senders, &parent.id).await;
658 let _ = parent_tx.send(AgentEvent::SubAgentStarted {
659 parent_session_id: parent.id.clone(),
660 child_session_id: child.id.clone(),
661 title: Some(child.title.clone()),
662 });
663
664 Ok(())
665 }
666
667 async fn cancel_child_run_and_wait(
668 &self,
669 child_session_id: &str,
670 ) -> Result<(), ChildSessionError> {
671 let cancelled = {
672 let mut runners = self.agent_runners.write().await;
673 if let Some(runner) = runners.get_mut(child_session_id) {
674 if matches!(runner.status, AgentStatus::Running) {
675 runner.cancel_token.cancel();
676 true
677 } else {
678 false
679 }
680 } else {
681 false
682 }
683 };
684
685 if !cancelled {
686 return Ok(());
687 }
688
689 let deadline = Instant::now() + Duration::from_secs(10);
690 loop {
691 let still_running = {
692 let runners = self.agent_runners.read().await;
693 runners
694 .get(child_session_id)
695 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
696 };
697 if !still_running {
698 return Ok(());
699 }
700 if Instant::now() >= deadline {
701 return Err(ChildSessionError::Execution(format!(
702 "timed out waiting for child session {child_session_id} to stop after cancellation"
703 )));
704 }
705 sleep(Duration::from_millis(50)).await;
706 }
707 }
708
709 async fn delete_child_session(
710 &self,
711 parent_session_id: &str,
712 child_id: &str,
713 ) -> Result<DeleteChildResult, ChildSessionError> {
714 let cancelled_running_child = {
715 let mut runners = self.agent_runners.write().await;
716 if let Some(runner) = runners.remove(child_id) {
717 runner.cancel_token.cancel();
718 true
719 } else {
720 false
721 }
722 };
723
724 let deleted = self
725 .storage
726 .delete_session(child_id)
727 .await
728 .map_err(|error| {
729 ChildSessionError::Execution(format!("failed to delete child session: {error}"))
730 })?;
731
732 self.sessions_cache.remove(child_id);
733 {
734 let mut senders = self.session_event_senders.write().await;
735 senders.remove(child_id);
736 if cancelled_running_child {
737 if let Some(parent_tx) = senders.get(parent_session_id) {
738 let _ = parent_tx.send(AgentEvent::SubAgentCompleted {
739 parent_session_id: parent_session_id.to_string(),
740 child_session_id: child_id.to_string(),
741 status: "cancelled".to_string(),
742 error: Some("Child session deleted while running".to_string()),
743 });
744 }
745 }
746 }
747
748 Ok(DeleteChildResult {
749 deleted,
750 cancelled_running_child,
751 })
752 }
753
754 async fn get_child_runner_info(&self, child_id: &str) -> Option<ChildRunnerInfo> {
755 let runners = self.agent_runners.read().await;
756 runners.get(child_id).map(|runner| ChildRunnerInfo {
757 started_at: Some(runner.started_at),
758 completed_at: runner.completed_at,
759 last_tool_name: runner.last_tool_name.clone(),
760 last_tool_phase: runner.last_tool_phase.clone(),
761 last_event_at: runner.last_event_at,
762 round_count: runner.round_count,
763 })
764 }
765
766 async fn register_parent_wait_for_child(
767 &self,
768 parent_session_id: &str,
769 child_session_id: &str,
770 tool_call_id: Option<&str>,
771 ) -> Result<(), ChildSessionError> {
772 ChildSessionAdapter::register_parent_wait_for_child(
773 self,
774 parent_session_id,
775 child_session_id,
776 tool_call_id,
777 )
778 .await
779 }
780
781 async fn register_parent_wait_for_children(
782 &self,
783 parent_session_id: &str,
784 child_session_ids: &[String],
785 policy: ChildWaitPolicy,
786 ) -> Result<usize, ChildSessionError> {
787 ChildSessionAdapter::register_parent_wait_for_children(
788 self,
789 parent_session_id,
790 child_session_ids,
791 policy,
792 )
793 .await
794 }
795
796 async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
797 ChildSessionAdapter::active_child_ids(self, parent_session_id).await
798 }
799
800 async fn terminal_child_ids(
801 &self,
802 parent_session_id: &str,
803 candidates: &[String],
804 ) -> Vec<(String, String)> {
805 ChildSessionAdapter::terminal_child_ids(self, parent_session_id, candidates).await
806 }
807
808 async fn ensure_child_indexed(&self, child_session_id: &str) {
809 let _ = self.session_store.get_index_entry(child_session_id).await;
810 }
811}