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 bamboo_agent_core::storage::{AttachmentReader, Storage};
16use bamboo_agent_core::tools::ToolExecutor;
17use bamboo_agent_core::{AgentEvent, Role, Session};
18use bamboo_config::PermissionMode;
19use bamboo_domain::ReasoningEffort;
20use bamboo_llm::Config;
21use bamboo_llm::LLMProvider;
22use bamboo_metrics::MetricsCollector;
23use bamboo_skills::SkillManager;
24
25use crate::runtime::config::{
26    AgentLoopConfig, AuxiliaryModelConfig, BashResumeHook, GoldConfig, GuardianConfig,
27    GuardianSpawner, ImageFallbackConfig, PromptMemoryFlags,
28};
29use crate::runtime::hooks::HookRunner;
30use crate::runtime::managers::{
31    LifecycleManager, LlmManager, MemoryManager, PromptManager, ToolManager,
32};
33use crate::runtime::model_roster::{ModelRoster, RoleModel};
34use crate::runtime::runner::run_agent_loop_with_config;
35use bamboo_domain::RuntimeSessionPersistence;
36
37// ---------------------------------------------------------------------------
38// AgentRuntime — shared resources (assembled once)
39// ---------------------------------------------------------------------------
40
41/// Shared runtime resources assembled once at server startup.
42///
43/// Each field is an `Arc` or `Clone`-cheap handle so that `AgentRuntime` itself
44/// is cheaply cloneable across tasks.
45#[derive(Clone)]
46pub struct AgentRuntime {
47    pub storage: Arc<dyn Storage>,
48    pub persistence: Arc<dyn RuntimeSessionPersistence>,
49    pub attachment_reader: Arc<dyn AttachmentReader>,
50    pub skill_manager: Arc<SkillManager>,
51    pub metrics_collector: MetricsCollector,
52    pub config: Arc<RwLock<Config>>,
53
54    /// Reloadable LLM provider handle (delegates to the latest provider).
55    pub provider: Arc<dyn LLMProvider>,
56
57    /// Default tool executor (root tools with full surface).
58    /// Call sites that need a reduced tool set (child / schedule) pass their
59    /// own via `ExecuteRequest::tools`.
60    pub default_tools: Arc<dyn ToolExecutor>,
61
62    // -- Optional manager overrides ----------------------------------------
63    /// When `Some`, uses a custom prompt manager instead of the default adapter.
64    pub prompt_manager: Option<Arc<dyn PromptManager>>,
65    /// When `Some`, uses a custom memory manager instead of the default adapter.
66    pub memory_manager: Option<Arc<dyn MemoryManager>>,
67    /// When `Some`, uses a custom tool manager instead of the default adapter.
68    pub tool_manager: Option<Arc<dyn ToolManager>>,
69    /// When `Some`, uses a custom LLM manager instead of the default adapter.
70    pub llm_manager: Option<Arc<dyn LlmManager>>,
71    /// When `Some`, uses a custom lifecycle manager instead of the default adapter.
72    pub lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
73    /// When `Some`, uses a custom hook runner instead of a no-op runner.
74    pub hook_runner: Option<HookRunner>,
75}
76
77// ---------------------------------------------------------------------------
78// AgentRuntimeBuilder
79// ---------------------------------------------------------------------------
80
81/// Builder for [`AgentRuntime`].
82///
83/// ```
84/// # use bamboo_engine::AgentRuntimeBuilder;
85/// // In real code all fields are provided by the server assembly.
86/// let rt = AgentRuntimeBuilder::new()
87///     // .storage(...)
88///     // .provider(...)
89///     .build();
90/// ```
91pub struct AgentRuntimeBuilder {
92    storage: Option<Arc<dyn Storage>>,
93    persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
94    attachment_reader: Option<Arc<dyn AttachmentReader>>,
95    skill_manager: Option<Arc<SkillManager>>,
96    metrics_collector: Option<MetricsCollector>,
97    config: Option<Arc<RwLock<Config>>>,
98    provider: Option<Arc<dyn LLMProvider>>,
99    default_tools: Option<Arc<dyn ToolExecutor>>,
100    prompt_manager: Option<Arc<dyn PromptManager>>,
101    memory_manager: Option<Arc<dyn MemoryManager>>,
102    tool_manager: Option<Arc<dyn ToolManager>>,
103    llm_manager: Option<Arc<dyn LlmManager>>,
104    lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
105    hook_runner: Option<HookRunner>,
106}
107
108impl AgentRuntimeBuilder {
109    pub fn new() -> Self {
110        Self {
111            storage: None,
112            persistence: None,
113            attachment_reader: None,
114            skill_manager: None,
115            metrics_collector: None,
116            config: None,
117            provider: None,
118            default_tools: None,
119            prompt_manager: None,
120            memory_manager: None,
121            tool_manager: None,
122            llm_manager: None,
123            lifecycle_manager: None,
124            hook_runner: None,
125        }
126    }
127
128    pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
129        self.storage = Some(v);
130        self
131    }
132
133    pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
134        self.persistence = Some(v);
135        self
136    }
137
138    pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
139        self.attachment_reader = Some(v);
140        self
141    }
142
143    pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
144        self.skill_manager = Some(v);
145        self
146    }
147
148    pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
149        self.metrics_collector = Some(v);
150        self
151    }
152
153    pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
154        self.config = Some(v);
155        self
156    }
157
158    pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
159        self.provider = Some(v);
160        self
161    }
162
163    pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
164        self.default_tools = Some(v);
165        self
166    }
167
168    pub fn prompt_manager(mut self, v: Arc<dyn PromptManager>) -> Self {
169        self.prompt_manager = Some(v);
170        self
171    }
172
173    pub fn memory_manager(mut self, v: Arc<dyn MemoryManager>) -> Self {
174        self.memory_manager = Some(v);
175        self
176    }
177
178    pub fn tool_manager(mut self, v: Arc<dyn ToolManager>) -> Self {
179        self.tool_manager = Some(v);
180        self
181    }
182
183    pub fn llm_manager(mut self, v: Arc<dyn LlmManager>) -> Self {
184        self.llm_manager = Some(v);
185        self
186    }
187
188    pub fn lifecycle_manager(mut self, v: Arc<dyn LifecycleManager>) -> Self {
189        self.lifecycle_manager = Some(v);
190        self
191    }
192
193    pub fn hook_runner(mut self, v: HookRunner) -> Self {
194        self.hook_runner = Some(v);
195        self
196    }
197
198    pub fn build(self) -> Result<AgentRuntime, &'static str> {
199        Ok(AgentRuntime {
200            storage: self.storage.ok_or_else(|| format_missing("storage"))?,
201            persistence: self
202                .persistence
203                .ok_or_else(|| format_missing("persistence"))?,
204            attachment_reader: self
205                .attachment_reader
206                .ok_or_else(|| format_missing("attachment_reader"))?,
207            skill_manager: self
208                .skill_manager
209                .ok_or_else(|| format_missing("skill_manager"))?,
210            metrics_collector: self
211                .metrics_collector
212                .ok_or_else(|| format_missing("metrics_collector"))?,
213            config: self.config.ok_or_else(|| format_missing("config"))?,
214            provider: self.provider.ok_or_else(|| format_missing("provider"))?,
215            default_tools: self
216                .default_tools
217                .ok_or_else(|| format_missing("default_tools"))?,
218            prompt_manager: None,
219            memory_manager: None,
220            tool_manager: None,
221            llm_manager: None,
222            lifecycle_manager: None,
223            hook_runner: None,
224        })
225    }
226}
227
228fn format_missing(field: &str) -> &'static str {
229    // Static strings for the common fields keep the error type `&'static str`.
230    // This is good enough for a builder that only runs at startup.
231    match field {
232        "storage" => "AgentRuntimeBuilder: missing storage",
233        "persistence" => "AgentRuntimeBuilder: missing persistence",
234        "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
235        "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
236        "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
237        "config" => "AgentRuntimeBuilder: missing config",
238        "provider" => "AgentRuntimeBuilder: missing provider",
239        "default_tools" => "AgentRuntimeBuilder: missing default_tools",
240        _ => "AgentRuntimeBuilder: missing required field",
241    }
242}
243
244impl Default for AgentRuntimeBuilder {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250// ---------------------------------------------------------------------------
251// ExecuteRequest — per-request parameters
252// ---------------------------------------------------------------------------
253
254/// Per-request parameters for agent execution.
255///
256/// Required fields (`initial_message`, `event_tx`, `cancel_token`) must always
257/// be provided.  The provider is taken from [`AgentRuntime::provider`]; tools
258/// default to [`AgentRuntime::default_tools`] when `None`.
259pub struct ExecuteRequest {
260    // -- Required ----------------------------------------------------------
261    pub initial_message: String,
262    pub event_tx: mpsc::Sender<AgentEvent>,
263    pub cancel_token: CancellationToken,
264
265    // -- Tool override -----------------------------------------------------
266    /// Override runtime's `default_tools`.  When `None`, uses the runtime's
267    /// default tool executor.
268    pub tools: Option<Arc<dyn ToolExecutor>>,
269    /// Override the LLM provider for this execution. When `None`, uses the
270    /// runtime's shared provider handle.
271    pub provider_override: Option<Arc<dyn LLMProvider>>,
272
273    // -- Model selection (None roles → config defaults) -------------------
274    /// Cohesive primary + auxiliary model/provider selection. Replaces the old
275    /// `model` / `provider_name` / `provider_type` / `fast_model(+provider)` /
276    /// `background_model(+provider)` / `summarization_model(+provider)` clump.
277    /// Per-role `None` preserves the same `None → Config::get_*` fallbacks
278    /// applied during [`AgentRuntime::execute`].
279    pub model_roster: ModelRoster,
280    pub reasoning_effort: Option<ReasoningEffort>,
281    /// Optional per-round resolver for auxiliary model settings that should be
282    /// re-read from live global config between rounds.
283    pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
284    /// When `None`, falls back to `Config::disabled_tool_names()`.
285    pub disabled_tools: Option<BTreeSet<String>>,
286    /// When `None`, falls back to `Config::disabled_skill_ids()`.
287    pub disabled_skill_ids: Option<BTreeSet<String>>,
288    pub selected_skill_ids: Option<Vec<String>>,
289    pub selected_skill_mode: Option<String>,
290    pub image_fallback: Option<ImageFallbackConfig>,
291    pub gold_config: Option<GoldConfig>,
292    /// Optional guardian adversarial-review gate configuration.
293    pub guardian_config: Option<GuardianConfig>,
294    /// Late-bound spawner for the guardian reviewer child (wired by the server;
295    /// the runner cannot construct a child directly).
296    pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
297    /// Late-bound hook that arranges a self-resume after a background-bash
298    /// suspend (issue #84 Phase 2b). Wired by the server.
299    pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
300    /// Bamboo application data directory (typically `~/.bamboo`).
301    pub app_data_dir: Option<std::path::PathBuf>,
302}
303
304// ---------------------------------------------------------------------------
305// ExecuteRequestBuilder — ergonomic construction of ExecuteRequest
306// ---------------------------------------------------------------------------
307
308/// Fluent builder for [`ExecuteRequest`].
309///
310/// `ExecuteRequest` carries three required fields plus a long tail of optional
311/// overrides; constructing it by hand forces callers to spell out ~20 `None`s.
312/// This builder requires only `initial_message` + `event_tx` + `cancel_token`,
313/// defaults every optional field to `None` (matching the runtime's spawn
314/// defaults), and exposes fluent setters for the rest.
315///
316/// It lives in `bamboo-engine` so both the in-crate server layer (e.g. the
317/// schedule manager) and the root `bamboo_agent` SDK facade (which re-exports
318/// it) construct requests through one shared builder — no forked assembly.
319pub struct ExecuteRequestBuilder {
320    initial_message: String,
321    event_tx: mpsc::Sender<AgentEvent>,
322    cancel_token: CancellationToken,
323
324    tools: Option<Arc<dyn ToolExecutor>>,
325    provider_override: Option<Arc<dyn LLMProvider>>,
326    // Individual model fields are accumulated here for backward-compatible
327    // fluent setters; `build()` assembles them into a `ModelRoster`. A
328    // `.model_roster(..)` setter seeds all of them at once.
329    model: Option<String>,
330    provider_name: Option<String>,
331    provider_type: Option<String>,
332    fast_model: Option<String>,
333    fast_model_provider: Option<Arc<dyn LLMProvider>>,
334    background_model: Option<String>,
335    background_model_provider: Option<Arc<dyn LLMProvider>>,
336    summarization_model: Option<String>,
337    summarization_model_provider: Option<Arc<dyn LLMProvider>>,
338    reasoning_effort: Option<ReasoningEffort>,
339    auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
340    disabled_tools: Option<BTreeSet<String>>,
341    disabled_skill_ids: Option<BTreeSet<String>>,
342    selected_skill_ids: Option<Vec<String>>,
343    selected_skill_mode: Option<String>,
344    image_fallback: Option<ImageFallbackConfig>,
345    gold_config: Option<GoldConfig>,
346    guardian_config: Option<GuardianConfig>,
347    guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
348    bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
349    app_data_dir: Option<std::path::PathBuf>,
350}
351
352impl ExecuteRequestBuilder {
353    /// Create a builder with the three required fields. All optional overrides
354    /// default to `None`.
355    pub fn new(
356        initial_message: impl Into<String>,
357        event_tx: mpsc::Sender<AgentEvent>,
358        cancel_token: CancellationToken,
359    ) -> Self {
360        Self {
361            initial_message: initial_message.into(),
362            event_tx,
363            cancel_token,
364            tools: None,
365            provider_override: None,
366            model: None,
367            provider_name: None,
368            provider_type: None,
369            fast_model: None,
370            fast_model_provider: None,
371            background_model: None,
372            background_model_provider: None,
373            summarization_model: None,
374            summarization_model_provider: None,
375            reasoning_effort: None,
376            auxiliary_model_resolver: None,
377            disabled_tools: None,
378            disabled_skill_ids: None,
379            selected_skill_ids: None,
380            selected_skill_mode: None,
381            image_fallback: None,
382            gold_config: None,
383            guardian_config: None,
384            guardian_spawner: None,
385            bash_resume_hook: None,
386            app_data_dir: None,
387        }
388    }
389
390    /// Override the tool executor for this execution.
391    pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
392        self.tools = Some(v);
393        self
394    }
395
396    /// Override the LLM provider for this execution.
397    pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
398        self.provider_override = Some(v);
399        self
400    }
401
402    /// Seed the full model selection from a [`ModelRoster`].
403    ///
404    /// Decomposes the roster back into the builder's individual fields so it
405    /// composes with the existing per-field fluent setters; later individual
406    /// setters override the corresponding roster entry.
407    pub fn model_roster(mut self, roster: ModelRoster) -> Self {
408        self.fast_model = roster.fast_model();
409        self.fast_model_provider = roster.fast_model_provider();
410        self.background_model = roster.background_model();
411        self.background_model_provider = roster.background_model_provider();
412        self.summarization_model = roster.summarization_model();
413        self.summarization_model_provider = roster.summarization_model_provider();
414        self.model = roster.model;
415        self.provider_name = roster.provider_name;
416        self.provider_type = roster.provider_type;
417        self
418    }
419
420    /// Override the primary model name.
421    pub fn model(mut self, v: impl Into<String>) -> Self {
422        self.model = Some(v.into());
423        self
424    }
425
426    /// Override the provider name.
427    pub fn provider_name(mut self, v: impl Into<String>) -> Self {
428        self.provider_name = Some(v.into());
429        self
430    }
431
432    /// Override the provider type.
433    pub fn provider_type(mut self, v: impl Into<String>) -> Self {
434        self.provider_type = Some(v.into());
435        self
436    }
437
438    /// Override the fast-model name.
439    pub fn fast_model(mut self, v: impl Into<String>) -> Self {
440        self.fast_model = Some(v.into());
441        self
442    }
443
444    /// Override the provider used for fast-model calls.
445    pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
446        self.fast_model_provider = Some(v);
447        self
448    }
449
450    /// Override the background-model name.
451    pub fn background_model(mut self, v: impl Into<String>) -> Self {
452        self.background_model = Some(v.into());
453        self
454    }
455
456    /// Override the provider used for background/memory model calls.
457    pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
458        self.background_model_provider = Some(v);
459        self
460    }
461
462    /// Override the summarization-model name.
463    pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
464        self.summarization_model = Some(v.into());
465        self
466    }
467
468    /// Override the provider used for summarization/compression calls.
469    pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
470        self.summarization_model_provider = Some(v);
471        self
472    }
473
474    /// Set the reasoning effort.
475    pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
476        self.reasoning_effort = Some(v);
477        self
478    }
479
480    /// Set the per-round auxiliary-model resolver.
481    pub fn auxiliary_model_resolver(
482        mut self,
483        v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
484    ) -> Self {
485        self.auxiliary_model_resolver = Some(v);
486        self
487    }
488
489    /// Set the disabled tool names (merged with config defaults at runtime).
490    pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
491        self.disabled_tools = Some(v);
492        self
493    }
494
495    /// Set the disabled skill ids.
496    pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
497        self.disabled_skill_ids = Some(v);
498        self
499    }
500
501    /// Set the explicitly selected skill ids.
502    pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
503        self.selected_skill_ids = Some(v);
504        self
505    }
506
507    /// Set the skill-selection mode.
508    pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
509        self.selected_skill_mode = Some(v.into());
510        self
511    }
512
513    /// Set the image fallback configuration.
514    pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
515        self.image_fallback = Some(v);
516        self
517    }
518
519    /// Set the internal `gold_config` feature flag.
520    ///
521    /// `gold_config` is an internal feature flag (not part of the public SDK
522    /// surface), so this setter is crate-visible only. Public SDK callers always
523    /// leave it `None`; the in-crate spawn paths thread a resolved value through
524    /// here. Defaults to `None`.
525    pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
526        self.gold_config = v;
527        self
528    }
529
530    /// Set the internal `guardian_config` feature flag (crate-visible, like
531    /// [`Self::gold_config`]). Public SDK callers leave it `None`.
532    pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
533        self.guardian_config = v;
534        self
535    }
536
537    /// Set the late-bound guardian reviewer spawner (crate-visible; wired by the
538    /// server's spawn path so the runner can create the reviewer child).
539    pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
540        self.guardian_spawner = v;
541        self
542    }
543
544    /// Set the late-bound bash self-resume hook (crate-visible; wired by the
545    /// server so a session suspended on background bash is always resumed).
546    pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
547        self.bash_resume_hook = v;
548        self
549    }
550
551    /// Set the Bamboo application data directory.
552    pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
553        self.app_data_dir = Some(v);
554        self
555    }
556
557    /// Materialize the underlying [`ExecuteRequest`].
558    ///
559    /// `gold_config` is an internal feature flag with only a crate-visible
560    /// setter ([`Self::gold_config`]); public SDK callers leave it `None`.
561    pub fn build(self) -> ExecuteRequest {
562        let model_roster = ModelRoster {
563            model: self.model,
564            provider_name: self.provider_name,
565            provider_type: self.provider_type,
566            fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
567            background: RoleModel::from_parts(
568                self.background_model,
569                self.background_model_provider,
570            ),
571            summarization: RoleModel::from_parts(
572                self.summarization_model,
573                self.summarization_model_provider,
574            ),
575        };
576        ExecuteRequest {
577            initial_message: self.initial_message,
578            event_tx: self.event_tx,
579            cancel_token: self.cancel_token,
580            tools: self.tools,
581            provider_override: self.provider_override,
582            model_roster,
583            reasoning_effort: self.reasoning_effort,
584            auxiliary_model_resolver: self.auxiliary_model_resolver,
585            disabled_tools: self.disabled_tools,
586            disabled_skill_ids: self.disabled_skill_ids,
587            selected_skill_ids: self.selected_skill_ids,
588            selected_skill_mode: self.selected_skill_mode,
589            image_fallback: self.image_fallback,
590            gold_config: self.gold_config,
591            guardian_config: self.guardian_config,
592            guardian_spawner: self.guardian_spawner,
593            bash_resume_hook: self.bash_resume_hook,
594            app_data_dir: self.app_data_dir,
595        }
596    }
597}
598
599// ---------------------------------------------------------------------------
600// Helpers
601// ---------------------------------------------------------------------------
602
603/// Extract the system prompt from session messages.
604fn extract_system_prompt(session: &Session) -> Option<String> {
605    session
606        .messages
607        .iter()
608        .find(|m| matches!(m.role, Role::System))
609        .map(|m| m.content.clone())
610}
611
612// ---------------------------------------------------------------------------
613// Execution
614// ---------------------------------------------------------------------------
615
616impl AgentRuntime {
617    /// Execute the agent loop with the given request.
618    ///
619    /// Builds an [`AgentLoopConfig`] from the request parameters and shared
620    /// runtime resources, then delegates to [`run_agent_loop_with_config`].
621    pub async fn execute(
622        &self,
623        session: &mut Session,
624        req: ExecuteRequest,
625    ) -> crate::runtime::runner::Result<()> {
626        let system_prompt = extract_system_prompt(session);
627        let config = self.config.read().await;
628        let ExecuteRequest {
629            initial_message,
630            event_tx,
631            cancel_token,
632            tools,
633            provider_override,
634            model_roster,
635            reasoning_effort,
636            auxiliary_model_resolver,
637            disabled_tools,
638            disabled_skill_ids,
639            selected_skill_ids,
640            selected_skill_mode,
641            image_fallback,
642            gold_config,
643            guardian_config,
644            guardian_spawner,
645            bash_resume_hook,
646            app_data_dir,
647        } = req;
648        let tools = tools.unwrap_or_else(|| self.default_tools.clone());
649        let llm = provider_override.unwrap_or_else(|| self.provider.clone());
650
651        // Decompose the roster back into the loose locals the resolution logic
652        // below expects. This preserves the byte-for-byte `None → Config`
653        // fallbacks: a `None` role yields `None` name/provider exactly as the
654        // old loose fields did.
655        let fast_model = model_roster.fast_model();
656        let fast_model_provider = model_roster.fast_model_provider();
657        let background_model = model_roster.background_model();
658        let background_model_provider = model_roster.background_model_provider();
659        let summarization_model = model_roster.summarization_model();
660        let summarization_model_provider = model_roster.summarization_model_provider();
661        let ModelRoster {
662            model,
663            provider_name,
664            provider_type,
665            ..
666        } = model_roster;
667
668        let loop_config = AgentLoopConfig {
669            max_rounds: 200,
670            system_prompt,
671            disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
672            selected_skill_ids,
673            selected_skill_mode,
674            skill_manager: Some(self.skill_manager.clone()),
675            skip_initial_user_message: true,
676            storage: Some(self.storage.clone()),
677            persistence: Some(self.persistence.clone()),
678            attachment_reader: Some(self.attachment_reader.clone()),
679            metrics_collector: Some(self.metrics_collector.clone()),
680            model_name: model,
681            fast_model_name: fast_model.or_else(|| config.get_fast_model()),
682            fast_model_provider,
683            background_model_name: background_model
684                .or_else(|| config.get_memory_background_model()),
685            planning_model_name: config
686                .defaults
687                .as_ref()
688                .and_then(|d| d.planning.as_ref())
689                .map(|r| r.model.clone()),
690            search_model_name: config
691                .defaults
692                .as_ref()
693                .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
694                .map(|r| r.model.clone()),
695            compression_instructions: None,
696            summarization_model_name: summarization_model
697                .or_else(|| config.get_task_summary_model()),
698            background_model_provider,
699            summarization_model_provider,
700            provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
701            provider_type,
702            reasoning_effort,
703            auxiliary_model_resolver,
704            disabled_tools: {
705                let mut merged = config.disabled_tool_names();
706                if let Some(dt) = disabled_tools {
707                    merged.extend(dt);
708                }
709                merged
710            },
711            image_fallback,
712            app_data_dir,
713            prompt_memory_flags: config
714                .memory
715                .as_ref()
716                .map(PromptMemoryFlags::from)
717                .unwrap_or_default(),
718            features_dynamic_model_routing: config.features.dynamic_model_routing,
719            permission_mode: session
720                .agent_runtime_state
721                .as_ref()
722                .and_then(|state| state.plan_mode.as_ref())
723                .map(|_| PermissionMode::Plan),
724            gold_config,
725            guardian_config,
726            guardian_spawner,
727            bash_resume_hook,
728            // Capture the tool executor's server-level guidance (connected MCP
729            // servers' `instructions`) once, so it lands in the system prompt only
730            // while those servers are loaded for this run.
731            mcp_tool_guidance: tools.tool_guidance(),
732            ..Default::default()
733        };
734
735        drop(config);
736
737        run_agent_loop_with_config(
738            session,
739            initial_message,
740            event_tx,
741            llm,
742            tools,
743            cancel_token,
744            loop_config,
745        )
746        .await
747    }
748}