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
208 self.parent_wait_slots
222 .remove_if(parent_session_id, |_, slot| slot.pending.lock().is_empty());
223
224 Ok(())
225 }
226
227 pub async fn register_parent_wait_for_children(
234 &self,
235 parent_session_id: &str,
236 child_session_ids: &[String],
237 policy: ChildWaitPolicy,
238 ) -> Result<usize, ChildSessionError> {
239 if child_session_ids.is_empty() {
240 return Ok(0);
241 }
242 let batch: Vec<(String, Option<String>)> = child_session_ids
243 .iter()
244 .map(|id| (id.clone(), None))
245 .collect();
246 self.flush_parent_waits(parent_session_id, &batch, policy)
247 .await?;
248 Ok(batch.len())
249 }
250
251 pub async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
254 self.storage
255 .list_child_run_statuses(parent_session_id)
256 .await
257 .unwrap_or_default()
258 .into_iter()
259 .filter(|(_, status)| !status.as_deref().is_some_and(is_terminal_child_status))
260 .map(|(id, _)| id)
261 .collect()
262 }
263
264 async fn flush_parent_waits(
266 &self,
267 parent_session_id: &str,
268 batch: &[(String, Option<String>)],
269 policy: ChildWaitPolicy,
270 ) -> Result<(), ChildSessionError> {
271 let Some(mut parent) =
272 self.storage
273 .load_session(parent_session_id)
274 .await
275 .map_err(|error| {
276 ChildSessionError::Execution(format!(
277 "failed to load parent session {parent_session_id}: {error}"
278 ))
279 })?
280 else {
281 return Err(ChildSessionError::NotFound(parent_session_id.to_string()));
282 };
283
284 let mut runtime_state = read_runtime_state(&parent);
288
289 let now = Utc::now();
290 let mut wait = runtime_state
291 .waiting_for_children
292 .take()
293 .unwrap_or_else(|| WaitingForChildrenState::for_children(Vec::new(), policy, now));
294 wait.wait_for = policy;
296 for (child_session_id, tool_call_id) in batch {
297 if !wait
298 .child_session_ids
299 .iter()
300 .any(|id| id == child_session_id)
301 {
302 wait.child_session_ids.push(child_session_id.clone());
303 }
304 if wait.registered_by_tool_call_id.is_none() {
305 wait.registered_by_tool_call_id = tool_call_id.clone();
306 }
307 }
308 wait.child_session_ids.sort();
309 wait.child_session_ids.dedup();
310 runtime_state.waiting_for_children = Some(wait);
311
312 write_runtime_state(&mut parent, &runtime_state);
313 parent.metadata.insert(
314 "runtime.suspend_reason".to_string(),
315 "waiting_for_children".to_string(),
316 );
317 parent.updated_at = Utc::now();
318
319 self.persistence
324 .save_runtime_only(&mut parent)
325 .await
326 .map_err(|error| {
327 ChildSessionError::Execution(format!("failed to save parent wait state: {error}"))
328 })?;
329 self.sessions_cache.insert(
330 parent.id.clone(),
331 Arc::new(parking_lot::RwLock::new(parent)),
332 );
333
334 Ok(())
335 }
336}
337
338fn map_index_entry_to_child_entry(entry: &SessionIndexEntry) -> ChildSessionEntry {
339 ChildSessionEntry {
340 child_session_id: entry.id.clone(),
341 title: entry.title.clone(),
342 pinned: entry.pinned,
343 message_count: entry.message_count,
344 updated_at: entry.updated_at.to_rfc3339(),
345 last_run_status: entry.last_run_status.clone(),
346 last_run_error: entry.last_run_error.clone(),
347 }
348}
349
350#[async_trait]
351impl SubagentResolutionPort for ChildSessionAdapter {
352 async fn resolve_subagent_model(
353 &self,
354 subagent_type: &str,
355 ) -> Option<bamboo_domain::ProviderModelRef> {
356 ChildSessionAdapter::resolve_subagent_model(self, subagent_type).await
357 }
358
359 async fn resolve_runtime_metadata(
360 &self,
361 subagent_type: &str,
362 ) -> std::collections::HashMap<String, String> {
363 ChildSessionAdapter::resolve_runtime_metadata(self, subagent_type).await
364 }
365}
366
367#[async_trait]
375impl bamboo_engine::GuardianSpawner for ChildSessionAdapter {
376 async fn spawn_guardian_review(
377 &self,
378 parent_session: &Session,
379 review_prompt: String,
380 model: String,
381 disabled_tools: Option<std::collections::BTreeSet<String>>,
382 ) -> Result<String, String> {
383 let input = bamboo_engine::session_app::child_session::CreateChildInput {
384 parent_session: parent_session.clone(),
385 child_id: format!("guardian-{}", uuid::Uuid::new_v4()),
386 title: "Guardian review".to_string(),
387 responsibility: "Adversarially verify the parent agent's completed work.".to_string(),
388 assignment_prompt: review_prompt,
389 subagent_type: "guardian".to_string(),
392 workspace: parent_session.workspace_path_meta().unwrap_or_default(),
393 model_override: Some(model),
394 model_ref_override: None,
395 runtime_metadata: HashMap::new(),
396 auto_run: true,
397 reasoning_effort: None,
398 lifecycle: None,
399 resident_name: None,
400 resident_context: None,
401 disabled_tools,
402 context_fork: None,
403 };
404 bamboo_engine::session_app::child_session::create_child_action(self, input)
405 .await
406 .map(|result| result.child_session_id)
407 .map_err(|error| error.to_string())
408 }
409}
410
411#[async_trait]
412impl ChildSessionPort for ChildSessionAdapter {
413 async fn load_root_session(&self, root_session_id: &str) -> Result<Session, ChildSessionError> {
414 let Some(session) = self
415 .storage
416 .load_session(root_session_id)
417 .await
418 .map_err(|error| {
419 ChildSessionError::Execution(format!(
420 "failed to load session {root_session_id}: {error}"
421 ))
422 })?
423 else {
424 return Err(ChildSessionError::NotFound(root_session_id.to_string()));
425 };
426
427 if session.kind != SessionKind::Root {
428 return Err(ChildSessionError::NotRootSession(
429 root_session_id.to_string(),
430 ));
431 }
432
433 Ok(session)
434 }
435
436 async fn load_child_for_parent(
437 &self,
438 parent_session_id: &str,
439 child_session_id: &str,
440 ) -> Result<Session, ChildSessionError> {
441 let Some(child) = self
442 .storage
443 .load_session(child_session_id)
444 .await
445 .map_err(|error| {
446 ChildSessionError::Execution(format!(
447 "failed to load child session {child_session_id}: {error}"
448 ))
449 })?
450 else {
451 return Err(ChildSessionError::NotFound(child_session_id.to_string()));
452 };
453
454 if child.kind != SessionKind::Child {
455 return Err(ChildSessionError::NotChildSession(
456 child_session_id.to_string(),
457 ));
458 }
459
460 if child.parent_session_id.as_deref() != Some(parent_session_id) {
461 return Err(ChildSessionError::NotChildOfParent {
462 child_id: child_session_id.to_string(),
463 parent_id: parent_session_id.to_string(),
464 });
465 }
466
467 Ok(child)
468 }
469
470 async fn save_child_session(&self, child: &mut Session) -> Result<(), ChildSessionError> {
471 self.persistence
472 .merge_save_runtime(child)
473 .await
474 .map_err(|error| {
475 ChildSessionError::Execution(format!("failed to save child session: {error}"))
476 })?;
477
478 self.sessions_cache.insert(
479 child.id.clone(),
480 Arc::new(parking_lot::RwLock::new(child.clone())),
481 );
482
483 Ok(())
484 }
485
486 async fn is_child_running(&self, child_session_id: &str) -> bool {
487 let runners = self.agent_runners.read().await;
488 runners
489 .get(child_session_id)
490 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
491 }
492
493 async fn list_children(&self, parent_session_id: &str) -> Vec<ChildSessionEntry> {
494 self.session_store
495 .list_index_entries()
496 .await
497 .into_iter()
498 .filter(|entry| {
499 entry.kind == SessionKind::Child
500 && entry.parent_session_id.as_deref() == Some(parent_session_id)
501 })
502 .map(|entry| map_index_entry_to_child_entry(&entry))
503 .collect()
504 }
505
506 async fn find_resident_child(
507 &self,
508 root_session_id: &str,
509 resident_name: &str,
510 ) -> Option<String> {
511 let name = resident_name.trim();
512 if name.is_empty() {
513 return None;
514 }
515 let mut best: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
519 for entry in self.session_store.list_index_entries().await {
520 if entry.kind == SessionKind::Child
521 && entry.root_session_id == root_session_id
522 && entry.resident_name.as_deref() == Some(name)
523 {
524 match &best {
525 Some((_, ts)) if *ts >= entry.updated_at => {}
526 _ => best = Some((entry.id.clone(), entry.updated_at)),
527 }
528 }
529 }
530 best.map(|(id, _)| id)
531 }
532
533 async fn enqueue_child_run(
534 &self,
535 parent: &Session,
536 child: &Session,
537 ) -> Result<(), ChildSessionError> {
538 let model = if child.model.trim().is_empty() {
539 parent.model.clone()
540 } else {
541 child.model.clone()
542 };
543 if model.trim().is_empty() {
544 return Err(ChildSessionError::Execution(
545 "child model is empty and parent model is unavailable".to_string(),
546 ));
547 }
548
549 let disabled_tools = child
555 .metadata
556 .get("disabled_tools")
557 .and_then(|raw| serde_json::from_str::<std::collections::BTreeSet<String>>(raw).ok())
558 .filter(|set| !set.is_empty())
559 .map(|set| set.into_iter().collect::<Vec<String>>());
560
561 self.scheduler
567 .enqueue(SpawnJob {
568 parent_session_id: parent.id.clone(),
569 child_session_id: child.id.clone(),
570 model,
571 disabled_tools,
572 })
573 .await
574 .map_err(ChildSessionError::Execution)?;
575
576 let parent_tx = get_or_create_event_sender(&self.session_event_senders, &parent.id).await;
577 let _ = parent_tx.send(AgentEvent::SubAgentStarted {
578 parent_session_id: parent.id.clone(),
579 child_session_id: child.id.clone(),
580 title: Some(child.title.clone()),
581 });
582
583 Ok(())
584 }
585
586 async fn cancel_child_run_and_wait(
587 &self,
588 child_session_id: &str,
589 ) -> Result<(), ChildSessionError> {
590 let cancelled = {
591 let mut runners = self.agent_runners.write().await;
592 if let Some(runner) = runners.get_mut(child_session_id) {
593 if matches!(runner.status, AgentStatus::Running) {
594 runner.cancel_token.cancel();
595 true
596 } else {
597 false
598 }
599 } else {
600 false
601 }
602 };
603
604 if !cancelled {
605 return Ok(());
606 }
607
608 let deadline = Instant::now() + Duration::from_secs(10);
609 loop {
610 let still_running = {
611 let runners = self.agent_runners.read().await;
612 runners
613 .get(child_session_id)
614 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
615 };
616 if !still_running {
617 return Ok(());
618 }
619 if Instant::now() >= deadline {
620 return Err(ChildSessionError::Execution(format!(
621 "timed out waiting for child session {child_session_id} to stop after cancellation"
622 )));
623 }
624 sleep(Duration::from_millis(50)).await;
625 }
626 }
627
628 async fn delete_child_session(
629 &self,
630 parent_session_id: &str,
631 child_id: &str,
632 ) -> Result<DeleteChildResult, ChildSessionError> {
633 let cancelled_running_child = {
634 let mut runners = self.agent_runners.write().await;
635 if let Some(runner) = runners.remove(child_id) {
636 runner.cancel_token.cancel();
637 true
638 } else {
639 false
640 }
641 };
642
643 let deleted = self
644 .storage
645 .delete_session(child_id)
646 .await
647 .map_err(|error| {
648 ChildSessionError::Execution(format!("failed to delete child session: {error}"))
649 })?;
650
651 self.sessions_cache.remove(child_id);
652 {
653 let mut senders = self.session_event_senders.write().await;
654 senders.remove(child_id);
655 if cancelled_running_child {
656 if let Some(parent_tx) = senders.get(parent_session_id) {
657 let _ = parent_tx.send(AgentEvent::SubAgentCompleted {
658 parent_session_id: parent_session_id.to_string(),
659 child_session_id: child_id.to_string(),
660 status: "cancelled".to_string(),
661 error: Some("Child session deleted while running".to_string()),
662 });
663 }
664 }
665 }
666
667 Ok(DeleteChildResult {
668 deleted,
669 cancelled_running_child,
670 })
671 }
672
673 async fn get_child_runner_info(&self, child_id: &str) -> Option<ChildRunnerInfo> {
674 let runners = self.agent_runners.read().await;
675 runners.get(child_id).map(|runner| ChildRunnerInfo {
676 started_at: Some(runner.started_at),
677 completed_at: runner.completed_at,
678 last_tool_name: runner.last_tool_name.clone(),
679 last_tool_phase: runner.last_tool_phase.clone(),
680 last_event_at: runner.last_event_at,
681 round_count: runner.round_count,
682 })
683 }
684
685 async fn register_parent_wait_for_child(
686 &self,
687 parent_session_id: &str,
688 child_session_id: &str,
689 tool_call_id: Option<&str>,
690 ) -> Result<(), ChildSessionError> {
691 ChildSessionAdapter::register_parent_wait_for_child(
692 self,
693 parent_session_id,
694 child_session_id,
695 tool_call_id,
696 )
697 .await
698 }
699
700 async fn register_parent_wait_for_children(
701 &self,
702 parent_session_id: &str,
703 child_session_ids: &[String],
704 policy: ChildWaitPolicy,
705 ) -> Result<usize, ChildSessionError> {
706 ChildSessionAdapter::register_parent_wait_for_children(
707 self,
708 parent_session_id,
709 child_session_ids,
710 policy,
711 )
712 .await
713 }
714
715 async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
716 ChildSessionAdapter::active_child_ids(self, parent_session_id).await
717 }
718
719 async fn ensure_child_indexed(&self, child_session_id: &str) {
720 let _ = self.session_store.get_index_entry(child_session_id).await;
721 }
722}