Skip to main content

bamboo_engine/sdk/
runner.rs

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