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}