Skip to main content

bamboo_engine/runtime/
model_roster.rs

1//! Cohesive model/provider value object shared by the request/spawn param structs.
2//!
3//! Historically the per-request model selection was a "data clump" of ~8 loose
4//! fields (`model`, `provider_name`, `provider_type`, plus three auxiliary
5//! role pairs `<role>_model` + `<role>_model_provider`) repeated verbatim across
6//! [`ExecuteRequest`](crate::ExecuteRequest),
7//! [`ExecuteRequestBuilder`](crate::ExecuteRequestBuilder),
8//! `SessionExecutionArgs`, and the server's `ResolvedRunConfig` /
9//! `SpawnAgentExecution`. [`ModelRoster`] collapses that clump into one value
10//! object so the param structs carry a single cohesive field.
11//!
12//! ## Lives in `bamboo-engine`
13//!
14//! [`RoleModel::provider_override`] is an `Arc<dyn LLMProvider>`, and
15//! `LLMProvider` lives in `bamboo-infrastructure`. `bamboo-domain` does **not**
16//! depend on `bamboo-infrastructure`, so hosting the roster there would force a
17//! new runtime dependency just to carry a live provider handle. `bamboo-engine`
18//! already depends on infrastructure (and is depended on by the server), making
19//! it the natural home for a value object that carries live provider handles.
20//!
21//! ## Resolution semantics are preserved byte-for-byte
22//!
23//! The roster carries *intent* only; it performs no `Config` fallback itself.
24//! A `None` auxiliary role maps exactly to the old "both name and provider were
25//! `None`" state, so the downstream `None → Config::get_*` fallbacks in
26//! [`AgentRuntime::execute`](crate::AgentRuntime) are unchanged. The primary
27//! `model`/`provider_name`/`provider_type` keep the same request→config cascade
28//! at their resolution sites.
29
30use std::sync::Arc;
31
32use bamboo_llm::LLMProvider;
33
34/// A single auxiliary role's model selection: an optional model name plus an
35/// optional dedicated provider handle.
36///
37/// Both fields are independent and optional so the mapping from the old loose
38/// `<role>_model: Option<String>` + `<role>_model_provider: Option<Arc<…>>`
39/// pair is lossless. A role is represented as `Some(RoleModel)` only when at
40/// least one of the two was set; an all-`None` role collapses to `None` on the
41/// owning [`ModelRoster`], preserving the old "fall back to `Config`" behavior.
42#[derive(Clone, Default)]
43pub struct RoleModel {
44    /// Model name for this role. `None` defers to the relevant `Config::get_*`
45    /// fallback at the resolution site.
46    pub name: Option<String>,
47    /// Optional dedicated provider handle for this role's LLM calls. `None`
48    /// uses the shared agent-loop provider.
49    pub provider_override: Option<Arc<dyn LLMProvider>>,
50}
51
52impl RoleModel {
53    /// Build a role from the old loose pair, collapsing an all-`None` pair to
54    /// `None` so it maps exactly onto the previous fallback behavior.
55    pub fn from_parts(
56        name: Option<String>,
57        provider_override: Option<Arc<dyn LLMProvider>>,
58    ) -> Option<Self> {
59        if name.is_none() && provider_override.is_none() {
60            None
61        } else {
62            Some(Self {
63                name,
64                provider_override,
65            })
66        }
67    }
68}
69
70/// Cohesive value object for a request's primary + auxiliary model selection.
71///
72/// This replaces the loose model/provider field clump on the per-request param
73/// structs. Capability-driven models (`planning_model_name`,
74/// `search_model_name`) are intentionally **not** part of the roster — they are
75/// resolved separately from `Config` and are not role-driven.
76#[derive(Clone, Default)]
77pub struct ModelRoster {
78    /// Primary (main chat) model name. `None` defers to the config default at
79    /// the resolution site (see [`AgentRuntime::execute`](crate::AgentRuntime)).
80    pub model: Option<String>,
81    /// Provider routing key for the primary model. `None` cascades to
82    /// `Config::provider` at the resolution site.
83    pub provider_name: Option<String>,
84    /// Underlying provider type for the primary model (e.g. `openai`,
85    /// `anthropic`, `copilot`). Distinct from `provider_name` so
86    /// provider-specific behavior stays correct when the routing key is an
87    /// instance id.
88    pub provider_type: Option<String>,
89    /// Fast/cheap role. `None` falls back to `Config::get_fast_model()`.
90    pub fast: Option<RoleModel>,
91    /// Memory/background role. `None` falls back to
92    /// `Config::get_memory_background_model()`.
93    pub background: Option<RoleModel>,
94    /// Summarization/compression role. `None` falls back to
95    /// `Config::get_task_summary_model()`.
96    pub summarization: Option<RoleModel>,
97}
98
99impl ModelRoster {
100    /// A roster with no overrides — every selection defers to `Config`.
101    pub fn empty() -> Self {
102        Self::default()
103    }
104
105    /// Build a roster from a resolved [`GlobalAreaModels`] plus the primary
106    /// model selection. Centralizes the fast/background/summarization mapping
107    /// (`RoleModel::from_parts(area.model_name, area.provider)`) that the
108    /// execute handler and schedule manager previously hand-rolled identically.
109    pub fn from_areas(
110        model: Option<String>,
111        provider_name: Option<String>,
112        provider_type: Option<String>,
113        areas: crate::model_areas::GlobalAreaModels,
114    ) -> Self {
115        Self {
116            model,
117            provider_name,
118            provider_type,
119            fast: RoleModel::from_parts(
120                areas.fast.as_ref().map(|m| m.model_name.clone()),
121                areas.fast.map(|m| m.provider),
122            ),
123            background: RoleModel::from_parts(
124                areas.background.as_ref().map(|m| m.model_name.clone()),
125                areas.background.map(|m| m.provider),
126            ),
127            summarization: RoleModel::from_parts(
128                areas.summarization.as_ref().map(|m| m.model_name.clone()),
129                areas.summarization.map(|m| m.provider),
130            ),
131        }
132    }
133
134    /// Fast-model name override, if any.
135    pub fn fast_model(&self) -> Option<String> {
136        self.fast.as_ref().and_then(|r| r.name.clone())
137    }
138
139    /// Fast-model dedicated provider handle, if any.
140    pub fn fast_model_provider(&self) -> Option<Arc<dyn LLMProvider>> {
141        self.fast.as_ref().and_then(|r| r.provider_override.clone())
142    }
143
144    /// Background-model name override, if any.
145    pub fn background_model(&self) -> Option<String> {
146        self.background.as_ref().and_then(|r| r.name.clone())
147    }
148
149    /// Background-model dedicated provider handle, if any.
150    pub fn background_model_provider(&self) -> Option<Arc<dyn LLMProvider>> {
151        self.background
152            .as_ref()
153            .and_then(|r| r.provider_override.clone())
154    }
155
156    /// Summarization-model name override, if any.
157    pub fn summarization_model(&self) -> Option<String> {
158        self.summarization.as_ref().and_then(|r| r.name.clone())
159    }
160
161    /// Summarization-model dedicated provider handle, if any.
162    pub fn summarization_model_provider(&self) -> Option<Arc<dyn LLMProvider>> {
163        self.summarization
164            .as_ref()
165            .and_then(|r| r.provider_override.clone())
166    }
167
168    /// Set the fast role from the old loose pair.
169    pub fn set_fast(&mut self, name: Option<String>, provider: Option<Arc<dyn LLMProvider>>) {
170        self.fast = RoleModel::from_parts(name, provider);
171    }
172
173    /// Set the background role from the old loose pair.
174    pub fn set_background(&mut self, name: Option<String>, provider: Option<Arc<dyn LLMProvider>>) {
175        self.background = RoleModel::from_parts(name, provider);
176    }
177
178    /// Set the summarization role from the old loose pair.
179    pub fn set_summarization(
180        &mut self,
181        name: Option<String>,
182        provider: Option<Arc<dyn LLMProvider>>,
183    ) {
184        self.summarization = RoleModel::from_parts(name, provider);
185    }
186}