Skip to main content

bamboo_server/tools/
child_session_adapter.rs

1//! Shared adapter implementing `ChildSessionPort` for server-side child session tools.
2//!
3//! The unified `SubAgentTool` delegates to this adapter instead of
4//! duplicating `ChildSessionPort` implementations.
5
6use 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
29/// Server-side adapter that bridges domain `ChildSessionPort` to infrastructure.
30///
31/// Holds all shared state needed by `SubAgentTool`.
32/// Implements the full `ChildSessionPort` trait with real methods (no stubs).
33pub 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    /// Optional subagent model resolver: maps subagent_type → provider+model ref.
42    pub(crate) subagent_model_resolver: crate::tools::OptionalSubagentModelResolver,
43    /// Application config for resolving subagent routing and external agent profiles.
44    pub(crate) config: Arc<RwLock<Config>>,
45    /// Coalesces concurrent parent-wait registrations for the same parent that
46    /// arrive in one spawn round (the LLM emitting several `SubAgent.create`
47    /// calls at once → `join_all`) into a single parent persist. See
48    /// [`ChildSessionAdapter::register_parent_wait_for_child`].
49    pub(crate) parent_wait_slots: Arc<dashmap::DashMap<String, Arc<ParentWaitSlot>>>,
50}
51
52/// Per-parent coalescing slot for batched wait registration.
53///
54/// `flush_lock` is a barrier distinct from the persistence per-session lock
55/// (using the latter here would deadlock, since the flush itself takes it). The
56/// first registration to win the barrier drains `pending` and persists the whole
57/// batch once; concurrent registrations that find `pending` already drained were
58/// persisted by that holder before it released the barrier, so they return
59/// without an extra write.
60#[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
68/// Terminal child run statuses, as mirrored into the session index. A child not
69/// in one of these states is considered active (still pending/running).
70fn 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    /// Construct an adapter. Public so a self-orchestrating WORKER (Phase 6:
101    /// direct nested execution) can build its OWN child-session machinery
102    /// against its own store/scheduler — the struct fields are `pub(crate)`, so
103    /// out-of-crate callers (the worker binary) go through this constructor.
104    #[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            // Fresh per-adapter wait-coalescing map (the type is private to this
127            // crate, so out-of-crate callers can't supply it).
128            parent_wait_slots: Arc::new(dashmap::DashMap::new()),
129        }
130    }
131
132    /// Resolve the provider+model ref for a given subagent_type using the configured resolver.
133    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    /// Resolve runtime metadata (e.g. external agent routing) for a subagent_type.
144    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    /// Register a durable parent wait for an enqueued child session.
150    ///
151    /// This is intentionally idempotent: repeated registrations for the same
152    /// child merge into the existing wait set. The child runner owns timeout
153    /// and liveness; the parent wait timeout is a long lease for observability.
154    ///
155    /// Registrations are **coalesced** per parent: when several children are
156    /// spawned in one round (the LLM issuing multiple `SubAgent.create` calls
157    /// that `join_all` runs concurrently), the first call to win the per-parent
158    /// barrier drains all currently-pending registrations and persists the parent
159    /// once, instead of each child triggering its own load+write. Callers whose
160    /// child was drained-and-persisted by that holder return without an extra
161    /// write — and only after the holder's write committed, so durability holds.
162    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        // 1. Enqueue this registration.
175        slot.pending.lock().push((
176            child_session_id.to_string(),
177            tool_call_id.map(str::to_string),
178        ));
179
180        // 2. Barrier: serialize flushers for this parent.
181        let _flush_guard = slot.flush_lock.lock().await;
182
183        // 3. Drain everything pending for this parent (siblings that enqueued
184        //    while we waited for the barrier are picked up here too).
185        let batch: Vec<(String, Option<String>)> = {
186            let mut pending = slot.pending.lock();
187            pending.drain(..).collect()
188        };
189        if batch.is_empty() {
190            // A prior barrier holder already persisted our child before releasing
191            // the barrier we just acquired — nothing left to write.
192            return Ok(());
193        }
194
195        // 4. Persist the whole batch in a single parent write.
196        if let Err(error) = self
197            .flush_parent_waits(parent_session_id, &batch, ChildWaitPolicy::All)
198            .await
199        {
200            // Re-queue so nothing is silently lost; a retry or sibling picks it up.
201            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    /// Explicitly register a parent wait for an arbitrary set of children with a
211    /// chosen policy. Used by the `SubAgent.wait` action (wait on all active
212    /// children) and the end-of-turn safety net. A single parent write.
213    ///
214    /// Returns the number of children the wait now covers (0 means there was
215    /// nothing to wait on and no wait was registered).
216    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    /// The parent's currently-active (non-terminal) children, derived from the
235    /// session index (single source of truth).
236    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    /// Persist a batch of parent-wait registrations in one runtime-only save.
248    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        // The active/completed child sets are derived from the session index
268        // (single source of truth), so we no longer maintain a denormalized copy
269        // here. Only the durable wait state below is parent-owned.
270        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        // An explicit wait re-asserts the policy on any pre-existing wait state.
278        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        // Runtime-only save: registering a parent's wait mutates the
303        // control-plane (runtime_state + suspend metadata) but NEVER the message
304        // history. Writing just the sidecar keeps spawn O(1) in conversation
305        // length instead of rewriting the parent's full session.json per child.
306        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/// Lets a [`ChildSessionAdapter`] act as the engine's guardian-review spawner.
351///
352/// `Arc<ChildSessionAdapter>` therefore doubles as `Arc<dyn GuardianSpawner>`
353/// (wired onto `AppState`), so the terminal gate spawns the read-only reviewer
354/// through the same child-session machinery the `SubAgent` tool uses — no second
355/// spawn path. The reviewer is a real sub-agent: it fetches the diff and runs
356/// tests itself via its (read-only) toolset.
357#[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            // The coordinator branches on this subagent_type to recognize a
373            // guardian completion and parse its verdict.
374            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        // Scan the index for a child in this root tree tagged with the resident
499        // name. Prefer the most recently updated if (defensively) more than one
500        // exists. Index-backed: no session.json loads.
501        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        // Per-child tool denylist: persisted onto the child session by
533        // `create_child_action` (JSON in metadata). Most sub-agents are full
534        // agents and carry none; a read-only Guardian reviewer carries a
535        // denylist here so the worker trims its toolset. `SpawnJob` wants a
536        // `Vec<String>`, so collect the set.
537        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        // NOTE: enqueue only *runs* the child in the background. Registering the
545        // parent's wait (which suspends the parent) is now an explicit, separate
546        // step so the model can spawn several children without each one
547        // suspending it — see `register_parent_wait_for_child` /
548        // `register_parent_wait_for_children` and the `SubAgent.wait` action.
549        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}