Skip to main content

bamboo_engine/sdk/
runner.rs

1//! Ergonomic child-session runner facade over the canonical spawn core.
2//!
3//! [`ChildRunner`] is a thin wrapper around the existing [`SpawnContext`] (it
4//! does NOT introduce a parallel `RuntimeDeps` god-struct). It builds a
5//! [`SpawnJob`] and delegates to [`crate::sdk::spawn::run_child_spawn`] — the
6//! single canonical spawn path.
7//!
8//! Sub-agents are plain full agents: there is no per-role tool trimming, so the
9//! job's `disabled_tools` is always `None` here (the unrelated global
10//! `config.disabled_tools` path is applied elsewhere, not via this runner).
11//!
12//! The assignment prompt and system prompt already live in the persisted child
13//! session (matching real spawn semantics: `ExecuteRequest.initial_message` is
14//! empty; the last user message in the child drives execution). `RunChildInput`
15//! therefore stays minimal.
16
17use bamboo_agent_core::AgentEvent;
18use tokio::sync::broadcast;
19
20use crate::runtime::execution::session_events::get_or_create_event_sender;
21use crate::runtime::execution::spawn::{SpawnContext, SpawnJob};
22
23/// Minimal input to run a child session.
24///
25/// The persisted child session already holds the system prompt + the pending user
26/// message, so only routing identifiers and the resolved model are required here.
27#[derive(Debug, Clone)]
28pub struct RunChildInput {
29    /// Child session id (already persisted with kind=child + a pending user msg).
30    pub child_session_id: String,
31    /// Parent session id whose event stream receives `SubAgent*` events.
32    pub parent_session_id: String,
33    /// Resolved model string for the child run.
34    pub model: String,
35}
36
37/// Ergonomic facade for child spawns.
38///
39/// Reuses [`SpawnContext`] (agent, tools, caches, router, completion handler) —
40/// the same dependency bundle the background scheduler uses.
41pub struct ChildRunner {
42    ctx: SpawnContext,
43}
44
45/// Construct a [`ChildRunner`] from an existing [`SpawnContext`].
46pub fn child_runner(ctx: SpawnContext) -> ChildRunner {
47    ChildRunner::new(ctx)
48}
49
50impl ChildRunner {
51    /// Create a runner over the given spawn context.
52    pub fn new(ctx: SpawnContext) -> Self {
53        Self { ctx }
54    }
55
56    /// Build a [`SpawnJob`] for the given input.
57    ///
58    /// Sub-agents are full agents with the full toolset, so `disabled_tools`
59    /// is always `None`.
60    pub(crate) fn build_job(&self, input: &RunChildInput) -> SpawnJob {
61        SpawnJob {
62            parent_session_id: input.parent_session_id.clone(),
63            child_session_id: input.child_session_id.clone(),
64            model: input.model.clone(),
65            disabled_tools: None,
66        }
67    }
68
69    async fn announce_started(&self, input: &RunChildInput) {
70        self.ctx
71            .replayable_event_publisher()
72            .publish(
73                &input.parent_session_id,
74                AgentEvent::SubAgentStarted {
75                    parent_session_id: input.parent_session_id.clone(),
76                    child_session_id: input.child_session_id.clone(),
77                    title: None,
78                },
79            )
80            .await;
81    }
82
83    /// Run a child session via the canonical spawn core.
84    ///
85    /// ANTI-FORK: constructs a [`SpawnJob`] and delegates to
86    /// [`crate::sdk::spawn::run_child_spawn`]; there is no inline execute/finalize.
87    pub async fn run_child(&self, input: RunChildInput) -> Result<(), String> {
88        let job = self.build_job(&input);
89        self.announce_started(&input).await;
90        crate::sdk::spawn::run_child_spawn(self.ctx.clone(), job).await
91    }
92
93    /// Run a child session and return a receiver of the child's
94    /// [`AgentEvent`] stream.
95    ///
96    /// The receiver is subscribed from the existing broadcast infra
97    /// (`ctx.session_event_senders`) *before* the spawn is started, so no events
98    /// are missed. This reuses the canonical broadcast channel — it does NOT
99    /// invent a parallel `RunOutcomeStream`/`status_rx` mpsc.
100    pub async fn run_child_stream(
101        &self,
102        input: RunChildInput,
103    ) -> Result<broadcast::Receiver<AgentEvent>, String> {
104        let child_tx =
105            get_or_create_event_sender(&self.ctx.session_event_senders, &input.child_session_id)
106                .await;
107        let rx = child_tx.subscribe();
108        let job = self.build_job(&input);
109        self.announce_started(&input).await;
110        crate::sdk::spawn::run_child_spawn(self.ctx.clone(), job).await?;
111        Ok(rx)
112    }
113}