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}