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, GoldConfig, 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: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
291    /// When `None`, falls back to `Config::disabled_tool_names()`.
292    pub disabled_tools: Option<BTreeSet<String>>,
293    /// When `None`, falls back to `Config::disabled_skill_ids()`.
294    pub disabled_skill_ids: Option<BTreeSet<String>>,
295    pub selected_skill_ids: Option<Vec<String>>,
296    pub selected_skill_mode: Option<String>,
297    pub image_fallback: Option<ImageFallbackConfig>,
298    pub gold_config: Option<GoldConfig>,
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            gold_config,
355            app_data_dir,
356        } = req;
357        let tools = tools.unwrap_or_else(|| self.default_tools.clone());
358        let llm = provider_override.unwrap_or_else(|| self.provider.clone());
359
360        let loop_config = AgentLoopConfig {
361            max_rounds: 200,
362            system_prompt,
363            disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
364            selected_skill_ids,
365            selected_skill_mode,
366            skill_manager: Some(self.skill_manager.clone()),
367            skip_initial_user_message: true,
368            storage: Some(self.storage.clone()),
369            persistence: Some(self.persistence.clone()),
370            attachment_reader: Some(self.attachment_reader.clone()),
371            metrics_collector: Some(self.metrics_collector.clone()),
372            model_name: model,
373            fast_model_name: fast_model.or_else(|| config.get_fast_model()),
374            fast_model_provider,
375            background_model_name: background_model
376                .or_else(|| config.get_memory_background_model()),
377            planning_model_name: config
378                .defaults
379                .as_ref()
380                .and_then(|d| d.planning.as_ref())
381                .map(|r| r.model.clone()),
382            search_model_name: config
383                .defaults
384                .as_ref()
385                .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
386                .map(|r| r.model.clone()),
387            compression_instructions: None,
388            summarization_model_name: summarization_model
389                .or_else(|| config.get_task_summary_model()),
390            background_model_provider,
391            summarization_model_provider,
392            provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
393            provider_type,
394            reasoning_effort,
395            auxiliary_model_resolver,
396            disabled_tools: {
397                let mut merged = config.disabled_tool_names();
398                if let Some(dt) = disabled_tools {
399                    merged.extend(dt);
400                }
401                merged
402            },
403            image_fallback,
404            app_data_dir,
405            prompt_memory_flags: config
406                .memory
407                .as_ref()
408                .map(PromptMemoryFlags::from)
409                .unwrap_or_default(),
410            features_dynamic_model_routing: config.features.dynamic_model_routing,
411            permission_mode: session
412                .agent_runtime_state
413                .as_ref()
414                .and_then(|state| state.plan_mode.as_ref())
415                .map(|_| PermissionMode::Plan),
416            gold_config,
417            ..Default::default()
418        };
419
420        drop(config);
421
422        run_agent_loop_with_config(
423            session,
424            initial_message,
425            event_tx,
426            llm,
427            tools,
428            cancel_token,
429            loop_config,
430        )
431        .await
432    }
433}