Skip to main content

bamboo_engine/runtime/
runtime.rs

1//! Unified agent execution runtime.
2//!
3//! [`AgentRuntime`] holds shared resources assembled once at server startup,
4//! including the LLM provider and default tool executor.
5//! [`ExecuteRequest`] captures per-request parameters.  Together they replace
6//! the three duplicated `AgentLoopConfig` construction sites in the server
7//! layer (HTTP handler, spawn scheduler, schedule manager).
8
9use std::collections::BTreeSet;
10use std::sync::Arc;
11
12use tokio::sync::{mpsc, RwLock};
13use tokio_util::sync::CancellationToken;
14
15use crate::metrics::MetricsCollector;
16use crate::skills::SkillManager;
17use bamboo_agent_core::storage::{AttachmentReader, Storage};
18use bamboo_agent_core::tools::ToolExecutor;
19use bamboo_agent_core::{AgentEvent, Role, Session};
20use bamboo_domain::ReasoningEffort;
21use bamboo_infrastructure::config::PermissionMode;
22use bamboo_infrastructure::Config;
23use bamboo_infrastructure::LLMProvider;
24
25use crate::runtime::config::{AgentLoopConfig, ImageFallbackConfig, PromptMemoryFlags};
26use crate::runtime::hooks::HookRunner;
27use crate::runtime::managers::{
28    LifecycleManager, LlmManager, MemoryManager, PromptManager, ToolManager,
29};
30use crate::runtime::runner::run_agent_loop_with_config;
31
32// ---------------------------------------------------------------------------
33// AgentRuntime — shared resources (assembled once)
34// ---------------------------------------------------------------------------
35
36/// Shared runtime resources assembled once at server startup.
37///
38/// Each field is an `Arc` or `Clone`-cheap handle so that `AgentRuntime` itself
39/// is cheaply cloneable across tasks.
40#[derive(Clone)]
41pub struct AgentRuntime {
42    pub storage: Arc<dyn Storage>,
43    pub attachment_reader: Arc<dyn AttachmentReader>,
44    pub skill_manager: Arc<SkillManager>,
45    pub metrics_collector: MetricsCollector,
46    pub config: Arc<RwLock<Config>>,
47
48    /// Reloadable LLM provider handle (delegates to the latest provider).
49    pub provider: Arc<dyn LLMProvider>,
50
51    /// Default tool executor (root tools with full surface).
52    /// Call sites that need a reduced tool set (child / schedule) pass their
53    /// own via `ExecuteRequest::tools`.
54    pub default_tools: Arc<dyn ToolExecutor>,
55
56    // -- Optional manager overrides ----------------------------------------
57    /// When `Some`, uses a custom prompt manager instead of the default adapter.
58    pub prompt_manager: Option<Arc<dyn PromptManager>>,
59    /// When `Some`, uses a custom memory manager instead of the default adapter.
60    pub memory_manager: Option<Arc<dyn MemoryManager>>,
61    /// When `Some`, uses a custom tool manager instead of the default adapter.
62    pub tool_manager: Option<Arc<dyn ToolManager>>,
63    /// When `Some`, uses a custom LLM manager instead of the default adapter.
64    pub llm_manager: Option<Arc<dyn LlmManager>>,
65    /// When `Some`, uses a custom lifecycle manager instead of the default adapter.
66    pub lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
67    /// When `Some`, uses a custom hook runner instead of a no-op runner.
68    pub hook_runner: Option<HookRunner>,
69}
70
71// ---------------------------------------------------------------------------
72// AgentRuntimeBuilder
73// ---------------------------------------------------------------------------
74
75/// Builder for [`AgentRuntime`].
76///
77/// ```
78/// # use bamboo_engine::AgentRuntimeBuilder;
79/// // In real code all fields are provided by the server assembly.
80/// let rt = AgentRuntimeBuilder::new()
81///     // .storage(...)
82///     // .provider(...)
83///     .build();
84/// ```
85pub struct AgentRuntimeBuilder {
86    storage: Option<Arc<dyn Storage>>,
87    attachment_reader: Option<Arc<dyn AttachmentReader>>,
88    skill_manager: Option<Arc<SkillManager>>,
89    metrics_collector: Option<MetricsCollector>,
90    config: Option<Arc<RwLock<Config>>>,
91    provider: Option<Arc<dyn LLMProvider>>,
92    default_tools: Option<Arc<dyn ToolExecutor>>,
93    prompt_manager: Option<Arc<dyn PromptManager>>,
94    memory_manager: Option<Arc<dyn MemoryManager>>,
95    tool_manager: Option<Arc<dyn ToolManager>>,
96    llm_manager: Option<Arc<dyn LlmManager>>,
97    lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
98    hook_runner: Option<HookRunner>,
99}
100
101impl AgentRuntimeBuilder {
102    pub fn new() -> Self {
103        Self {
104            storage: None,
105            attachment_reader: None,
106            skill_manager: None,
107            metrics_collector: None,
108            config: None,
109            provider: None,
110            default_tools: None,
111            prompt_manager: None,
112            memory_manager: None,
113            tool_manager: None,
114            llm_manager: None,
115            lifecycle_manager: None,
116            hook_runner: None,
117        }
118    }
119
120    pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
121        self.storage = Some(v);
122        self
123    }
124
125    pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
126        self.attachment_reader = Some(v);
127        self
128    }
129
130    pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
131        self.skill_manager = Some(v);
132        self
133    }
134
135    pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
136        self.metrics_collector = Some(v);
137        self
138    }
139
140    pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
141        self.config = Some(v);
142        self
143    }
144
145    pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
146        self.provider = Some(v);
147        self
148    }
149
150    pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
151        self.default_tools = Some(v);
152        self
153    }
154
155    pub fn prompt_manager(mut self, v: Arc<dyn PromptManager>) -> Self {
156        self.prompt_manager = Some(v);
157        self
158    }
159
160    pub fn memory_manager(mut self, v: Arc<dyn MemoryManager>) -> Self {
161        self.memory_manager = Some(v);
162        self
163    }
164
165    pub fn tool_manager(mut self, v: Arc<dyn ToolManager>) -> Self {
166        self.tool_manager = Some(v);
167        self
168    }
169
170    pub fn llm_manager(mut self, v: Arc<dyn LlmManager>) -> Self {
171        self.llm_manager = Some(v);
172        self
173    }
174
175    pub fn lifecycle_manager(mut self, v: Arc<dyn LifecycleManager>) -> Self {
176        self.lifecycle_manager = Some(v);
177        self
178    }
179
180    pub fn hook_runner(mut self, v: HookRunner) -> Self {
181        self.hook_runner = Some(v);
182        self
183    }
184
185    pub fn build(self) -> Result<AgentRuntime, &'static str> {
186        Ok(AgentRuntime {
187            storage: self.storage.ok_or_else(|| format_missing("storage"))?,
188            attachment_reader: self
189                .attachment_reader
190                .ok_or_else(|| format_missing("attachment_reader"))?,
191            skill_manager: self
192                .skill_manager
193                .ok_or_else(|| format_missing("skill_manager"))?,
194            metrics_collector: self
195                .metrics_collector
196                .ok_or_else(|| format_missing("metrics_collector"))?,
197            config: self.config.ok_or_else(|| format_missing("config"))?,
198            provider: self.provider.ok_or_else(|| format_missing("provider"))?,
199            default_tools: self
200                .default_tools
201                .ok_or_else(|| format_missing("default_tools"))?,
202            prompt_manager: None,
203            memory_manager: None,
204            tool_manager: None,
205            llm_manager: None,
206            lifecycle_manager: None,
207            hook_runner: None,
208        })
209    }
210}
211
212fn format_missing(field: &str) -> &'static str {
213    // Static strings for the common fields keep the error type `&'static str`.
214    // This is good enough for a builder that only runs at startup.
215    match field {
216        "storage" => "AgentRuntimeBuilder: missing storage",
217        "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
218        "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
219        "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
220        "config" => "AgentRuntimeBuilder: missing config",
221        "provider" => "AgentRuntimeBuilder: missing provider",
222        "default_tools" => "AgentRuntimeBuilder: missing default_tools",
223        _ => "AgentRuntimeBuilder: missing required field",
224    }
225}
226
227impl Default for AgentRuntimeBuilder {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233// ---------------------------------------------------------------------------
234// ExecuteRequest — per-request parameters
235// ---------------------------------------------------------------------------
236
237/// Per-request parameters for agent execution.
238///
239/// Required fields (`initial_message`, `event_tx`, `cancel_token`) must always
240/// be provided.  The provider is taken from [`AgentRuntime::provider`]; tools
241/// default to [`AgentRuntime::default_tools`] when `None`.
242pub struct ExecuteRequest {
243    // -- Required ----------------------------------------------------------
244    pub initial_message: String,
245    pub event_tx: mpsc::Sender<AgentEvent>,
246    pub cancel_token: CancellationToken,
247
248    // -- Tool override -----------------------------------------------------
249    /// Override runtime's `default_tools`.  When `None`, uses the runtime's
250    /// default tool executor.
251    pub tools: Option<Arc<dyn ToolExecutor>>,
252    /// Override the LLM provider for this execution. When `None`, uses the
253    /// runtime's shared provider handle.
254    pub provider_override: Option<Arc<dyn LLMProvider>>,
255
256    // -- Optional overrides (None → config defaults) ----------------------
257    pub model: Option<String>,
258    pub provider_name: Option<String>,
259    /// When `None`, falls back to `Config::get_memory_background_model()`.
260    pub background_model: Option<String>,
261    /// Optional provider override for background/fast model calls.
262    /// When set, compression/summarization use this provider instead of the shared `llm`.
263    pub background_model_provider: Option<Arc<dyn LLMProvider>>,
264    pub reasoning_effort: Option<ReasoningEffort>,
265    /// When `None`, falls back to `Config::disabled_tool_names()`.
266    pub disabled_tools: Option<BTreeSet<String>>,
267    /// When `None`, falls back to `Config::disabled_skill_ids()`.
268    pub disabled_skill_ids: Option<BTreeSet<String>>,
269    pub selected_skill_ids: Option<Vec<String>>,
270    pub selected_skill_mode: Option<String>,
271    pub image_fallback: Option<ImageFallbackConfig>,
272}
273
274// ---------------------------------------------------------------------------
275// Helpers
276// ---------------------------------------------------------------------------
277
278/// Extract the system prompt from session messages.
279fn extract_system_prompt(session: &Session) -> Option<String> {
280    session
281        .messages
282        .iter()
283        .find(|m| matches!(m.role, Role::System))
284        .map(|m| m.content.clone())
285}
286
287// ---------------------------------------------------------------------------
288// Execution
289// ---------------------------------------------------------------------------
290
291impl AgentRuntime {
292    /// Execute the agent loop with the given request.
293    ///
294    /// Builds an [`AgentLoopConfig`] from the request parameters and shared
295    /// runtime resources, then delegates to [`run_agent_loop_with_config`].
296    pub async fn execute(
297        &self,
298        session: &mut Session,
299        req: ExecuteRequest,
300    ) -> crate::runtime::runner::Result<()> {
301        let system_prompt = extract_system_prompt(session);
302        let config = self.config.read().await;
303        let ExecuteRequest {
304            initial_message,
305            event_tx,
306            cancel_token,
307            tools,
308            provider_override,
309            model,
310            provider_name,
311            background_model,
312            background_model_provider,
313            reasoning_effort,
314            disabled_tools,
315            disabled_skill_ids,
316            selected_skill_ids,
317            selected_skill_mode,
318            image_fallback,
319        } = req;
320        let tools = tools.unwrap_or_else(|| self.default_tools.clone());
321        let llm = provider_override.unwrap_or_else(|| self.provider.clone());
322
323        let loop_config = AgentLoopConfig {
324            max_rounds: 200,
325            system_prompt,
326            disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
327            selected_skill_ids,
328            selected_skill_mode,
329            skill_manager: Some(self.skill_manager.clone()),
330            skip_initial_user_message: true,
331            storage: Some(self.storage.clone()),
332            attachment_reader: Some(self.attachment_reader.clone()),
333            metrics_collector: Some(self.metrics_collector.clone()),
334            model_name: model,
335            fast_model_name: config.get_fast_model(),
336            background_model_name: background_model
337                .or_else(|| config.get_memory_background_model()),
338            background_model_provider,
339            planning_model_name: config
340                .defaults
341                .as_ref()
342                .and_then(|d| d.planning.as_ref())
343                .map(|r| r.model.clone()),
344            search_model_name: config
345                .defaults
346                .as_ref()
347                .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
348                .map(|r| r.model.clone()),
349            provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
350            reasoning_effort,
351            disabled_tools: disabled_tools.unwrap_or_else(|| config.disabled_tool_names()),
352            image_fallback,
353            prompt_memory_flags: config
354                .memory
355                .as_ref()
356                .map(PromptMemoryFlags::from)
357                .unwrap_or_default(),
358            features_dynamic_model_routing: config.features.dynamic_model_routing,
359            permission_mode: session
360                .agent_runtime_state
361                .as_ref()
362                .and_then(|state| state.plan_mode.as_ref())
363                .map(|_| PermissionMode::Plan),
364            ..Default::default()
365        };
366
367        drop(config);
368
369        run_agent_loop_with_config(
370            session,
371            initial_message,
372            event_tx,
373            llm,
374            tools,
375            cancel_token,
376            loop_config,
377        )
378        .await
379    }
380}