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) session_messenger: Option<Arc<bamboo_engine::SessionMessenger>>,
38 pub(crate) scheduler: Arc<SpawnScheduler>,
39 pub(crate) sessions_cache: bamboo_engine::SessionCache,
40 pub(crate) agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
41 pub(crate) session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
42 pub(crate) subagent_model_resolver: crate::tools::OptionalSubagentModelResolver,
44 pub(crate) config: Arc<RwLock<Config>>,
46 pub(crate) project_store: Option<Arc<bamboo_projects::ProjectStore>>,
50 pub(crate) workspace_resolver: bamboo_agent_core::workspace_state::WorkspaceResolver,
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 fn finish_child_save(
111 &self,
112 child: &Session,
113 saved: std::io::Result<()>,
114 ) -> Result<(), ChildSessionError> {
115 saved.map_err(|error| {
116 ChildSessionError::Execution(format!("failed to save child session: {error}"))
117 })?;
118 self.sessions_cache.insert(
119 child.id.clone(),
120 Arc::new(parking_lot::RwLock::new(child.clone())),
121 );
122 Ok(())
123 }
124
125 #[allow(clippy::too_many_arguments)]
130 pub fn new(
131 session_store: Arc<SessionStoreV2>,
132 storage: Arc<dyn Storage>,
133 persistence: Arc<LockedSessionStore>,
134 scheduler: Arc<SpawnScheduler>,
135 sessions_cache: bamboo_engine::SessionCache,
136 agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
137 session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
138 session_messenger: Option<Arc<bamboo_engine::SessionMessenger>>,
139 subagent_model_resolver: crate::tools::OptionalSubagentModelResolver,
140 config: Arc<RwLock<Config>>,
141 ) -> Self {
142 Self {
143 session_store,
144 storage,
145 persistence,
146 session_messenger,
147 scheduler,
148 sessions_cache,
149 agent_runners,
150 session_event_senders,
151 subagent_model_resolver,
152 config,
153 project_store: None,
154 workspace_resolver:
155 bamboo_agent_core::workspace_state::WorkspaceResolver::from_process_globals(),
156 parent_wait_slots: Arc::new(dashmap::DashMap::new()),
159 }
160 }
161
162 pub async fn resolve_subagent_model(
164 &self,
165 subagent_type: &str,
166 ) -> Option<bamboo_domain::ProviderModelRef> {
167 match &self.subagent_model_resolver {
168 Some(resolver) => resolver(subagent_type.to_string()).await,
169 None => None,
170 }
171 }
172
173 pub async fn resolve_runtime_metadata(&self, subagent_type: &str) -> HashMap<String, String> {
175 let config = self.config.read().await;
176 bamboo_engine::external_agents::config::resolve_runtime_metadata(&config, subagent_type)
177 }
178
179 pub async fn register_parent_wait_for_child(
193 &self,
194 parent_session_id: &str,
195 child_session_id: &str,
196 tool_call_id: Option<&str>,
197 ) -> Result<(), ChildSessionError> {
198 let slot = self
199 .parent_wait_slots
200 .entry(parent_session_id.to_string())
201 .or_default()
202 .clone();
203
204 slot.pending.lock().push((
206 child_session_id.to_string(),
207 tool_call_id.map(str::to_string),
208 ));
209
210 let _flush_guard = slot.flush_lock.lock().await;
212
213 let batch: Vec<(String, Option<String>)> = {
216 let mut pending = slot.pending.lock();
217 pending.drain(..).collect()
218 };
219 if batch.is_empty() {
220 return Ok(());
223 }
224
225 if let Err(error) = self
227 .flush_parent_waits(parent_session_id, &batch, ChildWaitPolicy::All)
228 .await
229 {
230 let mut pending = slot.pending.lock();
232 for item in batch {
233 pending.push(item);
234 }
235 return Err(error);
236 }
237
238 self.parent_wait_slots
252 .remove_if(parent_session_id, |_, slot| slot.pending.lock().is_empty());
253
254 Ok(())
255 }
256
257 pub async fn register_parent_wait_for_children(
264 &self,
265 parent_session_id: &str,
266 child_session_ids: &[String],
267 policy: ChildWaitPolicy,
268 ) -> Result<usize, ChildSessionError> {
269 if child_session_ids.is_empty() {
270 return Ok(0);
271 }
272 let batch: Vec<(String, Option<String>)> = child_session_ids
273 .iter()
274 .map(|id| (id.clone(), None))
275 .collect();
276 self.flush_parent_waits(parent_session_id, &batch, policy)
277 .await?;
278 Ok(batch.len())
279 }
280
281 pub async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
284 self.storage
285 .list_child_run_statuses(parent_session_id)
286 .await
287 .unwrap_or_default()
288 .into_iter()
289 .filter(|(_, status)| !status.as_deref().is_some_and(is_terminal_child_status))
290 .map(|(id, _)| id)
291 .collect()
292 }
293
294 pub async fn terminal_child_ids(
299 &self,
300 parent_session_id: &str,
301 candidates: &[String],
302 ) -> Vec<(String, String)> {
303 let statuses = self
304 .storage
305 .list_child_run_statuses(parent_session_id)
306 .await
307 .unwrap_or_default();
308 candidates
309 .iter()
310 .filter_map(|candidate| {
311 statuses.iter().find_map(|(id, status)| {
312 let status = status.as_deref()?;
313 (id == candidate && is_terminal_child_status(status))
314 .then(|| (candidate.clone(), status.to_string()))
315 })
316 })
317 .collect()
318 }
319
320 async fn flush_parent_waits(
322 &self,
323 parent_session_id: &str,
324 batch: &[(String, Option<String>)],
325 policy: ChildWaitPolicy,
326 ) -> Result<(), ChildSessionError> {
327 let Some(mut parent) =
328 self.storage
329 .load_session(parent_session_id)
330 .await
331 .map_err(|error| {
332 ChildSessionError::Execution(format!(
333 "failed to load parent session {parent_session_id}: {error}"
334 ))
335 })?
336 else {
337 return Err(ChildSessionError::NotFound(parent_session_id.to_string()));
338 };
339
340 let mut runtime_state = read_runtime_state(&parent);
344
345 let now = Utc::now();
346 let mut wait = runtime_state
347 .waiting_for_children
348 .take()
349 .unwrap_or_else(|| WaitingForChildrenState::for_children(Vec::new(), policy, now));
350 wait.wait_for = policy;
352 for (child_session_id, tool_call_id) in batch {
353 if !wait
354 .child_session_ids
355 .iter()
356 .any(|id| id == child_session_id)
357 {
358 wait.child_session_ids.push(child_session_id.clone());
359 }
360 if wait.registered_by_tool_call_id.is_none() {
361 wait.registered_by_tool_call_id = tool_call_id.clone();
362 }
363 }
364 wait.child_session_ids.sort();
365 wait.child_session_ids.dedup();
366 runtime_state.waiting_for_children = Some(wait);
367
368 write_runtime_state(&mut parent, &runtime_state);
369 parent.metadata.insert(
370 "runtime.suspend_reason".to_string(),
371 "waiting_for_children".to_string(),
372 );
373 parent.updated_at = Utc::now();
374
375 self.persistence
380 .save_runtime_only(&mut parent)
381 .await
382 .map_err(|error| {
383 ChildSessionError::Execution(format!("failed to save parent wait state: {error}"))
384 })?;
385 self.sessions_cache.insert(
386 parent.id.clone(),
387 Arc::new(parking_lot::RwLock::new(parent)),
388 );
389
390 Ok(())
391 }
392}
393
394fn map_index_entry_to_child_entry(entry: &SessionIndexEntry) -> ChildSessionEntry {
395 ChildSessionEntry {
396 child_session_id: entry.id.clone(),
397 title: entry.title.clone(),
398 pinned: entry.pinned,
399 message_count: entry.message_count,
400 updated_at: entry.updated_at.to_rfc3339(),
401 last_run_status: entry.last_run_status.clone(),
402 last_run_error: entry.last_run_error.clone(),
403 }
404}
405
406#[async_trait]
407impl SubagentResolutionPort for ChildSessionAdapter {
408 async fn resolve_subagent_model(
409 &self,
410 subagent_type: &str,
411 ) -> Option<bamboo_domain::ProviderModelRef> {
412 ChildSessionAdapter::resolve_subagent_model(self, subagent_type).await
413 }
414
415 async fn resolve_runtime_metadata(
416 &self,
417 subagent_type: &str,
418 ) -> std::collections::HashMap<String, String> {
419 ChildSessionAdapter::resolve_runtime_metadata(self, subagent_type).await
420 }
421}
422
423#[async_trait]
431impl bamboo_engine::GuardianSpawner for ChildSessionAdapter {
432 async fn spawn_guardian_review(
433 &self,
434 parent_session: &Session,
435 review_prompt: String,
436 model: String,
437 disabled_tools: Option<std::collections::BTreeSet<String>>,
438 ) -> Result<String, String> {
439 let persisted_parent_workspace = parent_session.workspace_path_meta();
440 let parent_workspace_is_project_default = parent_session
441 .metadata
442 .get(bamboo_engine::project_context::WORKSPACE_SOURCE_METADATA_KEY)
443 .map(String::as_str)
444 == Some(bamboo_engine::project_context::WorkspaceSource::ProjectDefault.as_str());
445 let workspace_source = if parent_workspace_is_project_default
446 || (persisted_parent_workspace.is_none()
447 && matches!(
448 bamboo_engine::project_context::ProjectContextResolver::session_project_identity(
449 parent_session
450 ),
451 bamboo_engine::project_context::SessionProjectIdentity::Assigned(_)
452 ))
453 {
454 bamboo_engine::project_context::WorkspaceSource::ProjectDefault
455 } else {
456 match parent_session
457 .metadata
458 .get(bamboo_engine::project_context::WORKSPACE_SOURCE_METADATA_KEY)
459 .map(String::as_str)
460 {
461 Some("project_default") => {
462 bamboo_engine::project_context::WorkspaceSource::ProjectDefault
463 }
464 _ => bamboo_engine::project_context::WorkspaceSource::Session,
465 }
466 };
467 let input = bamboo_engine::session_app::child_session::CreateChildInput {
468 parent_session: parent_session.clone(),
469 child_id: format!("guardian-{}", uuid::Uuid::new_v4()),
470 title: "Guardian review".to_string(),
471 responsibility: "Adversarially verify the parent agent's completed work.".to_string(),
472 assignment_prompt: review_prompt,
473 subagent_type: "guardian".to_string(),
476 workspace: if parent_workspace_is_project_default {
477 String::new()
478 } else {
479 persisted_parent_workspace.unwrap_or_default()
480 },
481 workspace_source,
482 model_override: Some(model),
483 model_ref_override: None,
484 runtime_metadata: HashMap::new(),
485 auto_run: true,
486 reasoning_effort: None,
487 lifecycle: None,
488 resident_name: None,
489 resident_context: None,
490 disabled_tools,
491 context_fork: None,
492 };
493 bamboo_engine::session_app::child_session::create_child_action(self, input)
494 .await
495 .map(|result| result.child_session_id)
496 .map_err(|error| error.to_string())
497 }
498}
499
500#[async_trait]
501impl ChildSessionPort for ChildSessionAdapter {
502 fn publish_child_workspace(
503 &self,
504 session_id: &str,
505 workspace: std::path::PathBuf,
506 source: &str,
507 ) -> std::path::PathBuf {
508 self.workspace_resolver
509 .publish_resolved_workspace(session_id, workspace, source)
510 }
511
512 async fn validate_child_workspace(
513 &self,
514 project_id: Option<&bamboo_domain::ProjectId>,
515 requested_workspace: &str,
516 ) -> Result<String, ChildSessionError> {
517 let Some(store) = self.project_store.as_deref() else {
518 if requested_workspace.trim().is_empty() {
519 return Err(ChildSessionError::InvalidArguments(
520 "child workspace must be a non-empty path".to_string(),
521 ));
522 }
523 let requested = std::path::PathBuf::from(requested_workspace);
524 if requested.exists() && !requested.is_dir() {
525 return Err(ChildSessionError::InvalidArguments(format!(
526 "child workspace is not a directory: {requested_workspace}"
527 )));
528 }
529 let canonical = requested.canonicalize().unwrap_or(requested);
530 let final_workspace =
531 bamboo_agent_core::workspace_state::resolve_workspace_path(canonical);
532 return Ok(bamboo_config::paths::path_to_display_string(
533 &final_workspace,
534 ));
535 };
536 let final_workspace = crate::project_context::validate_workspace_assignment_with_resolver(
537 store,
538 project_id,
539 Some(requested_workspace),
540 &self.workspace_resolver,
541 )
542 .map_err(|error| ChildSessionError::InvalidArguments(error.to_string()))?
543 .ok_or_else(|| {
544 ChildSessionError::InvalidArguments(
545 "child workspace must be a non-empty path".to_string(),
546 )
547 })?;
548 Ok(bamboo_config::paths::path_to_display_string(
549 &final_workspace,
550 ))
551 }
552
553 async fn load_root_session(&self, root_session_id: &str) -> Result<Session, ChildSessionError> {
554 let Some(session) = self
555 .storage
556 .load_session(root_session_id)
557 .await
558 .map_err(|error| {
559 ChildSessionError::Execution(format!(
560 "failed to load session {root_session_id}: {error}"
561 ))
562 })?
563 else {
564 return Err(ChildSessionError::NotFound(root_session_id.to_string()));
565 };
566
567 if session.kind != SessionKind::Root {
568 return Err(ChildSessionError::NotRootSession(
569 root_session_id.to_string(),
570 ));
571 }
572
573 Ok(session)
574 }
575
576 async fn load_child_for_parent(
577 &self,
578 parent_session_id: &str,
579 child_session_id: &str,
580 ) -> Result<Session, ChildSessionError> {
581 let Some(child) = self
582 .storage
583 .load_session(child_session_id)
584 .await
585 .map_err(|error| {
586 ChildSessionError::Execution(format!(
587 "failed to load child session {child_session_id}: {error}"
588 ))
589 })?
590 else {
591 return Err(ChildSessionError::NotFound(child_session_id.to_string()));
592 };
593
594 if child.kind != SessionKind::Child {
595 return Err(ChildSessionError::NotChildSession(
596 child_session_id.to_string(),
597 ));
598 }
599
600 if child.parent_session_id.as_deref() != Some(parent_session_id) {
601 return Err(ChildSessionError::NotChildOfParent {
602 child_id: child_session_id.to_string(),
603 parent_id: parent_session_id.to_string(),
604 });
605 }
606
607 Ok(child)
608 }
609
610 async fn save_child_session(&self, child: &mut Session) -> Result<(), ChildSessionError> {
611 let saved = self.persistence.merge_save_runtime(child).await;
615 self.finish_child_save(child, saved)
616 }
617
618 async fn save_child_session_authoritative_flags(
619 &self,
620 child: &mut Session,
621 ) -> Result<(), ChildSessionError> {
622 let saved = self
626 .persistence
627 .save_runtime_authoritative_flags(child)
628 .await;
629 self.finish_child_save(child, saved)
630 }
631
632 async fn save_resident_reuse_state(
633 &self,
634 child: &mut Session,
635 workspace: &str,
636 workspace_source: bamboo_engine::project_context::WorkspaceSource,
637 permission_audit: bamboo_domain::PermissionAuditSeed,
638 no_human_approver: bool,
639 ) -> Result<(), ChildSessionError> {
640 let child_id = child.id.clone();
641 let workspace_value = workspace.to_string();
642 let source_value = workspace_source.as_str().to_string();
643 let saved = self
644 .persistence
645 .update_authoritative_permission_posture_and_publish(
646 &child_id,
647 &permission_audit,
648 |latest| {
649 latest.workspace = Some(workspace_value.clone());
650 latest.set_workspace_path_meta(&workspace_value);
651 latest.metadata.insert(
652 bamboo_engine::project_context::WORKSPACE_SOURCE_METADATA_KEY.to_string(),
653 source_value,
654 );
655 latest
656 .agent_runtime_state
657 .get_or_insert_with(bamboo_domain::AgentRuntimeState::default)
658 .no_human_approver = no_human_approver;
659 },
660 |latest| {
661 self.sessions_cache.insert(
662 latest.id.clone(),
663 Arc::new(parking_lot::RwLock::new(latest.clone())),
664 );
665 },
666 )
667 .await
668 .map_err(|error| {
669 ChildSessionError::Execution(format!(
670 "failed to atomically re-seed resident child: {error}"
671 ))
672 })?
673 .ok_or_else(|| ChildSessionError::NotFound(child_id.clone()))?;
674 *child = saved;
675 self.publish_child_workspace(
676 &child.id,
677 std::path::PathBuf::from(workspace),
678 workspace_source.as_str(),
679 );
680 Ok(())
681 }
682
683 async fn send_session_message(
684 &self,
685 source_session_id: &str,
686 target_session_id: &str,
687 message: &str,
688 idempotency_key: Option<&str>,
689 ) -> Result<
690 bamboo_engine::session_app::child_session::ChildSessionMessageDelivery,
691 ChildSessionError,
692 > {
693 let messenger = self.session_messenger.as_ref().ok_or_else(|| {
694 ChildSessionError::Execution(
695 "logical SessionMessenger is not configured for this runtime".to_string(),
696 )
697 })?;
698 let id = idempotency_key.map_or_else(bamboo_domain::SessionMessageId::new, |key| {
699 bamboo_domain::SessionMessageId::stable(
700 "subagent_send_message",
701 &serde_json::json!({
702 "source_session_id": source_session_id,
703 "target_session_id": target_session_id,
704 "tool_call_id": key,
705 }),
706 )
707 });
708 let envelope = bamboo_domain::SessionMessageEnvelope {
709 id,
710 source: bamboo_domain::SessionMessageSource::Session {
711 session_id: source_session_id.to_string(),
712 },
713 target_session_id: target_session_id.to_string(),
714 kind: bamboo_domain::SessionMessageKind::PeerMessage,
715 body: bamboo_domain::SessionMessageBody::Content(
716 bamboo_domain::SessionMessageContent::text(message),
717 ),
718 created_at: chrono::Utc::now(),
719 thread_id: None,
720 in_reply_to: None,
721 attempt: None,
722 correlation_id: None,
723 };
724 match messenger.send(envelope).await {
725 Ok(receipt) => Ok(
726 bamboo_engine::session_app::child_session::ChildSessionMessageDelivery::Activated(
727 receipt,
728 ),
729 ),
730 Err(bamboo_engine::SessionMessengerError::Activation {
731 receipt, source, ..
732 }) => Ok(
733 bamboo_engine::session_app::child_session::ChildSessionMessageDelivery::ActivationPending {
734 delivery: receipt,
735 error: source.to_string(),
736 },
737 ),
738 Err(error) => Err(ChildSessionError::Execution(error.to_string())),
739 }
740 }
741
742 async fn is_child_running(&self, child_session_id: &str) -> bool {
743 let runners = self.agent_runners.read().await;
744 runners
745 .get(child_session_id)
746 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
747 }
748
749 async fn list_children(&self, parent_session_id: &str) -> Vec<ChildSessionEntry> {
750 self.session_store
751 .list_index_entries()
752 .await
753 .into_iter()
754 .filter(|entry| {
755 entry.kind == SessionKind::Child
756 && entry.parent_session_id.as_deref() == Some(parent_session_id)
757 })
758 .map(|entry| map_index_entry_to_child_entry(&entry))
759 .collect()
760 }
761
762 async fn find_resident_child(
763 &self,
764 root_session_id: &str,
765 resident_name: &str,
766 ) -> Option<String> {
767 let name = resident_name.trim();
768 if name.is_empty() {
769 return None;
770 }
771 let mut best: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
775 for entry in self.session_store.list_index_entries().await {
776 if entry.kind == SessionKind::Child
777 && entry.root_session_id == root_session_id
778 && entry.resident_name.as_deref() == Some(name)
779 {
780 match &best {
781 Some((_, ts)) if *ts >= entry.updated_at => {}
782 _ => best = Some((entry.id.clone(), entry.updated_at)),
783 }
784 }
785 }
786 best.map(|(id, _)| id)
787 }
788
789 async fn enqueue_child_run(
790 &self,
791 parent: &Session,
792 child: &Session,
793 ) -> Result<(), ChildSessionError> {
794 let model = if child.model.trim().is_empty() {
795 parent.model.clone()
796 } else {
797 child.model.clone()
798 };
799 if model.trim().is_empty() {
800 return Err(ChildSessionError::Execution(
801 "child model is empty and parent model is unavailable".to_string(),
802 ));
803 }
804
805 let disabled_tools = child
811 .metadata
812 .get("disabled_tools")
813 .and_then(|raw| serde_json::from_str::<std::collections::BTreeSet<String>>(raw).ok())
814 .filter(|set| !set.is_empty())
815 .map(|set| set.into_iter().collect::<Vec<String>>());
816
817 self.scheduler
823 .enqueue(SpawnJob {
824 parent_session_id: parent.id.clone(),
825 child_session_id: child.id.clone(),
826 model,
827 disabled_tools,
828 })
829 .await
830 .map_err(ChildSessionError::Execution)?;
831
832 let parent_tx = get_or_create_event_sender(&self.session_event_senders, &parent.id).await;
833 let _ = parent_tx.send(AgentEvent::SubAgentStarted {
834 parent_session_id: parent.id.clone(),
835 child_session_id: child.id.clone(),
836 title: Some(child.title.clone()),
837 });
838
839 Ok(())
840 }
841
842 async fn cancel_child_run_and_wait(
843 &self,
844 child_session_id: &str,
845 ) -> Result<(), ChildSessionError> {
846 let cancelled = {
847 let mut runners = self.agent_runners.write().await;
848 if let Some(runner) = runners.get_mut(child_session_id) {
849 if matches!(runner.status, AgentStatus::Running) {
850 runner.cancel_token.cancel();
851 true
852 } else {
853 false
854 }
855 } else {
856 false
857 }
858 };
859
860 if !cancelled {
861 return Ok(());
862 }
863
864 let deadline = Instant::now() + Duration::from_secs(10);
865 loop {
866 let still_running = {
867 let runners = self.agent_runners.read().await;
868 runners
869 .get(child_session_id)
870 .is_some_and(|runner| matches!(runner.status, AgentStatus::Running))
871 };
872 if !still_running {
873 return Ok(());
874 }
875 if Instant::now() >= deadline {
876 return Err(ChildSessionError::Execution(format!(
877 "timed out waiting for child session {child_session_id} to stop after cancellation"
878 )));
879 }
880 sleep(Duration::from_millis(50)).await;
881 }
882 }
883
884 async fn delete_child_session(
885 &self,
886 parent_session_id: &str,
887 child_id: &str,
888 ) -> Result<DeleteChildResult, ChildSessionError> {
889 let cancelled_running_child = {
890 let mut runners = self.agent_runners.write().await;
891 if let Some(runner) = runners.remove(child_id) {
892 runner.cancel_token.cancel();
893 true
894 } else {
895 false
896 }
897 };
898
899 let deleted = self
900 .storage
901 .delete_session(child_id)
902 .await
903 .map_err(|error| {
904 ChildSessionError::Execution(format!("failed to delete child session: {error}"))
905 })?;
906
907 self.sessions_cache.remove(child_id);
908 {
909 let mut senders = self.session_event_senders.write().await;
910 senders.remove(child_id);
911 if cancelled_running_child {
912 if let Some(parent_tx) = senders.get(parent_session_id) {
913 let _ = parent_tx.send(AgentEvent::SubAgentCompleted {
914 parent_session_id: parent_session_id.to_string(),
915 child_session_id: child_id.to_string(),
916 status: "cancelled".to_string(),
917 error: Some("Child session deleted while running".to_string()),
918 });
919 }
920 }
921 }
922
923 Ok(DeleteChildResult {
924 deleted,
925 cancelled_running_child,
926 })
927 }
928
929 async fn get_child_runner_info(&self, child_id: &str) -> Option<ChildRunnerInfo> {
930 let runners = self.agent_runners.read().await;
931 runners.get(child_id).map(|runner| ChildRunnerInfo {
932 started_at: Some(runner.started_at),
933 completed_at: runner.completed_at,
934 last_tool_name: runner.last_tool_name.clone(),
935 last_tool_phase: runner.last_tool_phase.clone(),
936 last_event_at: runner.last_event_at,
937 round_count: runner.round_count,
938 })
939 }
940
941 async fn register_parent_wait_for_child(
942 &self,
943 parent_session_id: &str,
944 child_session_id: &str,
945 tool_call_id: Option<&str>,
946 ) -> Result<(), ChildSessionError> {
947 ChildSessionAdapter::register_parent_wait_for_child(
948 self,
949 parent_session_id,
950 child_session_id,
951 tool_call_id,
952 )
953 .await
954 }
955
956 async fn register_parent_wait_for_children(
957 &self,
958 parent_session_id: &str,
959 child_session_ids: &[String],
960 policy: ChildWaitPolicy,
961 ) -> Result<usize, ChildSessionError> {
962 ChildSessionAdapter::register_parent_wait_for_children(
963 self,
964 parent_session_id,
965 child_session_ids,
966 policy,
967 )
968 .await
969 }
970
971 async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String> {
972 ChildSessionAdapter::active_child_ids(self, parent_session_id).await
973 }
974
975 async fn terminal_child_ids(
976 &self,
977 parent_session_id: &str,
978 candidates: &[String],
979 ) -> Vec<(String, String)> {
980 ChildSessionAdapter::terminal_child_ids(self, parent_session_id, candidates).await
981 }
982
983 async fn ensure_child_indexed(&self, child_session_id: &str) {
984 let _ = self.session_store.get_index_entry(child_session_id).await;
985 }
986}