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