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