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