1use std::collections::HashMap;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use chrono::{Duration as ChronoDuration, 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) subagent_profiles: Arc<bamboo_domain::subagent::SubagentProfileRegistry>,
48 pub(crate) tool_names: Vec<String>,
51 pub(crate) parent_wait_slots: Arc<dashmap::DashMap<String, Arc<ParentWaitSlot>>>,
56}
57
58#[derive(Default)]
67pub(crate) struct ParentWaitSlot {
68 flush_lock: tokio::sync::Mutex<()>,
69 pending: parking_lot::Mutex<Vec<(String, Option<String>)>>,
70}
71
72const AGENT_RUNTIME_STATE_METADATA_KEY: &str = "agent.runtime.state";
73
74fn is_terminal_child_status(status: &str) -> bool {
77 matches!(
78 status,
79 "completed" | "error" | "timeout" | "cancelled" | "skipped"
80 )
81}
82
83fn read_runtime_state(session: &Session) -> AgentRuntimeState {
84 session
85 .agent_runtime_state
86 .clone()
87 .or_else(|| {
88 session
89 .metadata
90 .get(AGENT_RUNTIME_STATE_METADATA_KEY)
91 .and_then(|raw| serde_json::from_str::<AgentRuntimeState>(raw).ok())
92 })
93 .unwrap_or_else(|| AgentRuntimeState::new(format!("{}-wait", session.id)))
94}
95
96fn write_runtime_state(session: &mut Session, runtime_state: &AgentRuntimeState) {
97 session.agent_runtime_state = Some(runtime_state.clone());
98 if let Ok(serialized) = serde_json::to_string(runtime_state) {
99 session
100 .metadata
101 .insert(AGENT_RUNTIME_STATE_METADATA_KEY.to_string(), serialized);
102 }
103}
104
105impl ChildSessionAdapter {
106 pub async fn resolve_subagent_model(
108 &self,
109 subagent_type: &str,
110 ) -> Option<bamboo_domain::ProviderModelRef> {
111 match &self.subagent_model_resolver {
112 Some(resolver) => resolver(subagent_type.to_string()).await,
113 None => None,
114 }
115 }
116
117 pub async fn resolve_runtime_metadata(&self, subagent_type: &str) -> HashMap<String, String> {
119 let config = self.config.read().await;
120 bamboo_engine::external_agents::config::resolve_runtime_metadata(&config, subagent_type)
121 }
122
123 pub fn resolve_subagent_prompt(&self, subagent_type: &str) -> String {
129 self.subagent_profiles
130 .resolve(subagent_type)
131 .system_prompt
132 .clone()
133 }
134
135 pub async fn register_parent_wait_for_child(
149 &self,
150 parent_session_id: &str,
151 child_session_id: &str,
152 tool_call_id: Option<&str>,
153 ) -> Result<(), ChildSessionError> {
154 let slot = self
155 .parent_wait_slots
156 .entry(parent_session_id.to_string())
157 .or_default()
158 .clone();
159
160 slot.pending.lock().push((
162 child_session_id.to_string(),
163 tool_call_id.map(str::to_string),
164 ));
165
166 let _flush_guard = slot.flush_lock.lock().await;
168
169 let batch: Vec<(String, Option<String>)> = {
172 let mut pending = slot.pending.lock();
173 pending.drain(..).collect()
174 };
175 if batch.is_empty() {
176 return Ok(());
179 }
180
181 if let Err(error) = self
183 .flush_parent_waits(parent_session_id, &batch, ChildWaitPolicy::All)
184 .await
185 {
186 let mut pending = slot.pending.lock();
188 for item in batch {
189 pending.push(item);
190 }
191 return Err(error);
192 }
193 Ok(())
194 }
195
196 pub async fn register_parent_wait_for_children(
203 &self,
204 parent_session_id: &str,
205 child_session_ids: &[String],
206 policy: ChildWaitPolicy,
207 ) -> Result<usize, ChildSessionError> {
208 if child_session_ids.is_empty() {
209 return Ok(0);
210 }
211 let batch: Vec<(String, Option<String>)> = child_session_ids
212 .iter()
213 .map(|id| (id.clone(), None))
214 .collect();
215 self.flush_parent_waits(parent_session_id, &batch, policy)
216 .await?;
217 Ok(batch.len())
218 }
219
220 pub async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
223 self.storage
224 .list_child_run_statuses(parent_session_id)
225 .await
226 .unwrap_or_default()
227 .into_iter()
228 .filter(|(_, status)| !status.as_deref().is_some_and(is_terminal_child_status))
229 .map(|(id, _)| id)
230 .collect()
231 }
232
233 async fn flush_parent_waits(
235 &self,
236 parent_session_id: &str,
237 batch: &[(String, Option<String>)],
238 policy: ChildWaitPolicy,
239 ) -> Result<(), ChildSessionError> {
240 let Some(mut parent) =
241 self.storage
242 .load_session(parent_session_id)
243 .await
244 .map_err(|error| {
245 ChildSessionError::Execution(format!(
246 "failed to load parent session {parent_session_id}: {error}"
247 ))
248 })?
249 else {
250 return Err(ChildSessionError::NotFound(parent_session_id.to_string()));
251 };
252
253 let mut runtime_state = read_runtime_state(&parent);
257
258 let now = Utc::now();
259 let mut wait = runtime_state
260 .waiting_for_children
261 .take()
262 .unwrap_or_else(|| WaitingForChildrenState {
263 child_session_ids: Vec::new(),
264 wait_for: policy,
265 registered_at: now,
266 timeout_at: Some(now + ChronoDuration::hours(6)),
267 registered_by_tool_call_id: None,
268 });
269 wait.wait_for = policy;
271 for (child_session_id, tool_call_id) in batch {
272 if !wait
273 .child_session_ids
274 .iter()
275 .any(|id| id == child_session_id)
276 {
277 wait.child_session_ids.push(child_session_id.clone());
278 }
279 if wait.registered_by_tool_call_id.is_none() {
280 wait.registered_by_tool_call_id = tool_call_id.clone();
281 }
282 }
283 wait.child_session_ids.sort();
284 wait.child_session_ids.dedup();
285 runtime_state.waiting_for_children = Some(wait);
286
287 write_runtime_state(&mut parent, &runtime_state);
288 parent.metadata.insert(
289 "runtime.suspend_reason".to_string(),
290 "waiting_for_children".to_string(),
291 );
292 parent.updated_at = Utc::now();
293
294 self.persistence
299 .save_runtime_only(&mut parent)
300 .await
301 .map_err(|error| {
302 ChildSessionError::Execution(format!("failed to save parent wait state: {error}"))
303 })?;
304 self.sessions_cache.insert(
305 parent.id.clone(),
306 Arc::new(parking_lot::RwLock::new(parent)),
307 );
308
309 Ok(())
310 }
311}
312
313fn map_index_entry_to_child_entry(entry: &SessionIndexEntry) -> ChildSessionEntry {
314 ChildSessionEntry {
315 child_session_id: entry.id.clone(),
316 title: entry.title.clone(),
317 pinned: entry.pinned,
318 message_count: entry.message_count,
319 updated_at: entry.updated_at.to_rfc3339(),
320 last_run_status: entry.last_run_status.clone(),
321 last_run_error: entry.last_run_error.clone(),
322 }
323}
324
325#[async_trait]
326impl SubagentResolutionPort for ChildSessionAdapter {
327 async fn resolve_subagent_model(
328 &self,
329 subagent_type: &str,
330 ) -> Option<bamboo_domain::ProviderModelRef> {
331 ChildSessionAdapter::resolve_subagent_model(self, subagent_type).await
332 }
333
334 async fn resolve_runtime_metadata(
335 &self,
336 subagent_type: &str,
337 ) -> std::collections::HashMap<String, String> {
338 ChildSessionAdapter::resolve_runtime_metadata(self, subagent_type).await
339 }
340
341 fn resolve_subagent_prompt(&self, subagent_type: &str) -> String {
342 ChildSessionAdapter::resolve_subagent_prompt(self, subagent_type)
343 }
344}
345
346#[async_trait]
347impl ChildSessionPort for ChildSessionAdapter {
348 async fn load_root_session(&self, root_session_id: &str) -> Result<Session, ChildSessionError> {
349 let Some(session) = self
350 .storage
351 .load_session(root_session_id)
352 .await
353 .map_err(|error| {
354 ChildSessionError::Execution(format!(
355 "failed to load session {root_session_id}: {error}"
356 ))
357 })?
358 else {
359 return Err(ChildSessionError::NotFound(root_session_id.to_string()));
360 };
361
362 if session.kind != SessionKind::Root {
363 return Err(ChildSessionError::NotRootSession(
364 root_session_id.to_string(),
365 ));
366 }
367
368 Ok(session)
369 }
370
371 async fn load_child_for_parent(
372 &self,
373 parent_session_id: &str,
374 child_session_id: &str,
375 ) -> Result<Session, ChildSessionError> {
376 let Some(child) = self
377 .storage
378 .load_session(child_session_id)
379 .await
380 .map_err(|error| {
381 ChildSessionError::Execution(format!(
382 "failed to load child session {child_session_id}: {error}"
383 ))
384 })?
385 else {
386 return Err(ChildSessionError::NotFound(child_session_id.to_string()));
387 };
388
389 if child.kind != SessionKind::Child {
390 return Err(ChildSessionError::NotChildSession(
391 child_session_id.to_string(),
392 ));
393 }
394
395 if child.parent_session_id.as_deref() != Some(parent_session_id) {
396 return Err(ChildSessionError::NotChildOfParent {
397 child_id: child_session_id.to_string(),
398 parent_id: parent_session_id.to_string(),
399 });
400 }
401
402 Ok(child)
403 }
404
405 async fn save_child_session(&self, child: &mut Session) -> Result<(), ChildSessionError> {
406 self.persistence
407 .merge_save_runtime(child)
408 .await
409 .map_err(|error| {
410 ChildSessionError::Execution(format!("failed to save child session: {error}"))
411 })?;
412
413 self.sessions_cache.insert(
414 child.id.clone(),
415 Arc::new(parking_lot::RwLock::new(child.clone())),
416 );
417
418 Ok(())
419 }
420
421 async fn is_child_running(&self, child_session_id: &str) -> bool {
422 let runners = self.agent_runners.read().await;
423 runners
424 .get(child_session_id)
425 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
426 }
427
428 async fn list_children(&self, parent_session_id: &str) -> Vec<ChildSessionEntry> {
429 self.session_store
430 .list_index_entries()
431 .await
432 .into_iter()
433 .filter(|entry| {
434 entry.kind == SessionKind::Child
435 && entry.parent_session_id.as_deref() == Some(parent_session_id)
436 })
437 .map(|entry| map_index_entry_to_child_entry(&entry))
438 .collect()
439 }
440
441 async fn find_resident_child(
442 &self,
443 root_session_id: &str,
444 resident_name: &str,
445 ) -> Option<String> {
446 let name = resident_name.trim();
447 if name.is_empty() {
448 return None;
449 }
450 let mut best: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
454 for entry in self.session_store.list_index_entries().await {
455 if entry.kind == SessionKind::Child
456 && entry.root_session_id == root_session_id
457 && entry.resident_name.as_deref() == Some(name)
458 {
459 match &best {
460 Some((_, ts)) if *ts >= entry.updated_at => {}
461 _ => best = Some((entry.id.clone(), entry.updated_at)),
462 }
463 }
464 }
465 best.map(|(id, _)| id)
466 }
467
468 async fn enqueue_child_run(
469 &self,
470 parent: &Session,
471 child: &Session,
472 ) -> Result<(), ChildSessionError> {
473 let model = if child.model.trim().is_empty() {
474 parent.model.clone()
475 } else {
476 child.model.clone()
477 };
478 if model.trim().is_empty() {
479 return Err(ChildSessionError::Execution(
480 "child model is empty and parent model is unavailable".to_string(),
481 ));
482 }
483
484 let disabled_tools = child
486 .subagent_type()
487 .map(|s| s.trim().to_string())
488 .filter(|s| !s.is_empty())
489 .and_then(|subagent_type| {
490 let profile = self.subagent_profiles.resolve(&subagent_type);
491 match &profile.tools {
492 bamboo_domain::subagent::ToolPolicy::Inherit => None,
493 policy => {
494 let names = bamboo_domain::subagent::disabled_tools_for_profile(
495 policy,
496 &self.tool_names,
497 );
498 if names.is_empty() {
499 None
500 } else {
501 Some(names)
502 }
503 }
504 }
505 });
506
507 self.scheduler
513 .enqueue(SpawnJob {
514 parent_session_id: parent.id.clone(),
515 child_session_id: child.id.clone(),
516 model,
517 disabled_tools,
518 })
519 .await
520 .map_err(ChildSessionError::Execution)?;
521
522 let parent_tx = get_or_create_event_sender(&self.session_event_senders, &parent.id).await;
523 let _ = parent_tx.send(AgentEvent::SubAgentStarted {
524 parent_session_id: parent.id.clone(),
525 child_session_id: child.id.clone(),
526 title: Some(child.title.clone()),
527 });
528
529 Ok(())
530 }
531
532 async fn cancel_child_run_and_wait(
533 &self,
534 child_session_id: &str,
535 ) -> Result<(), ChildSessionError> {
536 let cancelled = {
537 let mut runners = self.agent_runners.write().await;
538 if let Some(runner) = runners.get_mut(child_session_id) {
539 if matches!(runner.status, AgentStatus::Running) {
540 runner.cancel_token.cancel();
541 true
542 } else {
543 false
544 }
545 } else {
546 false
547 }
548 };
549
550 if !cancelled {
551 return Ok(());
552 }
553
554 let deadline = Instant::now() + Duration::from_secs(10);
555 loop {
556 let still_running = {
557 let runners = self.agent_runners.read().await;
558 runners
559 .get(child_session_id)
560 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
561 };
562 if !still_running {
563 return Ok(());
564 }
565 if Instant::now() >= deadline {
566 return Err(ChildSessionError::Execution(format!(
567 "timed out waiting for child session {child_session_id} to stop after cancellation"
568 )));
569 }
570 sleep(Duration::from_millis(50)).await;
571 }
572 }
573
574 async fn delete_child_session(
575 &self,
576 parent_session_id: &str,
577 child_id: &str,
578 ) -> Result<DeleteChildResult, ChildSessionError> {
579 let cancelled_running_child = {
580 let mut runners = self.agent_runners.write().await;
581 if let Some(runner) = runners.remove(child_id) {
582 runner.cancel_token.cancel();
583 true
584 } else {
585 false
586 }
587 };
588
589 let deleted = self
590 .storage
591 .delete_session(child_id)
592 .await
593 .map_err(|error| {
594 ChildSessionError::Execution(format!("failed to delete child session: {error}"))
595 })?;
596
597 self.sessions_cache.remove(child_id);
598 {
599 let mut senders = self.session_event_senders.write().await;
600 senders.remove(child_id);
601 if cancelled_running_child {
602 if let Some(parent_tx) = senders.get(parent_session_id) {
603 let _ = parent_tx.send(AgentEvent::SubAgentCompleted {
604 parent_session_id: parent_session_id.to_string(),
605 child_session_id: child_id.to_string(),
606 status: "cancelled".to_string(),
607 error: Some("Child session deleted while running".to_string()),
608 });
609 }
610 }
611 }
612
613 Ok(DeleteChildResult {
614 deleted,
615 cancelled_running_child,
616 })
617 }
618
619 async fn get_child_runner_info(&self, child_id: &str) -> Option<ChildRunnerInfo> {
620 let runners = self.agent_runners.read().await;
621 runners.get(child_id).map(|runner| ChildRunnerInfo {
622 started_at: Some(runner.started_at),
623 completed_at: runner.completed_at,
624 last_tool_name: runner.last_tool_name.clone(),
625 last_tool_phase: runner.last_tool_phase.clone(),
626 last_event_at: runner.last_event_at,
627 round_count: runner.round_count,
628 })
629 }
630
631 async fn register_parent_wait_for_child(
632 &self,
633 parent_session_id: &str,
634 child_session_id: &str,
635 tool_call_id: Option<&str>,
636 ) -> Result<(), ChildSessionError> {
637 ChildSessionAdapter::register_parent_wait_for_child(
638 self,
639 parent_session_id,
640 child_session_id,
641 tool_call_id,
642 )
643 .await
644 }
645
646 async fn register_parent_wait_for_children(
647 &self,
648 parent_session_id: &str,
649 child_session_ids: &[String],
650 policy: ChildWaitPolicy,
651 ) -> Result<usize, ChildSessionError> {
652 ChildSessionAdapter::register_parent_wait_for_children(
653 self,
654 parent_session_id,
655 child_session_ids,
656 policy,
657 )
658 .await
659 }
660
661 async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
662 ChildSessionAdapter::active_child_ids(self, parent_session_id).await
663 }
664
665 async fn ensure_child_indexed(&self, child_session_id: &str) {
666 let _ = self.session_store.get_index_entry(child_session_id).await;
667 }
668}